Developing a sokoban clone with React and Redux

Please click on the map to play. Use w-a-s-d keys to move. Push green boxes on the yellow tiles. (play here)

You can get the full source here : https://github.com/yortuc/ReduxSokoban

Sokoban is an ancient video game if you haven’t heard it yet.
(https://en.wikipedia.org/wiki/Sokoban)

Why?

This project is consist of my experiments on the technologies i’ve been playing around for a while. And i thought, it might be a useful example to see the big picture of how an ideal application architecture would be.

This project covers,

  1. UI should be rendered (on DOM or wherever) as a pure reflection of application state. Application state should be the only source of truth.

  2. A game can be programmed in a declarative fashion leveraging powerful React component model. This approach would be more sufficient for games which have a wide range of intractable game objects.

  3. When the whole application state is a single tree-structured object, it can be serialised, saved and restored easily. So, we’ll have an undo-redo mechanism and ability to save and continue the game later from the exact point where we left.

Image shows the project architecture.

react redux app structure

1. UI as a reflection of application state

Our tiny application here, represents its humble UI, the game, as a drawing on a Canvas element. We don’t need DOM nodes like most of the other web applications. So, main rendering context is the Canvas element. This is one of the biggest advantages of the React. Separation of rendering logic and rendering context. You can reflect the application state on browser as DOM nodes, on canvas element as drawings, on platforms such as iOS or Android as native components and so on.

In the latest release of React (v0.14, https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html), DOM context is separated from core React library. This encourages development of universal (not DOM dependent) component development and makes it easier to share functionality between React-DOM and React-Native platforms.

We are going to render our components mostly on the Canvas element, not on the browser DOM. And also, if rendering mechanism is a pure function which takes the application state and then outputs the UI, switching from one rendering context to another will not break any other parts of the application. For instance, rendering game objects on the DOM instead of canvas as html elements, is fairly simple and won’t require any change on any other part other than the red hexagon on the illustration above.

2. Declarative scene creation

At the beginning, i was planning to create a mario-like 2d side scroller. But the timing issues made the case over-complicated for me (challenge noted btw). So, i decided to implement a less dynamic game which renders only after a user input. React driven declarative component model served well in this case, imho.

Whole game is composed with React components representing game objects. Since Sokoban has quite simple hierarchy, a more complex game which has layers, levels, different kinds of game objects can benefit more from this approach.

Component structure goes this way:

<GameContainer store={reduxSokobanStore}>
  <SokobanGame>
     <Game>
         {[...gameObjects]}
     </Game>
  </SokobanGame>
</GameContainer>

Starting with “Game” component, the inner part of the hierarchy is project agnostic humble game engine layer. These components are re-usable and can be used in another game development. In this project, we have only one rendering context, 2d drawing context of the Canvas element. For a more complex game, leveraging the React component model, local contexts can be created to render on achieving double buffering for a better performance.

SokobanGame is the problem-specific, ad-hoc component composed of standard game-engine components. Also has the logic to parse application state and create game objects needed.

Outermost component, GameContainer, is controlled by Redux to inject application state into inner elements as props. With this approach, only GameContainer component is aware of Redux. The rest doesn’t care where the props are coming from.

I. Create rendering context

“Game” component will create an html Canvas element and then create the main rendering context on it.

render() { 
    this.state.ctx && this.state.ctx.clearRect(0, 0, 640, 480);
        
    return (
       <canvas id="game" width="900" height="600">
          { this.state.ctx ? this.mapChildren() : []}
       </canvas>
    )
} 

Since we don’t have the canvas element at the beginning (in the first render method call), we should create the rendering context after the canvas element has been rendered.

componentDidMount() {
    var c = document.getElementById("game");
    var ctx = c.getContext("2d");

    this.setState({ ctx: ctx });
}

II. Inject main rendering context into child components

We created the rendering context which all the child components will render themselves on. But, child components are not aware of the context, yet.

To pass rendering context to the child components, React’s context structure would be the best practice here. Since we don’t have a deep cascading game component structure here i don’t want to make things more complicated. And i’ll go with a simple manual injection.

In the render method of the “Game” component, inject the rendering context to its children.

mapChildren () {
    return React.Children.map(this.props.children, function (child) {
        return React.addons.cloneWithProps(child, {
            ctx: this.state.ctx 
        });
    }.bind(this));
}

And the render method of Game component

render() { 
    this.state.ctx && this.state.ctx.clearRect(0, 0, 640, 480);
        
    return (
        <canvas id="game" width="900" height="600">
            { this.state.ctx ? this.mapChildren() : []}
        </canvas>
    )
}

III. Render game objects

Now, child game objects can render themselves on the main rendering context. Take rectangle component for instance. It has properties such as position, size and color. And it renders itself depending on these properties.

render() { 
    var ctx = this.props.ctx;
    ctx.fillStyle= this.props.color;
    ctx.fillRect(this.props.x,this.props.y, this.props.width, this.props.height);
    return null;
}

And yes, we return null, we have nothing to do with DOM.

IV. Declare game scene

We have game-agnostic tools to render game components. Next step is to implement specific Sokoban game. We need a data structure to describe application state at ant given time t. Application state will store Sokobal level map, positions of game objects such as walls, boxes and position of player.

The simplest method i can think of to represent a level map like this is to use string literals. I’ll define the map (app state) in this way:

const initialState = {
	level: `1,1,1,1,1,1,1,1,1
	        1,0,0,0,0,0,0,0,1
	        1,0,0,0,3,0,0,0,1
	        1,0,0,0,1,0,0,0,1
	        1,0,0,1,1,1,2,0,1
	        1,0,2,0,1,0,0,0,1
	        1,0,0,0,3,0,0,0,1
	        1,0,0,0,0,0,0,0,1
	        1,1,1,1,1,1,1,1,1`,
	playerX: 1,
	playerY: 6
};

As you guessed, numbers represent tile type. 0: walkable area, 1: wall, 2: box, 3: box placement place.

We’ll parse this string literal and create game components depending on the type and its position in the string which maps to x and y coordinates on the screen.

createMap(txtLevel) {
   var level = txtLevel.replace(/\,/gi, '').split('\n').map(s=> {
      return s.replace(/\s/gi, '')
   });

   var tiles = [];
   var bgcolor = ["#fff", "#e5e5e5"];
   var colors = [null, "black", "green", "yellow", "blue"];
   var ind = 0;
   var color, row;

   for(var i=0; i<level.length; i++){
      row = level[i];

      for(var j=0; j<row.length; j++){
         color = row[j] === "0" ? bgcolor[(i+j)%2] : colors[row[j]];

         tiles.push(
            <Rect key={++ind} x={j*50} y={i*50} color={color} />
         );
      } 
   }

   return tiles;
}

Ok, this is not the cutest parser, but works for now. We parsed the level map and created tile components accordingly.

And finally we can render our map on the canvas, with React.

3. Redux as a state manager

Next step is to react to user input. When user interacts with application, this “changes” the application state. Using Redux, a new “action” is emitted on every user interaction. And then Redux runs the reducer function which returns a new application state depending on the previous state and received action.

(previousState, action) => newState

Reducer should be a pure function with no side-effects. This means, for the same inputs, always the same application state is created. This determinism makes it easier to debug your application, and trace the bug to the source.

In the application we have 2 kinds of user interaction. Moving player and traversing in the application state history via undo and redo buttons. Redux has actions to cary messages between state container and the entity which emits the action. Actions are plain javascript objects which have to have “type” property.

For instance, keyboard event handlers emit action named “PLAYER___MOVE_REQUEST”. This action also has the information required to move the player such as delta directions on x and y axises. The action has “request” in the name because every user movement is not valid. Player cant go through walls and any request that tries to go through walls aren’t going to change application state.

Our simple player move-request action:

// player action
{ 
   type: 'PLAYER_MOVE_REQUEST', 
   dm: {   
      dx: 1,  // delta movement on x-axis
      dy: 0   // delta movement on y-axis
   } 
}

// dispatched in the keyboard event handler
// a move request for one tile left in x-direction
store.dispatch({ type: 'PLAYER_MOVE_REQUEST', dm: {dx: -1} });

After the move-request action is emitted, main reducer function gets called by store.

// main reducer function
function reactSokoban (state, action) {

	switch(action.type) {
	case 'UNDO': 
		return appStates.length>1 ? appStates[--stateIndex] :
                                              appStates[stateIndex];
		break;

	case 'PLAYER_MOVE_REQUEST':
		var nextState = tryToMove(state, action.dm);

		if(nextState !== state){
			pushState(nextState);
		}

		return nextState;
		break;

	default: 
		return state;
	}
};

We check the incoming action’s type manually and act accordingly. If action type is PLAYER___MOVE_REQUEST, a new application state is created by a sub-reducer function, tryToMove(state, movement).

A check has to be made in the tryToMove function if the requested movement is valid depending on the current game scene and movement.

Since the level map is stored in a string template, we’ll create an array representation out of it to make computations easier.

The vital point here is not to mutate state. All of the operations made here are immutable. Some immutable operations might be cumbersome to do manually. Using an immutable library such as Immutable.js (again from Facebook) and Freezer.js (a lightweight alternative) would be helpful here. And again, for the sake of simplicity i’ll do the immutable operations manually.

function tryToMove(state, dm){
   /*
      return new state if user action is valid
   */
}

With previous state and action to be taken, we create the next application state. Or, maybe the move-request is invalid, user is tying to go through a wall so to speak, then reducer function returns the previous state.

4. State history and undo mechanism

After user requested a new player movement, if the movement is valid a different state object is created. Since these are pure objects, we can check if the new state is different then previous one just with default equal operator.

In the main reducer function, for the move-request action,

case 'PLAYER_MOVE_REQUEST':
   var nextState = tryToMove(state, action.dm);

   if(nextState !== state){
      pushState(nextState);
   }
	
   return nextState;
   break;

Now we can store this states in an array and traverse with an history index. pushState function does that.

function pushState(state){
   appStates = appStates.splice(0, stateIndex+1);
   appStates.push(state);
   stateIndex++;
}

Since we can render any application state without performance issues, going back and forth in state history is quite simple, almost for free.

Undo mechanism is inside the main reducer as a single line of code.

switch(action.type) {
   case 'UNDO': 
      return stateIndex>0 ? appStates[--stateIndex] : appStates[stateIndex];
   break;

Another approach to have undo-redo mechanism is to store not the states but the actions. Since actions are far more smaller objects than whole state, this would be more memory efficient. An action can be undone with applying movement in the opposite direction. And all user actions can be stored in an array, saved and replayed later.

Result

Increasing complexity of web applications is the main problem of the application development today. With React, finally, we can program the user interface as a pure function. Only then handling the application state with immutable data structures becomes a valid option. And once we have immutable application state, taming the complex state beast becomes way simpler.

And “simpler” means less errors, more comfortable debugging, more amusing development and happier developers. Happy coding!

Comments