The main goals are to demonstrate:
- that nested state machines are a good way to describe video games structurally.
- that reducers are an acceptable way to implement nested extended state machines.
I say reducers are "acceptable" because I think Nested Modal Transducers would be better in several respects. The long-term plan is to write a version using them instead, to compare.
- It's not intended to be a very playable, very original game.
State machine diagram
Tour of the code
The code uses Immutable.js to implement immutable data, and Redux.js to implement reducers. Neither library is used so extensively that it couldn't be easily eliminated, with the relevant parts of the code written "from scratch".
The code starts with some utility functions. These should be fairly self-explanatory. Some of them are designed to work with objects and actions inside reducers.
There is then a sequence of sections, one for each of the following objects: Player, Missile, Boulder, Game. Each section 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 reducer function that takes an object and an action and returns a new object.
- (optionally) other helper functions that the reducer function uses.
One action that all of the reducers for all of these kinds of objects take,
STEP, which progresses the state of the entire game by one timeslice
(usually somewhere around 1/60th of a second).
When a state machine is implemented with a reducer, 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 tries to be very careful about not modifying game state data in
the reducer functions, to the extent that it does not even overwrite
local variables; instead it introduces a new local variables (called e.g.
game3, etc.) and places the new value in it. (i.e., in ES6
The section for InputContext objects may seem somewhat roundabout. Strictly speaking, the InputContext is not necessary, but it intends to illustrate a couple of things.
In a web browser, the DOM translates key presses into "key down" and "key up" events. But in an arcade cabinet, buttons are simply switches that are either make or break when the circuit reads them. The virtual hardware tries to simulate that.
But it's still useful for the game code to see "button pressed" as events. So it's the job of InputContext to synthesize those button level changes back into events for the game reducers.
The virtual hardware tries to simulate 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 driver sets up the virtual hardware, hooking up the events to a Redux store; subscribes to the store to update the video display.
- 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.