2 points par GN⁺ 2024-10-19 | 1 commentaires | Partager sur WhatsApp
  • Tutoriel qui prolonge l’architecture ECS et la programmation métalinguistique sur un environnement de développement de jeux en Common Lisp, à travers un exemple concret de dungeon crawler
  • Après lecture d’une carte Tiled XML avec cl-tiled, les objets CLOS ne sont pas utilisés tels quels : ils sont transférés vers des composants ECS afin de séparer rendu, collisions et gestion mémoire
  • En combinant des préfabs de tuiles, des pointeurs d’images, des index parent-enfant et des finalizers, on évite le chargement en double et les double free, tout en exploitant les propriétés personnalisées de Tiled comme des données
  • Le joueur et les ennemis utilisent des systèmes ECS pour gérer déplacement, transitions d’animation et collisions ; les ennemis poursuivent le joueur en évitant les murs grâce à une recherche de chemin A* basée sur cl-astar
  • Une UI basée sur Nuklear, des objets narratifs, la pause et une condition de victoire sont ajoutés pour aboutir à un petit exemple de dungeon crawler d’environ 500 lignes

Démarrage du projet et exécution de base

  • On crée un petit dungeon crawler avec UI en utilisant l’architecture Entity-Component-System et les techniques de programmation métalinguistique abordées dans la partie 1
  • Le binaire de démonstration exécutable et le code source se trouvent dans le dépôt GitHub ecs-tutorial-2
  • L’environnement de développement suppose celui de développement de jeux en Common Lisp de la partie 1, et la distribution Quicklisp est mise à jour depuis le REPL SBCL
    • (ql-util:without-prompting (ql:update-all-dists))
  • On crée un nouveau projet ecs-tutorial-2 avec le template cookiecutter-lisp-game ; dans l’exemple, liballegro est choisi comme backend
  • Après avoir lié le répertoire du projet à local-projects de Quicklisp, on modifie la taille de la fenêtre dans src/main.lisp à 1280×800
  • En exécutant (ql:quickload :ecs-tutorial-2) puis (ecs-tutorial-2:main), une fenêtre noire à la résolution indiquée et un compteur de FPS s’affichent

Carte Tiled et stockage ECS

  • La création de la carte du donjon utilise l’éditeur de cartes open source Tiled
    • Tiled est un outil multiplateforme et multi-moteur, qui stocke les données de carte en XML
    • En Common Lisp, cl-tiled charge les fichiers Tiled sous forme d’objets Lisp
  • L’exemple utilise le tileset Dungeon Tileset II - Extended
    • Les tuiles originales en 16×16 étant petites, elles sont agrandies à 200 % avec ImageMagick pour être utilisées comme tuiles 32×32
    • level1.tmx et le fichier de tileset sont disponibles dans Resources.zip fourni par le tutoriel
  • On ajoute la dépendance cl-tiled à ecs-tutorial-2.asd, puis on crée src/map.lisp pour séparer le code de chargement et d’affichage de la carte
  • Dans src/package.lisp, cl-tiled est enregistré sous le surnom local tiled

Pourquoi transférer les objets CLOS vers des composants ECS

  • cl-tiled renvoie les données de carte sous forme d’objets CLOS, ce qui les rend pratiques à explorer dans le REPL
  • Les utiliser directement dans la boucle de jeu peut entraîner un coût de dispatch à l’exécution important
    • Remplir une fenêtre 1280×800 avec des tuiles 32×32 nécessite au minimum 40×25 = 1000 tuiles
    • Dans une démonstration distincte, sur un Ryzen 5 3600 à 12 cœurs, l’activation du rendu de la carte fait chuter les FPS de 20 000 à 600
    • Cela ajoute environ 1/600 - 1/20000 = 0.0016 seconde par frame, soit plus de 1,5 ms
  • Transférer les données lues par cl-tiled vers le stockage cl-fast-ecs permet de réduire le dispatch et d’améliorer l’utilisation du cache CPU
  • On ajoute la dépendance cl-fast-ecs, puis on appelle ecs:make-storage dans init et ecs:run-systems dans update

Composants de carte, tuiles et préfabs

  • map est un composant tag qui indique l’entité de carte chargée
  • map-tile représente une tuile individuelle et possède un slot booléen obstacle pour indiquer s’il s’agit d’un obstacle, comme un mur ou une porte fermée
  • Le composant parent indique de quelle entité de carte une tuile ou un objet lié à la carte est l’enfant
    • Le slot entity reçoit :index children afin de retrouver rapidement les entités enfants d’un parent donné
    • L’index repose sur une table de hachage à adressage ouvert et offre donc des recherches en O(1) en moyenne, mais implique un coût de mise à jour lors des créations et suppressions
  • Un hook est ajouté à ecs:*entity-deleting-hook* afin que, lorsqu’une entité parent est supprimée, les entités enfants trouvées via l’index children soient également supprimées
  • Le composant image ne stocke que le pointeur C vers ALLEGRO_BITMAP
    • L’image du tileset est découpée en fragments 32×32 avec al_create_sub_bitmap, puis les pointeurs sont stockés
  • map-tile-prefab est un préfab de tuile portant le gid, l’ID global de la tuile Tiled
    • gid reçoit :index map-tile-prefab :unique t afin de retrouver une unique entité préfab par ID
    • Les tuiles réelles sur la carte copient notamment l’image du préfab, mais possèdent leur propre composant position
  • Le finalizer de image n’appelle al_destroy_bitmap que lorsque l’entité est un map-tile-prefab
    • Cela permet d’éviter un double free, car plusieurs tuiles de la carte partagent le même pointeur ALLEGRO_BITMAP
  • position et size stockent les coordonnées à l’écran et la taille en single-float
    • liballegro manipulant les coordonnées d’écran en flottants simple précision pour des raisons de compatibilité OpenGL, le même choix est conservé

Rendu des images et chargement de la carte

  • Le système render-images rend les entités qui possèdent position et image
    • al_hold_bitmap_drawing active et désactive le sprite batching
    • al_draw_bitmap dessine l’image aux coordonnées indiquées
    • Les préfabs n’ayant pas de position, ils ne sont pas traités par ce système
  • load-bitmap est une fonction de chargement d’image qui enveloppe al_load_bitmap avec al:ensure-loaded
  • tile->spec construit la spécification d’objet ECS pour créer un préfab de tuile
    • Entité de carte parente
    • Fragment d’image de la tuile
    • ID global de tuile Tiled
    • Taille de la tuile
  • load-tile-prefab vérifie via l’index map-tile-prefab si le préfab est déjà chargé et, sinon, le crée avec make-object
  • load-tile copie les composants du préfab lors de la création d’une entité de tuile réelle de la carte, puis ajoute position
  • load-map parcourt les tilesets et les couches à partir de l’objet CLOS lu par tiled:load-map
    • Il charge l’image du tileset et crée chaque tuile comme préfab
    • Il crée une entité pour chaque cellule des couches de tuiles et copie les données du préfab
  • L’ordre des couches de Tiled est conservé tel qu’il apparaît dans l’éditeur, et make-entity garantit des numéros d’entités croissants
    • Comme les systèmes traitent les entités les plus anciennes en premier, les tuiles des couches supérieures sont dessinées plus tard et recouvrent les couches inférieures
  • Stocker toutes les tuiles comme entités distinctes n’est pas la seule solution ; il est aussi possible de pré-rendre une carte statique dans un buffer

Animation des tuiles

  • Tiled prend en charge les tuiles animées, ce qui permet de représenter des éléments comme des torches ou des fontaines magiques
  • Ajout de common.lisp et animation.lisp afin de séparer les composants communs des composants et systèmes liés à l’animation
  • Le composant animation-frame représente une image d’une animation
    • sequence est le nom de l’animation, stocké avec le type keyword
    • L’index sequence-frames permet de retrouver les images d’une animation donnée
    • duration est la durée d’affichage de l’image, en secondes
  • animation-state stocke l’état courant d’une tuile animée sur la carte réelle
    • La sequence courante
    • La frame courante
    • La duration de l’image courante
    • Le temps elapsed écoulé depuis l’affichage de l’image courante
  • Ajout de la dépendance let-plus pour écrire plus simplement le code de changement d’image
  • Le système update-animations incrémente elapsed de dt et passe à l’image suivante lorsque la durée est dépassée
    • Comme la durée d’une image peut être inférieure à un grand dt, floor sert à calculer combien d’images doivent être sautées
    • truncate permet de revenir au début lorsque le numéro d’image dépasse la longueur de la liste
    • Le pointeur bitmap de image est remplacé par le bitmap du prefab de l’image suivante
  • Les durées d’animation étant stockées en millisecondes dans Tiled, animation->spec les convertit en secondes
  • instantiate-animation crée un animation-state sur l’entité de tuile réelle et initialise elapsed avec une valeur aléatoire entre 0 et duration, afin que les animations identiques ne soient pas parfaitement synchronisées
  • Les tuiles animées doivent avoir la propriété Tiled "sequence"
    • Sans cette propriété, elles sont chargées avec le nom NIL, ne peuvent pas être retrouvées sous le nom d’animation attendu et peuvent provoquer une erreur de type

Personnage joueur et contrôles

  • Ajout de character.lisp et définition du composant character pour les personnages pouvant se déplacer
    • speed est la vitesse en pixels par seconde
    • target-x et target-y sont les coordonnées de destination du déplacement
    • Les valeurs cibles initiales sont définies à single-float-nan afin qu’un nouveau personnage ne se déplace pas sans raison vers le coin supérieur gauche
  • Le composant tag player utilise un slot bit et :index player-entity :unique t
    • Cette structure permet de trouver l’entité joueur en O(1) avec (player-entity 1)
    • L’entité joueur n’est pas stockée dans une variable globale
  • Dans l’implémentation initiale, une image d’orc est découpée depuis le tileset pour créer player.png, puis le joueur est créé en dur avec load-player
    • Position : (64.0, 64.0)
    • Taille : 32×32
    • Vitesse : 100.0
  • Le système move-characters déplace le personnage jusqu’à son point cible
    • Si les coordonnées cibles sont NaN, elles sont initialisées à la position courante
    • approx-equal est utilisé au lieu d’une comparaison directe de nombres à virgule flottante
    • Les nouvelles coordonnées sont calculées avec atan, cos, sin, la vitesse et dt
  • Le système control-player lit les touches W, A, S, D et met à jour les coordonnées cibles
    • Il utilise al:with-current-keyboard-state et al:key-down
    • clamp empêche de sortir des limites de l’écran
    • Il s’exécute après le système de déplacement avec :after (move-characters), afin d’éviter le problème d’initialisation à NaN

Chargement des collisions et des objets via les propriétés Tiled

  • Au départ, les murs sont de simples images comme les tuiles de sol, si bien que le joueur les traverse
  • Création d’une classe map-tile comme type personnalisé Tiled, avec ajout d’un membre Boolean obstacle
    • La propriété map-tile est ajoutée aux tuiles de mur et obstacle est coché
  • La fonction properties->spec convertit la table de hachage des propriétés Tiled en spécification d’objet ECS
    • Les classes personnalisées Tiled sont traitées comme des composants
    • Les membres de classe sont traités comme des slots de composant
    • Exemple sous la forme ((:map-tile :obstacle t))
  • load-tile-prefab inclut le résultat de properties->spec dans la spécification du prefab
    • S’il n’y a pas de propriété, spec-adjoin ajoute un composant map-tile par défaut et obstacle prend la valeur par défaut nil
  • Ajout d’un slot tile-hash et d’un index tiles au composant position
    • tile-hash convertit x et y en entiers, puis les compacte dans un seul entier 64 bits
    • Toutes les entités situées aux coordonnées du coin supérieur gauche d’une tuile donnée peuvent être retrouvées avec l’index tiles
  • tile-start renvoie les coordonnées du coin supérieur gauche de la tuile de grille contenant des coordonnées arbitraires
  • tile-obstacle-p vérifie si, parmi les entités aux mêmes coordonnées, il existe une tuile map-tile dont obstacle est vrai
  • obstaclep vérifie si la tuile correspondant à des coordonnées arbitraires est un obstacle
  • control-player examine, selon la direction du déplacement, les tuiles des coins pertinents du rectangle du personnage et, s’il y a un obstacle, remet les coordonnées cibles à la position courante
  • Cette méthode de collision n’est pas parfaite
    • Une conception fondée sur les coordonnées du centre du personnage pourrait simplifier les maths et le code, mais l’exemple conserve l’approche actuelle pour éviter davantage de complexité

Chargement du joueur et des personnages animés depuis la carte

  • Ajout des classes personnalisées character et player dans Tiled
    • character ne possède qu’un membre float speed
    • target-x et target-y sont omis afin d’utiliser les valeurs par défaut
    • player possède un membre int player, avec 1 comme valeur par défaut
  • Le personnage joueur est placé comme objet tuile sur une couche d’objets Tiled, avec les propriétés character et player
  • load-map est étendu pour traiter aussi tiled:object-layer
    • Les propriétés des objets sont converties en composants ECS avec properties->spec
    • tiled:tile-object copie les données de tuile et l’animation avec load-tile, puis définit la position
    • Les coordonnées d’objet Tiled étant basées sur le coin inférieur gauche, on soustrait la hauteur de l’objet à y pour les aligner sur le coin supérieur gauche
  • L’appel à load-player codé en dur et la fonction correspondante sont supprimés
  • Cette structure lit directement les données de carte Tiled sous forme d’objets ECS, ce qui se rapproche d’une programmation pilotée par les données
  • L’animation du personnage utilise les séquences orc-idle et orc-run de l’orc, définies dans le tileset
  • change-animation-sequence change l’animation courante d’une entité
    • Si la séquence est déjà la même, elle ne fait rien
    • Elle trouve la première image de la nouvelle séquence via l’index sequence-frames, puis met à jour animation-state et image-bitmap
  • move-characters bascule vers :orc-idle lorsque le personnage est immobile, et vers :orc-run lorsqu’il se déplace

Ennemis, game over et recherche de chemin A*

  • Le composant enemy possède deux slots nécessaires au comportement des ennemis
    • vision-range : distance à partir de laquelle l’ennemi voit le joueur et commence à réagir
    • attack-range : portée d’attaque
  • Pour mettre fin à la partie, une variable globale *should-quit* est ajoutée, et la boucle principale se termine si cette valeur est vraie
  • Le système handle-enemies récupère les coordonnées du joueur et les compare à celles des ennemis
    • Si le joueur est dans le champ de vision, les coordonnées cibles de l’ennemi sont définies sur la position du joueur
    • Si le joueur est à portée d’attaque, *should-quit* est défini sur vrai et une boîte de message native You died s’affiche
  • Les animations des ennemis utilisent les séquences demon-idle et demon-run
    • move-characters choisit l’animation d’orc pour le joueur et l’animation de démon pour les ennemis selon le résultat de has-player-p
  • Avec le suivi direct, les ennemis traversent eux aussi les murs ; une recherche de chemin A* est donc ajoutée
  • cl-astar est ajouté aux dépendances
    • Cette bibliothèque génère, au moyen d’une macro, une fonction de recherche de chemin optimisée pour le problème donné
  • Le chemin n’est pas stocké sous forme de tableau dans un slot de composant : chaque point du chemin est représenté par une entité distincte
    • path-point possède x, y et traveller, et traveller dispose d’un index path-points
    • path stocke la destination finale destination-x, destination-y
    • Les coordonnées cibles de character représentent le prochain point du chemin, tandis que path représente la destination finale
  • Le système follow-path récupère le premier point du chemin et déplace le personnage vers ce point
    • Lorsque le point est atteint, l’entité path-point correspondante est supprimée
    • S’il n’y a plus de point, le composant path est supprimé
  • find-path est défini avec a*:define-path-finder
    • La taille du monde est calculée en divisant la taille de la fenêtre par la taille des tuiles
    • Un indexeur en row-major est utilisé
    • L’arrivée à l’objectif est déterminée par l’égalité des coordonnées de tuile
    • Les voisins sont énumérés dans 8 directions
    • Les obstacles, ou les déplacements diagonaux qui traversent un obstacle, reçoivent un coût most-positive-single-float, ce qui les rend pratiquement impossibles
    • L’heuristique utilise l’octile distance
    • S’il existe déjà un chemin, ses points sont supprimés et un nouveau path est attribué
    • Chaque point du chemin résultant est créé comme une entité possédant path-point et parent
  • Quand un ennemi voit le joueur, handle-enemies appelle find-path si la destination du chemin existant diffère de la position du joueur
  • Après ces changements, les ennemis poursuivent le joueur tout en contournant les obstacles

UI de jeu basée sur Nuklear

  • Une GUI est nécessaire pour les éléments narratifs, mais les bibliothèques GUI traditionnelles comme Qt ou GTK ne conviennent pas à une UI de jeu dessinée dans le contexte graphique de liballegro
  • Nuklear est utilisé comme bibliothèque d’UI
    • Il existe un binding Common Lisp, cl-liballegro-nuklear, pour l’utiliser avec liballegro
    • Le binding fournit aussi un DSL pour une interface déclarative
  • La dépendance cl-liballegro-nuklear/declarative est ajoutée, ainsi qu’un nouveau fichier src/narrative.lisp
  • Un surnom local ui est enregistré dans le paquet afin de référencer plus brièvement cl-liballegro-nuklear/declarative
  • La police UI utilisée est Alegreya de Google Fonts, et le fichier est renommé alegreya-sc.ttf
  • ui:defwindow narrative définit la fonction de fenêtre narrative
    • La position de la fenêtre est calculée pour la placer dans la zone centrale de l’écran
    • ui:label-wrap affiche du texte avec retour à la ligne automatique
    • ui:button-label "Ok" renvoie vrai lorsqu’il est cliqué
  • Nuklear est une bibliothèque d’UI en immediate mode
    • Elle ne conserve pas des objets widgets en mémoire comme en retained mode, mais effectue le rendu et le traitement à chaque frame
    • Un clic sur un bouton n’est pas géré par callback, mais par une valeur de retour et des conditions à chaque frame
  • main.lisp charge la police UI et initialise le contexte UI avec nk:allegro-init
    • Dans la boucle d’événements, nk:input-begin, nk:allegro-handle-event et nk:input-end sont appelés
    • Lors du rendu, nk:allegro-render est appelé
    • À la fermeture, nk:allegro-shutdown et nk:allegro-font-del sont appelés

Skin UI et objets narratifs

  • Comme l’UI par défaut est monotone, elle est stylisée avec les assets d’image fantasy-ui-borders de Kenney
  • Les images UI sont stockées dans les variables globales *window-background*, *button-normal-background*, *button-hover-background* et *button-active-background*
  • load-ui charge les images avec nk:allegro-create-image, et unload-ui libère les ressources d’image côté C avec nk:allegro-del-image
  • load-ui est appelé dans init, et unload-ui est appelé à la fin de la boucle principale
  • L’argument :styles de ui:defwindow précise l’arrière-plan, les images selon l’état des boutons et la couleur du texte
  • Le composant narrative représente un objet destiné à la narration environnementale
    • text : texte à afficher
    • shown : indique si le texte a déjà été affiché une fois
    • active : indique si la fenêtre est actuellement active
    • active dispose d’un index active-narratives
  • Le système show-narrative affiche une fenêtre lorsque le joueur se trouve près d’un objet narratif
    • La distance d’interaction est calculée avec +interact-distance-factor+ et la taille de tuile du joueur
    • La fenêtre s’affiche si elle est déjà active, si elle n’a encore jamais été affichée, ou si la touche E est enfoncée
    • La fenêtre se ferme avec le bouton Ok, ou avec Esc, Space ou Enter
  • Un type personnalisé narrative est créé dans Tiled, avec un membre string text
  • Pour correspondre à la détection des collisions, les objets infranchissables doivent être alignés sur la grille de tuiles
  • Le problème du donjon qui continue à bouger quand la fenêtre narrative est affichée est évité via des conditions d’exécution des systèmes
    • :when (null (active-narratives t)) est ajouté à move-characters et control-player
    • S’il existe une narration active, les systèmes de déplacement et de contrôle ne s’exécutent pas
  • La condition de victoire est ajoutée sous forme de composant tag win
    • Lorsque la fenêtre d’un objet auquel narrative est aussi attaché est fermée, *should-quit* est défini sur vrai et la partie se termine

Conclusion et périmètre

  • L’exemple final utilise cl-fast-ecs, cl-tiled, cl-astar et cl-liballegro-nuklear pour construire un dungeon crawler de type Souls-like avec narration environnementale, IA ennemie et GUI
  • La taille de l’implémentation est d’environ 500 lignes de code
  • Le code complet se trouve dans le dépôt GitHub et inclut, en plus du code du tutoriel, des déclarations de type optionnelles utilisant declaim
  • Le sound design, les cinématiques, le menu principal, les transitions de niveau et le « door problem » ne sont pas abordés
  • L’Autumn Lisp Game Jam 2024 aura lieu le 25 octobre 2024 sur itch.io ; c’est un événement où l’on crée un jeu en 10 jours dans un dialecte Lisp, puis où l’on s’évalue mutuellement et échange des retours
  • Cette partie s’appuie sur Thoughtbound, une soumission au Spring Lisp Game Jam 2023
  • La partie suivante annonce le défi de passer à une plus grande échelle et d’ajouter une IA plus avancée pour créer un jeu de stratégie en temps réel

1 commentaires

 
GN⁺ 2024-10-19
Commentaires sur Hacker News
  • J’aimerais que tous les tutoriels techniques ressemblent à ça. La structure est excellente, il n’y a presque pas d’erreurs de grammaire, et chaque nouveau sujet est expliqué avec la juste dose de détail, avec en plus des exemples de code complets et des supports visuels montrant ce que fait réellement le code
    C’est assez long pour traiter le sujet en profondeur, tout en restant suffisamment autonome pour qu’on puisse suivre même sans avoir lu la partie 1, et avec seulement quelques mois de pratique de Common Lisp il y a plusieurs années. J’ai davantage pratiqué Clojure et Emacs Lisp
    Bravo, awkravchuk/Andrew :^)
    (Crossposted depuis https://mxjn.me/2024/10/17/1)

    • Surtout, le plus important, c’est que c’est du texte et pas une vidéo. On peut copier-coller, lire clairement, suivre à son propre rythme et le consommer au calme
      C’est aussi facile à sauvegarder pour un usage hors ligne ou l’archivage, à annoter, et à parcourir par recherche
  • Il y a peu de choses dans la tech qui me touchent autant qu’un excellent projet ou article sur Common Lisp. C’est vraiment un texte qui ressemble à un cadeau
    J’avais lu la partie 1 à sa sortie, et je suis vraiment impatient de lire celle-ci aussi. Bravo à l’auteur

  • Rien que package.sh et, plus largement, toute la gestion des builds sur 3 systèmes d’exploitation relèvent de la masterclass. J’ai déjà beaucoup appris rien qu’en parcourant le dépôt GitHub
    D’habitude, on construit les applications Common Lisp en ligne de commande avec SBCL ou LispWorks, mais la prochaine fois, je pourrais essayer ECL. C’est chouette d’avoir des builds macOS et Linux, et ce serait amusant d’essayer quelque chose de nouveau

    • Je construis cette configuration CI sur une infra CL depuis des années, et d’après lui, ça continue de casser :D
  • Très bon article. Je développe un shooter à sorts multijoueur à la troisième personne en Lisp, plus précisément en ClojureScript. C’est un jeu 3D web, et je compte bloguer sur le parcours, y compris les outils et abstractions que j’ai créés pour le projet
    Si ça vous intéresse, il y a une démo ici : https://wizardmasters.io

    • Jon Blow avait aussi essayé de faire un jeu comme ça il y a longtemps. Regarder comment et pourquoi il a échoué pourrait être instructif
  • L’article est vraiment solide en lui-même, mais quand on voit que la configuration de la partie 1 enchaîne Common Lisp lui-même, Python, C et plusieurs étapes, on comprend pourquoi CL n’est pas particulièrement populaire, surtout chez les jeunes programmeurs
    C’est dommage, et j’aimerais que quelqu’un fasse l’effort de rendre le langage plus accessible côté installation

    • Je ne suis pas sûr que cela vise exactement le même problème, mais https://ciel-lang.org/ essaie au moins de résoudre une partie du problème des trop nombreuses étapes
      Si j’ai bien compris, le projet se concentre davantage sur le fait qu’il y a trop d’options et que les choix par défaut historiques paraissent datés
  • La boucle d’événements est un excellent exemple de à quel point loop est un véritable langage spécifique au domaine pour l’itération. Qu’on aime ou non ;)

    • Pourquoi ne pas utiliser https://iterate.common-lisp.dev/ à la place de loop ? Pas de syntaxe étrange qui ne soit pas une S-expression, et pas besoin non plus de do pour revenir à une syntaxe Lisp
      On peut utiliser les if/when habituels sans les affreux else/end, et ça ajoute globalement des fonctionnalités utiles
    • Au début je m’en moquais, mais après quelques années à programmer en Common Lisp, loop est devenu l’un de mes composants CL préférés
  • Cet article me fait penser à "Caves of Clojure" : https://stevelosh.com/blog/2012/07/caves-of-clojure-01/

  • Justement cette semaine, j’ai commencé à développer un roguelike en Python, mais le faire en Lisp pourrait être sympa

  • Je me sens floué. Je suis venu pour apprendre à faire un petit jeu simple, et j’ai fini par en apprendre énormément sur l’informatique en général
    C’est vraiment excellent