Ce billet a été originalement posté sur mon blog, en anglais. Sa vignette est le logo du site common-lisp.net, sous licence CC BY-SA 4.0.
Cela fait un moment déjà que j’ai envie de (ré)apprendre un dialecte Lisp. Par curiosité. Parce que le langage conserve une très bonne réputation. Parce qu’un jour, il faudra peut-être que je m’attelle à faire des plugins Emacs digne de ce nom pour pijul. Dans le même temps, mon petit serveur de jeu 2D en ligne commence à marchotter. Il n’en fallait pas plus pour que mon cerveau, décidément un peu joueur, ne se décide à mixer les deux.
Voilà comment je me suis retrouvé avec l’objectif un peu saugrenu de faire un client en Lisp pour lykan. J’ai demandé sur Mastodon si quelqu’un connaissait un bon cadriciel pour écrire des jeux en Lisp : on m’a parlé (entre autres suggestions) de trivial-gamekit. J’ai choisi arbitrairement de me lancer dans l’aventure en l’utilisant. Pour l’occasion, je me suis dit que j’allais documenter mes trouvailles, en écrivant de temps en temps des articles sur le sujet. Vous être en train de lire la première occurrence de cette nouvelle série ; mon but ici est de vous montrer comment j’ai obtenu un programme Lisp, graphique et minimal.
Pour le moment, le projet s’appelle lysk. Il est hébergé sur mon serveur et j’utilise pijul comme gestionnaire de version. J’ai rassemblé tout le code présenté dans ce billet dans un gist pour ceux qui ne veulent pas s’embêter (et je les comprends) avec un VCS encore largement inconnu.
- Installer Common Lisp, Quicklisp et trivial-gamekit
- Nom de code : Lysk
- Un programme graphique en Lisp
- Créer un exécutable
Installer Common Lisp, Quicklisp et trivial-gamekit
Le site internet de trivial-gamekit liste quatre prérequis à son utilisation. Deux sont liés à Lisp :
- Quicklisp
- SBCL or CCL
Commençons par le commencement. Quicklisp est une sorte de gestionnaire de paquet Common Lisp dont la peinture est encore fraîche (ce qui peut sembler surprenant, vu l’âge du langage ). C’était facile à découvrir, parce que la documentation de trivial-gamekit fournit gentiment un lien vers le site web de Quicklisp. Par contre, je ne savais pas ce qu’était SBCL ou CCL, donc j’ai mis de côté « histoire de voir ». J’ai installé clisp
(packagé pour ArchLinux), mais impossible d’installer Quicklisp.
La raison ? Elle est simple : clisp
est une implémentation de Common Lisp, mais ce n’est pas la seule. sbcl
et ccl
en sont deux autres ! Comme Quicklisp ne fonctionne pas avec clisp
, je pouvais toujours essayer, ça ne pouvait pas marcher. Heureusement, sbcl
est aussi packagé dans ArchLinux et comme promis, Quicklist s’installe beaucoup mieux avec une implémentation compatible. Je laisse les curieux aller voir la procédure d’installation de Quicklisp sur le site du projet. Elle est assez simple. Quicklisp fonctionne un peu comme Go, avec une notion de workspace (~/quicklisp
par défaut, et les projets doivent être dans ~/quicklisp/local-projects
).
Le cadriciel trivial-gamekit n’est pas dans la distribution par défaut. Heureusement, l’auteur en fournit une qui possède le paquet. On peut l’ajouter à la configuration de Quicklisp avec cette ligne de commande :
1 | sbcl --eval "(ql-dist:install-dist \"http://bodge.borodust.org/dist/org.borodust.bodge.txt\")" --quit
|
Le http n’est malheureusement pas une erreur. Quicklisp ne supporte(ra) pas le protocole, mais il est prévu de laisser la possibilité aux mainteneurs de paquets de signer ces derniers. C’est quand même le strict minimum.
Nom de code : Lysk
La première chose que je regarde quand je veux apprendre un langage, c’est comment organiser un projet. De ce point de vue, trivial-gamekit m’a directement orienté vers Quicklisp.
Créer un nouveau projet pour Quicklisp est assez simple, même si je ne suis pas fan de ces histoires de workspace1. D’abord, il faut créer un répertoire pour le projet. Je vais appeler mon client lysk
, pour le moment (faute de mieux).
1 | mkdir ~/quicklisp/local-projects/lysk |
Ensuite, il faut créer un fichier qui décrit notre projet et définie notamment le ou les paquets qu’il propose. C’est un peu le Cargo.toml
de Rust, ou le .cabal
de Haskell. Dans le cas de Quicklisp/Common Lisp, l’extension est asd
.
Voilà le contenu de lysk.asd
:
1 2 3 4 5 6 7 8 9 | (asdf:defsystem lysk :description "Lykan Game Client" :author "lthms" :license "GPLv3" :version "0.0.1" :serial t :depends-on (trivial-gamekit) :components ((:file "package") (:file "lysk"))) |
La plupart des champs sont documentés rien que par leur nom. Je n’ai eu de mal à comprendre que :serial
. Cette option spécifie en fait que chaque fichier qui constitue notre package dépend de celui qui précède. Dans notre cas, donc, lysk.lisp
dépend de package.lisp
. On aurait pu le spécifier à la main :
1 | (:file "lysk" :depends-on "package") |
Le fichier package.lisp
nous permet de définir l’interface du paquet, en quelque sorte. Voilà son contenu :
1 2 3 | (defpackage :lysk (:use :cl) (:export run app)) |
De ce que j’ai comprs, :cl
est la bibliothèque standard de Common Lisp ; la ligne suivante nous permet de dire que l’on exporte deux symboles : run
et app
.
-
À ce sujet, il existe des méthodes pour assouplir un peu la chose (par exemple, créer des liens symboliques dans le workspace vers les projets, qui peuvent être ailleurs dans l’arborescence. ↩
Un programme graphique en Lisp
Mon but désormais est d’obtenir un programme minimal, qui crée une fenêtre, se met en plein écran et s’arrête de lui-même quand l’utilisateur ou l’utilisatrice relâche le bouton gauche de sa souris. Cela peut paraître peu, mais je rappelle que je suis novice en Lisp. À noter qu’en plus de cela, j’ai dû remonter un bug à l’upstream de trivial-gamekit ! Ce n’est pas forcément rassurant, mais dans le même temps, le problème a été résolu en moins d’une demi-heure et l’auteur a été très bienveillant avec moi quand j’ai rejoint le channel #lispgames
sur Freenode.
Bref, le contenu du fichier lysk.lisp
est très court, mais permet d’obtenir le comportement attendu :
1 2 3 4 5 6 7 8 9 10 11 | (cl:in-package :lysk) (gamekit:defgame app () () (:fullscreen-p 't)) (defmethod gamekit:post-initialize ((app app)) (gamekit:bind-button :mouse-left :released (lambda () (gamekit:stop)))) (defun run () (gamekit:start 'app)) |
La fonction run
, exportée par notre package, permet de lancer effectivement notre « jeu ». Depuis un terminal, il suffit d’écrire :
1 2 3 | $ sbcl # lance le REPL lisp * (ql:quickload :lysk) * (lysk:run) |
Créer un exécutable
Lisp a l’air très tourné vers le REPL-driven development ; c’est encore du chinois pour moi. Tout ce que je sais, c’est que si un jour mon client doit être utilisé par des joueurs, je ne vais par leur demander de lancer leur Lisp. Il me faut un moyen de leur donner un exécutable. « Fort heureusement », trivial-gamekit fournit une fonction pour ça. En suivant les conseils de l’auteur du cadriciel, je me suis retrouvé à définir un second package, dont le but est de fournir une fonction qui créer l’exécutable.
Première étape : modifier le fichier lysk.ads
:
1 2 3 4 5 6 7 8 | (asdf:defsystem lysk/bundle :description "Bundle the Lykan Game Client" :author "lthms" :license "GPLv3" :version "0.0.1" :serial t :depends-on (trivial-gamekit/distribution lysk) :components ((:file "bundle"))) |
La fonction deliver
est implémentée dans le fichier bundle.lisp
:
1 2 3 4 5 6 7 8 | (cl:defpackage :lysk.bundle (:use :cl) (:export deliver)) (cl:in-package :lysk.bundle) (defun deliver () (gamekit.distribution:deliver :lysk 'lysk:app)) |
Pour obtenir l’exécutable, on utilise cette fonction depuis le REPL de sbcl
. Une bonne manière de faire est d’utiliser l’option --eval
pour ne pas avoir à le faire à la main :
1 | sbcl --eval "(ql:quickload :lysk/bundle)" --eval "(lysk.bundle:deliver)" --quit |
Tada !
Voilà qui conclue la première étape de mon voyage à la conquête du Lisp. Prochaine étape ? Installer slime, un plugin Emacs pour avoir un mode Lisp qui patate.
Merci d’avoir lu !
En bonus, le Makefile que j’ai écris pour ne plus avoir à lancer manuellement lysk
depuis le REPL.
1 2 3 4 5 6 7 8 9 10 11 12 13 | run: @sbcl --eval "(ql:quickload :lysk)" \ --eval "(lysk:run)" bundle: @echo -en "[ ] Remove old build" @rm -rf build/ @echo -e "\r[*] Remove old build" @echo "[ ] Building" @sbcl --eval "(ql:quickload :lysk/bundle)" --eval "(lysk.bundle:deliver)" --quit @echo "[*] Building" .PHONY: bundle run |