Architecting a turn based game engine with Redux

Saman Bemel Benrud • Jan 15 2019

I spent the last week rewriting the game engine underlying wilderplace. The two driving forces behind the rewrite: a desire for a system that supports fast game mechanic prototyping, and a system that makes animation a fundamental concept. In this post I’ll talk through some of the patterns I’m using to achieve those goals. As I mentioned in my first post, the game is a React app and I’m using Redux to manage state.

Before I dive into code, here’s an update on the state of the game. I stripped all the old art. This is what it looks like now:

state of the game

It’s hard to tell what’s going isn’t it? The point of the game is to help monsters that are possessed by spirits. Here are some of the specific mechanics at play:

  • There’s a humor alignment system. The two alignments in the gif are red and green (will eventually be ‘stone’ and ‘plant’).
  • When the player shares a space with a possessed monster, a spirit is transferred from the monster to the player.
  • When the player is possessed by a spirit and moves to an empty space, the player enchants that space with the spirit’s humor.
  • When the player shares a space on the board with a monster, the player may enchant their staff with the monster’s humor.
  • If the player’s staff is enchanted, the player may “placate” a spirit by selecting an enchanted tile. The spirit, the staff, and the tile must all have the same humor. Placating a spirit creates a new obstacle on the board but cures the player.

I’m about halfway through with the core mechanics, and everything can still change from here.

Okay. Let’s dive into some of the code patterns.

Minimal state

The game state consists of a single board property. The board is a two-dimensional array and each element in the board is a game object (like a player or a monster). All state is managed in redux, no React components use state at all.

I need to know specific details about the game board in order to enable certain player actions e.g. can the player move up? Can the player placate a spirit? Has the player run out of hit points? Without a “player” property in state, how do I do that conveniently?

The answer is reselect, a selector library designed to work with Redux. Here’s an example selector that tells me if the current level is complete or not:

/*
 * Neither player nor monster have spirits
 * @param {Object} state
 * @returns {function(): boolean}
 */
export const isComplete = createSelector(
  [getPossessedCount, getPlayer],
  (count, player) => count === 0 && !player.spirits
);

Function composition is central to reselect and it’s a powerful concept. The first argument to createSelector is an array of other selectors, so, for example, I only need to write the logic for finding the player on the board once, and then I can use it in all my other selectors. Selector composition has made it easy to perform complex lookups on the game board while allowing me to keep state simple.

One consequence of storing everything in one state key is that whenever I update the board, I need to re-compute every selector. The game runs smoothly now, but I may need to switch to a more performance-sensitive data model down the line.

Updating state with animation support

How do updates to the game’s board property actually happen? Not with a game loop! Since my game is not realtime, I haven’t found the need for a traditional game loop.

The game uses the standard Redux action creator pattern with some tweaks to support animation and sequencing. Components call action creators, and then action creator functions add items to an action queue. Any action in the action queue can be wrapped in a delay function to enable animations and transitions, and action creators can even add delays that don’t trigger any action in order to control timing.

Here’s how my action queue works:

// Initialize event queue
const actionQueue = new promiseQueue();

/*
 * Function that maybe triggers a follow-up action.
 * @param {function} selector gets info from latest state.
 * @param {promise} promise that calls an action
 * @returns {Promise} resolves after action or no-op.
 */
const maybe = (selector, promise) =>
  selector(getState())) ? promise() : Promise.resolve();

/*
 * Move the player and queue additional actions
 * @param {string} dir 'left', 'right',' top', 'bottom'
 * @returns {?Promise}
 */
function playerMove(dir) {
  // If an action queue is in progress, return early.
  if (actionQueue.getLength()) return;

  actionQueue.add([
    // Update player position on board
    () => move(dir),
    // Call delay without an action to pause after state update
    () => delay(),
    // call delay with an action to pause before state update.
    () => maybe(isGameOver, delay(gameSetModeEnd))
    // ...imagine more conditional actions here
  ]);

  actionQueue.execute();
}

This pattern supports animation because the delay function sends actions to the store that update a pendingAction property. Components then subscribe to the pendingAction in order to run animations before changes have propagated to the state. I’ve always struggled to do animation in React and I find react-transition-group super annoying to use, so I’m happy that I have a pattern that side steps problems with React animation.

It has been working well for me so far. I like how legible the action sequencing is. It’s not very “functional” and breaks some Redux best practices. For example, the maybe function makes a call to getState(). I will try to refactor the code so each action returns the game’s state, eliminating the need for that getState().

I expect this part of my code to evolve as the game progresses.

Test driven development

At work I try my best to be a disciplined test writer, but in personal projects I usually haven’t bothered, until now!

Since most of the core game logic in the game is described with pure selector or updater functions, test-driven development has been easy, fun, and fast. For the past few days, when starting to implement a new mechanic, I’ll first write the test. You can see in the test below how the selector pattern benefits test writing.

Here’s an example test:

test("PLAYER_IMBUE_STAFF sets staff to humor of monster", () => {
  // Create a test board from a board schematic
  const testBoard = getBoardFromDiagram([[["M", "@"]]]);

  // Set up initial state
  const newState = reducer(initial.set("board", testBoard), {
    type: IMBUE_STAFF
  });

  // Use a selector to get the player from the board
  const newPlayer = getPlayer(newState);

  expect(newPlayer.get("staff")).toEqual(PLANT);
});

One pattern I’m relying on to both author boards and write tests shows up in the code snippet above. See getBoardFromDiagram. This function takes a board diagram and populates it with real game objects. Each level in my game is a JSON file that looks kinda like this:

(["◄", ".", ".", "d", "0", "0"],
[".", "S", "t", ".", "►", "0"],
["t", ".", ".", ".", "t", "0"],
["0", "P", "0", ".", "t", "."],
["0", ".", "t", "t", "0", "0"],
[".", ".", ".", ".", "t", "."],
["0", ".", "0", ".", "t", "0"])

Each character on the board corresponds to a possible game piece.

Problems and uncertainties

  • I miss having a type checking system. I use flow at work. I considered adding flow or TypeScript to this project but have been holding off simply because it would be a significant lift. I’ve been using Joi a little in tests, which is okay, but the fragility of JavaScript data structures is annoying. I feel like I’m working blind more than I’d like.

  • My action queue is rough around the edges. I expect to polish this code over time.

  • I have a lingering worry that it’s a mistake to not start with a game loop. Am I missing something here? My game is turn-based so it doesn’t seem to make sense, but all code samples out there seem to use a game loop.

  • Based on how I’ve architected animation, I’ll need to rely on implicit synchronization between CSS animations and the delay function in my queue. I love CSS and plan to rely on CSS for all graphics and animations, but hate how separate it is from Javascript. I want to be able to share variables between the two. Maybe PostCSS?


Thanks for reading, 👋