Developing a roguelike game functional-style with JS and React+Redux

Published on Author mzabskyLeave a comment

Roguelike games are traditionally an excellent playground for programmers since they are very systems-oriented and place minimal focus on visual presentation. This focus on gameplay systems and data structures also presents a unique opportunity to utilize functional programming, which is not traditionally very popular among game developers. This post details my initial impressions after experimenting with roguelike game development using the React+Redux JavaScript stack.

Structure of a React+Redux application

React is a UI library which focuses on declarative UIs, taking a lot of ideas from functional programming. Redux is a state container with a clever React binding.

As is usual with GUI-based applications, a React+Redux application revolves around a main loop. This time, the loop looks approximately like this:react_redux_diagram

Let’s walk through the individual steps in this loop.

State

The state is a tree of plain objects which contains the entire state of the application. For a roguelike game, it might look like this:

This state has to maintain one crucial property: it is immutable – once the state instance is created it (or any part of it) must not be altered in any way. This is an invariant that all other code in the application relies upon, and it is also the source of its functional nature.

Whenever the code needs to make a change to the state, a new instance of the state has to be created instead. The easiest way to achieve this is generally by merging a tree of changes into the old state tree:

Lodash offers a suitable merge function for this purpose.

This merging workflow necessitates that all non-leaf nodes in the state tree are plain objects, even something like an array makes manipulating the state much more difficult.

Copying the whole tree over sounds expensive at first, but the immutable nature of the tree mitigates new allocations greatly. Consider a small tree with 7 nodes (on the left) to which we add a new node (8):

immutable_tree

The new tree (on the right) can reference the old tree’s subtrees 2 and 6, since those (or any of their descendants) were not changed in any way. Nodes 1, 3 and 7 have to be copied over. Generally, only the nodes being directly modified (in this case 7, since a child is being added to it and 8 as a new node) and all of its ancestors (1 and 3) have to be allocated/copied. In practice, each operation only involves copying over several nodes and all others are preserved from the old tree.

UI

UI is where React shines. In React, UI is described as a function which effectively transforms the state into HTML code. Instead of saying “update the player’s HP field to contain 2”, you simply say “this field displays state.player.hp”. React then handles updating the field as necessary whenever the state changes:

The component’s code is specified using a custom “sublanguage” called JSX, which allows you to blend HTML and JavaScript in an unusually elegant fashion.

Components can then also reference other components:

You can of course also make reusable components with parameters that modify the component’s appearance and behavior.

The exact appearance of the application can be completed with the use of CSS or one of the style preprocessors (I personally prefer the terse syntax of Stylus).

Actions

The UI can invoke back-end code by calling a so-called action creators. Action creator is a function which produces a plain object called an “action message” (or just “action”):

A component can then make use of the action:

Reducer

A reducer is the final piece of the puzzle. It is a function which takes an “old” state, an action message and returns an updated state object. These are the only code which is actually allowed to produce a new state (effectively making changes to the state).

The reducer generally looks like this:

Each application only has a single “root reducer”, but this root reducer generally only calls sub-reducers dedicated to individual concern areas.

Advantages

Easy debugging

Most of the “business” logic is located in the reducer functions, which are by their nature quite straightforward. An instance of state and a small action message objects come in and another state instance comes out. JavaScript makes it very easy to dump all of those into the console log, so most bugs are very easy to reproduce.

Easy unit testing

Since almost all of the code base is composed out of pure functions, unit testing is also made very easy. You just set up the initial state and the action message, call the reducer and then validate the produced state (example using the Ava testing library):

Easy and powerful UI

The web front-end stack of technologies (HTML+JS+CSS) is miles ahead of any other UI solution out there. Where roguelikes usually rely on simple unformatted text and very basic UI in general, a web-based roguelike gets access to extremely rich UI features. Things like mouseover tooltips, formatted text with inline icons and symbols, dialogs and windows, forms, transitions and other features come very cheaply.

Support for both desktop and web

A React-based application can be either exposed on web (where the build process produces a single HTML file, a single JS file and a single CSS file you can just place on any web hosting) or built into a fully-featured multi-platform desktop application with Electron (which produces a single executable file containing both a Chromium browser and the web application itself).

…and disadvantages

Stringly typing

Even the trivial example mentioned in this article already contains traces of stringly typing (specifically the action type and the monster type) and it gets much much worse.

The root cause of the need for stringly typing is the inability to have typed objects and to even reference other objects directly. In an object-oriented environment, an instance of a monster “zombie” would either be an instance of a class “Zombie” derived from some class “Monster” or at least have reference to an object containing a common definition of all zombies. Since our state has to be a simple tree of plain objects, the only way the individual zombie instances have to reference the common zombie definition is a string key.

You could define constants for each of these common string keys, but that gets rather cumbersome (especially if you have to reference them across multiple source files) and it doesn’t even help that much in the dynamic world of JavaScript.

Tangled state

While the example state in this article is fairly simple, once you start dealing with more complicated concepts like inventory, monster and item abilities, behavior trees etc., it gets very complicated very quickly. In my game, it is not uncommon for the tree to be 7 levels deep.

This combined with the fact that the whole tree is dynamically typed makes for some moments of frustration.

It would probably be quite practical to keep at least some external documentation of the state tree, especially if more than one person was to work with the code base.

Performance

The nature of state tree as an immutable data structure keeps the merging reasonable in most cases (see above), but you still have to be careful with the design of the state tree.

It is especially advisable to make sure each node in the tree has a reasonable number of children, since every time a change is made to that node or any of its descendants, the whole node has to be copied over.

I ran into this precise issue with my message log – almost any action ended up adding a message to it and the message log node merges would end up dramatically slowing down over time.

Another issue is that it is difficult using any clever data structures with the state tree since every non-leaf node is a hash map and that’s all you get to work with.

Non-merge state tree operations

In 95% cases, you can create the derived state tree using a merge operation as demonstrated above, however in some cases that can’t be done. This generally comes up when you need to remove nodes from the tree. In this case, you will need to fiddle with the tree nodes manually.

For example, removing the monster “1” from the state tree would look like this:

This gets progressively worse as the tree grows more complex.

The amount of non-merge tree operations can fortunately be in most cases mitigated with a clever design of the state tree: for example by marking monsters dead instead of straight removing them from the tree.

Conclusion

I am still figuring out a lot of different things about this approach, but it seems enjoyable and viable enough to continue working on it. If there is enough interest, I will write more about it, perhaps focusing more on actual code instead of abstract concepts and tiny examples.

Leave a Reply

Your email address will not be published. Required fields are marked *