Cosmos Boulders
Version 0.5 | Play it online @ catseye.tc | See also: Nested Modal Transducers ∘ Jaft ∘ Bubble Escape

An arcade-style HTML5 minigame built with immutable data and Nested Modal Transducers in ES6 JavaScript, with the core game logic written in a tiny subset of JavaScript called Jaft for high assurance that it is purely functional (modulo a handful of effects that we deem desirable, namely: randomness and throwing errors).
Goals
The main goals are to demonstrate:
- that a simple but full-featured arcade game (including, e.g., attract mode) can be written in a few hundred lines of JavaScript code.
- that the game logic for such a game can be written in a purely functional style (no mutable data, no side-effects).
- that nested extended state machines are a good way to describe video games structurally.
- that Nested Modal Transducers are a good way to implement nested extended state machines in a functional setting.
Previous versions of this implementation used reducers (a la Redux) to implement the state machines, which was an acceptable way to implement them; but the plan has always been to implement them using Nested Modal Transducers, which we claim is a better way, and which has now been realized.
Non-goals
- It's not intended to be a very playable, very original game.
State machine diagram
This diagram was generated by State Machine Cat from the description in cosmos-boulders.smcat. While it does not capture every detail, it does show the overall structure.

Tour of the code
The code uses Immutable.js to implement immutable data, and Redux.js to handle top-level events with a top-level reducer. Neither library is used so extensively that it couldn't be easily eliminated, with the relevant parts of the code written "from scratch", and there are vague plans to do so someday.
The code was originally written as one big JavaScript file, but has since been split up into individual files formatted as ES6 modules. The functions in the core game logic modules have been rewritten in Jaft, using the concoctor interface to compile them into JavaScript functions on the fly while restricting the functions that they can call to those that we know are well-behaved, giving us a high assurance that they are themselves well-behaved. (Jaft is virtually a subset of JavaScript, so the "impedance mismatch" with the surrounding code is minimized.)
Some utility functions are exposed in utils.js. These should be fairly
self-explanatory. Some of them are designed to work with objects and actions
inside transducers.
The logic of the objects in the game is captured in the four modules
player.js, missile.js, boulder.js, and game.js.
Each of these modules contains
- (optionally) a "reset" function that takes an object and returns a version of that object which is reset to its starting state
- a "make" function that returns a new object of that type. It may take some arguments, which influence the created object. This function usually builds on the reset function.
- a transducer function that takes an object and an action and returns a new object and a list of effects.
- (optionally) other helper functions that the transducer function uses.
(One thing that might be confusing about these objects is that sometimes they are plain JavaScript objects and sometimes they are Immutable.js Map objects. In either case, they are not mutated. A future version of the code may unify these usages.)
One action that all of the transducers for all of these kinds of objects take,
is STEP, which progresses the state of the object (and in aggregate, the
entire game) by one timeslice (usually somewhere around 1/60th of a second).
When a state machine is implemented with a transducer, the "state variable"
which can take on a finite set of values, is not called state as might
be expected; instead it is called the mode of the object. This is to
avoid confusion between it and the state of the entire object.
The code is careful about not modifying game state data in the transducer functions. These functions are written in Jaft, and in normal use, Jaft does not permit mutating data like this.
The input-context.js module may seem somewhat roundabout. Strictly
speaking, the InputContext is not necessary, but it is meant to illustrate
the differences between how an arcade machine and a web browser interface
with the world around them.
Namely, in a web browser, the DOM translates key presses into "key down" and "key up" events, while in an arcade cabinet, buttons are simply switches that are either make or break when the circuit reads them. The virtual hardware presents an interface that models those switches. But! It's still useful for the game code to see "button pressed" as an event that it reacts to. So it's the job of InputContext to synthesize those button level changes back into events for the game reducers.
The display.js module contains the logic for how each game object is
displayed on the video game hardware, separating content from presentation.
The hardware.js module simulates the basic circuitry of a video game
arcade cabinet, including the screen (as a canvas) and the controls (as switches
whose make or break state is reflected in a boolean value). It generates
events when a new video frame is ready to be displayed, and when a coin is
inserted.
Finally, the main.js module sets up the virtual hardware, hooking up the events
to a Redux store, and subscribing to the store to update the video display.
Further reading
- A couple of years ago (2017) I was motivated to finally write down my formulation of A Basic Theory of Video Games, which I had been thinking about on-and-off for many years beforehand.
- Earlier this year (2019) I tried to refine the ideas there into a more formal model that I called Nested Modal Transducers.
- Many years previous to these (2008), James Hague wrote a series of articles about Purely Functional Retrogames, which touches on a few of the same issues.
- John Earnest's Deep: A Postmortem (2011), also touches on a few of the same issues.
Commit History
@master
git clone https://git.catseye.tc/Cosmos-Boulders/
- Give better names to some functions Chris Pressey 2 months ago
- Remove GONE boulders from boulders list to allow detect pass level Chris Pressey 2 months ago
- The functionality of collision.js has been replicated elsewhere. Chris Pressey 2 months ago
- Handle collisions between missiles and boulders. Chris Pressey 2 months ago
- Steps toward missile-boulder collisions. Chris Pressey 2 months ago
- Bring closer to functional style. Chris Pressey 2 months ago
- The boulder participating in a collision also explodes now. Chris Pressey 2 months ago
- Concoct, don't bless. Add filter() as a function on lists. Chris Pressey 2 months ago
- Use the returned effects to initiate EXPLODING state in player. Chris Pressey 2 months ago
- Reduce boulder list to retrieve set of effects from it. Chris Pressey 2 months ago
- Boulder transducer can produce a COLLIDED_WITH_PLAYER effect. Chris Pressey 3 months ago
- Ensure player and missiles are available on boulder's STEP action. Chris Pressey 3 months ago
- Refactor deeper Chris Pressey 3 months ago
- Refactor updateBoulder towards returning some effects. Chris Pressey 3 months ago
- They're all transducers now. Now some fun redesign work begins. Chris Pressey 3 months ago
- updatePlayer() is now a transducer. Chris Pressey 3 months ago
- Add some notes on how Nested Modal Transducers are used here. Chris Pressey 3 months ago
- The set of outputs (effects) is implemented with a list. Chris Pressey 3 months ago
- Jaft supports null directly, no need for newNull(). Chris Pressey 3 months ago
- updateBoulder() is a transducer. Chris Pressey 3 months ago
- Remove set(), rename setr() to set(). Chris Pressey 3 months ago
- Convert all set() calls to setr(). Chris Pressey 3 months ago
- Convert two reducers to transducers. Chris Pressey 3 months ago
- Mark the functions we need to convert. There actually aren't many. Chris Pressey 3 months ago
- Add summary of Nested Modal Transducers, and update README. Chris Pressey 3 months ago
- Bump version shown in README. Chris Pressey 4 months ago
- Upgrade Jaft to latest (probably final) version 0.2. Chris Pressey 4 months ago
- Use tuples to return and handle multiple values in reduce. Chris Pressey 4 months ago
- Upgrade Jaft to version 0.2. Chris Pressey 4 months ago
- Move concoctor and Jaft into contrib directory. Chris Pressey 4 months ago