Become a Spritely supporter today!

We're building the next generation of distributed web technology for secure collaboration and we need your help to fill up our health bar

$19,427 of $80,000 raised so far! Please support Spritely!

Cirkoban: Sokoban meets cellular automata written in Scheme

Dave Thompson —

Last week, we released a small puzzle game called Cirkoban. Cirkoban is the very first publicly accessible application developed by Spritely that features the Goblins distributed programming library running in web browsers. We bet big on Hoot, our Scheme-to-WebAssembly compiler, a little over a year ago in order to bring Goblins to the web. That bet is starting to pay off! In this post, we’ll talk in detail about how we made Cirkoban and how it showcases Spritely technology.

About Cirkoban

Cirkoban combines Sokoban block pushing with the Wireworld cellular automaton. It was made in 10 days for the Spring Lisp Game Jam 2024.

In Cirkoban, you play as an owl stuck in a secret, ethereal lab filled with strange circuitry. You must solve devious puzzles to ascend the stairs and reach the top floor. It has minimal controls (4 direction movement and an “undo” button) and contains 15 levels of increasing difficulty. Solving the puzzles requires precise movement, but you can travel back in time to undo mistakes. For an added challenge, there is a gem in each level that is harder to reach than the stairs to the next level.

Goals

While it is fun to make games, the real reason we made Cirkoban was to exercise Spritely technology and produce an appealing user-facing application under a tight deadline. We wanted to demonstrate the current state of Hoot, as well as the early progress made towards porting Goblins to the web. Games also have the tendency to stress language implementations and libraries since they require asynchronous event loops, perform a lot of math calculations and I/O, and need to refresh at 60Hz or so to appear smooth to the player.

I successfully used Hoot in the autumn edition of the Lisp Game Jam, when it was barely usable for such things. This time around, we were hoping that to spend very little time debugging Hoot issues and much more time making the game and testing out something new: Goblins!

Thanks to the work of Juli Sims, it is now possible to compile and run a subset of the Goblins library with Hoot. Specifically, we had a working port of the (goblins core) module that we wanted to incorporate into the architecture of the game. The port is not yet at the state where we can do networking, so instead we set out to demonstrate local actors and transactional rollback.

Design

If you’re a regular reader of our blog, then you know that Wireworld comes up often around here. It is our favorite cellular automaton because, with a few simple rules, you can build logic gates, diodes, and other components which can be composed into complicated circuitry. The simplicity of Wireworld has made it a compelling demo program for Hoot at various stages of its development.

Christine Lemmer-Webber thought it would be interesting to create a puzzle game where the levels contain broken circuits that need to be fixed by moving some puzzle pieces around. Sokoban was a natural starting point for such a game, and it also provided an opportunity to show off Goblins’ rollback feature. In Sokoban, if you push a block into a corner then you can’t push it anymore and thus may not be able to complete the puzzle. Games like Baba is You allow the player to undo previous moves when they get stuck. With the addition of a similar undo mechanic, we now had a way for a user to directly experience rollback as part of the gameplay.

Terminal Phase time-traveldemo

In the past, we have shown off rollback using the space shooter game Terminal Phase, a text-based game that runs in the player’s terminal. With Cirkoban, we now demonstrate that we have successfully ported this core feature to the web!

For the sake of fun gameplay, we decided to take some liberties with the Wireworld rules. For example, we wanted to condense common constructs which require many cells in Wireworld, such as electron-emitting generators and logic gates, into a single cell. This would help us keep the levels small and more readable, especially for the average player who isn’t already familiar with Wireworld, at the expense of more complicated code under the hood.

Development

To model the game state, we used Goblins actors. Every object in the game world is represented as an actor that responds to messages sent from other actors. Every time the player presses a key, the game either advances by a turn or rolls back to the state of the previous turn, in the case of the “undo” key. The game “client” then procsses the events of that turn, playing sound and visual effects as appropriate, and updates the visual representation of the level. The game state and rendering state are thus separated from one another.

Emacs with Cirkoban actor source code open

To edit the code, we used Emacs, naturally. I use paredit and rainbow-delimiters to efficiently edit Scheme, and the unsurpassed magit for working with our Git repository. A simple Makefile handles build automation and running a local web server for testing the game.

For graphics, we thought a low-resolution pixel art style with our own original art would work well. Using HTML5 canvas was an obvious choice to make rendering the game as simple as possible. WebGL/WebGPU are not currently viable options with Wasm GC, anyway. We chose to feature an owl as the player character because the Hoot mascot is an owl. Christine came up with the ethereal “technomancer” theme and created lots of amazing sprites in Libresprite.

Musical artist and Spritely community member EncryptedWhispers whipped up a background music track that fit our theme based on a very funny recording of Christine imitating a theremin. Juli and I made retro sound effects using the classic game jam tool sfxr.

Tiled map editor window with a Cirkoban level loaded

To design the levels, we used Tiled. We kept the map structure as simple as possible so that we could build levels quickly. There are only two layers in each map: A background tile layer for the static level tiles and an object layer for dynamic objects such as pushable blocks. Tiled’s native .tmx format uses XML, but we did not want to be in the business of fetching and parsing XML at runtime. Instead, we baked the levels into the Wasm binary by compiling Tiled map files to Scheme. The compiled levels use Scheme bytevector literals to densely pack only the essential map data needed at runtime. As levels were added, the change to the total binary size was nearly imperceptible. The compiled game code takes up much more space than all of the levels combined.

At the last minute I added some onscreen controls for touchscreens using Kenney assets so the game was playable on phones and tablets. The controls were shown or hidden based on the pointer CSS media query. When the pointer is fine (mouse), the onscreen controls are display: none; when coarse (touchscreen), they are shown. I used Inkscape to export just the directional pad and A button from Kenney’s combined SVG file, made some modifications to the resulting XML by hand to remove extraneous things, and then inlined the SVG into index.html. With the SVG inlined, I was able attach click handlers to individual components of the graphic, such as each button on the directional pad. I had no idea you could this before! After the deployment freeze of the jam rating period I made some additional improvements to how the game canvas is scaled on small phone screens.

I even had the time to sneak in some quality-of-life improvements. For instance, game progress is automatically saved to localStorage at the start of each level. We save the last completed level and the set of levels for which gems have been collected. This small feature became very important for testing as the level count grew, and players who played multiple sessions appreciated it.

Reflections

We’re a bit biased, but Hoot is actually pretty good now! We built Cirkoban using the latest unstable code in Hoot’s main branch, rather than the most recent 0.4.1 release. Even so, I only found one small problem during development and it was trivial to fix.

Now that Hoot is feeling quite stable, the lack of proper Scheme debug tools has become the most annoying developer experience issue. The current state of WebAssembly makes it hard for Hoot to do things like show a proper Scheme backtrace, but we need to see what we can do with the tools we have to improve the debugging story.

Relatedly, I also miss the REPL. Towards the end of the jam, compile times were about 20 seconds long. Every time I made a small tweak I had to recompile the entire program. Hoot is a whole-program, ahead-of-time, cross compiler that does a good job generating optimized binaries, but during development it would be nice to have a way to build incrementally like with native Guile. Generating Wasm from within Wasm is something we haven’t done yet, but Andy Wingo has thoughts about how to do it.

Finally, the Goblins port is going well! The (goblins core) module worked reliably and very little time was spent debugging issues. Juli has been making incredible progress porting Goblins to Hoot, and if the jam had been just a week or two later we might have even had vats, our asynchronous event loops, running on top of Hoot’s fibers asynchronous programming API. We could have also had true persistence that saved the game state exactly where you left off; players could take a break in the middle of a level and not have to start that level over when they returned. We are looking forward to having the full power of Goblins available in the browser in the coming months.

Reactions

Cirkoban received positive ratings and comments amongst the other jam participants, ranking second in the jam overall! We are simply thrilled with how the game turned out and the feedback we’ve received. We love shiny demos here, and the positive feedback is a good indicator that we should continue to make them!