Spring Lisp Game Jam 2026 · Andrey Listopadov
Spring Lisp Game Jam 2026
Sun, May 24, 2026
@programming @gamedev<br>tic80<br>fennel<br>~14 minutes read
This year I decided to participate in the Spring Lisp Game Jam.<br>It’s an annual event, where you have to make a game in any kind of lisp in a limited time, usually a week.
I’ve been putting this away for several years, because every time the jam started I wasn’t ready to spend time on it, because of work or other duties I had at the time.<br>This year, however, I decided to take a vacation, and give it my all.<br>Life, of course, had other plans, but I managed to persevere.
This game jam doesn’t have a particular theme, which is good, as I always wanted to make a particular kind of a game - a movement-oriented platformer.<br>I really enjoyed games like Metroid Fusion and Metroid Dread, which, to my taste, have great platforming, especially in Dread, so I wanted to make something akin to it.<br>On paper, this idea seemed doable.<br>In practice, well, you’ll see.
I decided to make “speed” the main thing of my game, thinking that the core idea will be to maintain speed, and complete room-based obstacle courses.<br>My engine of choice was again TIC-80 as I had some experience with it already, and it’s the most frictionless environment I know for making small games.<br>That’s debatable, of course, given TIC’s restrictions of resolution of 240x136 pixels and a 16-color-only palette.<br>But I like restrictions, they breed creativity, as we all know.
This is more of a postmortem post, as I’m writing this after finishing the game - I simply had no capacity of writing this during the development.<br>So information below is more of how I remember this, and of notes I made along the way.
Day 1
First things first, I chose TIC-80, and it supports programming in lisp out of the box.<br>The language in question is Fennel, which is a nice lisp that compiles to Lua.<br>I needed a few things though.
TIC doesn’t have any built-in functionality besides the essential things for drawing on the screen and reading user’s input.<br>My game needs to deal with collisions, and while other game engines, like LOVE, include a library for that, TIC is certainly not.
A popular library for this is bump.lua, however, TIC doesn’t allow filesystem interactions, so the library must be bundled inside the cart.<br>However, this is a Lua library, and I’m making a game in Fennel.
Fennel has a way of inlining dependencies via the --require-as-include option that could be used, however, it only works when compiling Fennel code to Lua.<br>And I’m not sure if this will pass for the game jam.<br>Plus, Fennel doesn’t compile comments, so I couldn’t just write the game cart in Fennel and then compile it to Lua, I’d need to translate all of the cart metadata by hand.
Luckily, a few years ago I made a port of bump.lua to Fennel for cases like this.<br>I used it in the other game I was making at the time.<br>You can read my old dev-log here.<br>Anyhow, I needed to make an actual game, not just mess with libraries, so I picked a palette and began working.
First day was all about player controller.<br>I needed a solid foundation to build the game around, and none of my previous player controllers were adequate for this.<br>They were too stiff.
At the end of the day I ended up with this:
Your browser does not support the video tag.
This is a bit more than just a player controller - I have a camera system, level editor, placeholder graphics, and powerups already coded.
This is my camera system:
(local camera<br>{:x 96<br>:y 40<br>:update<br>(fn [self {: x : y}]<br>(doto self<br>(tset :x (* (// x 240) 240))<br>(tset :y (* (// y 136) 136))))<br>:draw<br>(fn [{: x : y} remap]<br>(map (// x 8) (// y 8) (+ (// x 8) 31) (+ (// y 8) 18) 0 0 0 1 remap))<br>:translate<br>(fn [{:x cam-x :y cam-y} x y]<br>(values (- x cam-x) (- y cam-y)))})
It’s a simple object that has three main methods.
The update method is responsible for camera positioning.<br>It is called on each frame, and it simply clamps the x, y position into the one full screen range.<br>Thus, this is not a free camera, it is locked to the current screen.
I had dynamic cameras in the past, and while I like them, for this game I wanted something different.<br>Essentially, my vision was that each screen will be an obstacle course, although even at this point I already knew this would be a bit problematic.
The draw method simply draws the current visible range of tiles via the map command.<br>I have a remap callback ready in case I’ll need to change some tiles dynamically.
The final method translate is an interesting one.<br>When I draw anything, like the player, or interactable objects, I need to do so in the camera space.<br>So I call (camera:translate obj.x obj.y) to get screen-space coordinates.<br>In other words, object’s coordinates may be 1337,420, but in camera space they as well may be 42,27.
The camera draws the level from the...