Puzzling with React and Redux

I needed a refresher on React, Redux and JavaScript functional programming and to do something hands-on I decided to make a collection of mini-games using those techniques. For gamification purposes I also created a generic cloud-hosted high score service that the games integrate with.

All code for these exercises are available from GitHub and this blog post describes the outline and ideas for the games and the high score service.

Intro

For front-end development, I have focused on Angular the last couple of years. I love Angular but I miss working with React and applying the functional paradigm (compared to using imperative + OOP that is more suited for Angular). To get back into the React mindset I dived into React+Redux and created some simple mini-games:

  • Image Puzzle
    A game where you should swap position of scrambled tiles until they form a complete image.
  • Sliding Image Puzzle
    A classic sliding puzzle where the tiles are parts of an image. You should align the tiles to form a complete image with the empty tile in the bottom right corner.
  • Snake Game
    A classic snake game implementation using a grid of div-elements for drawing. You should capture dots/food with the snake’s head while the snake grows and avoid colliding with the tail or with the game’s border.

About React

React is a minimal framework for building interactive user interfaces for web browsers. The UI is composed with a hierarchy of components where JavaScript defines what html to generate. React works with a virtual DOM which makes updates super-fast.

This blog post is not a tutorial on React, so if needed, head over to https://reactjs.org/ to learn more.

About Redux

Redux is a library that provides a state container for JavaScript applications. The state is kept in a store and updates for the store are dispatched with actions from the application. The actions are typically initiated by a user interaction. The store updates are handled by reducer functions that calculates a new state based on the old state and the action, i.e. (state, action) => newState. You can connect a React component to the store so that it get updates (input props) when certain parts of the state changes.

Redux flow

It is feasible to implement your own store and dispatching mechanism but the redux and react-redux library provides an easy way to get started with automatic bindings for React components. Using Redux implies that you have a centralized state for the application which makes the application behavior more predictable, more testable, easier to debug and makes undo/redo functionality a breeze to implement.

To get started with React + Redux you can use the create-react-app tool and then add the redux libraries:

npx create-react-app my-app
cd my-app
yarn add redux react-redux

In my applications I also include the redux-thunk library that allows for async behaviour in reducers (for operations against the highscore api e.g.).

An alternative is to include Redux Toolkit with create-react app:

npx create-react-app my-app --template redux

This will scaffold an application with best practices for React + Redux. Yet another alternative is to create the application with TypeScript instead of JavaScript.

npx create-react-app my-app --template typescript

and with TypeScript and Redux Toolkit:

npx create-react-app my-app --template redux-typescript

Image puzzle implementation

The image puzzle game is based on a random image that is split into 5×5, 6×6 or 7×7 shuffled tiles. You swap the positions of two tiles by first selecting one tile and then selecting a different tile. The goal is to align the tiles to form a complete image with as few swapping turns as possible.

Image puzzle game in action

For hints during puzzling, the tiles are marked with an inner border when they are placed at the correct position.

The application is based on a grid of div-elements where each div shows the same image (with the background-image style attribute) but use different background-position settings that offset what part of the image is showed in the tile/div.

The reducer for the game handles three actions: INIT_GAME, SHUFFLE_TILES and SELECT_TILE.

const initialState = {
turnNo: 1,
numClicksWithinTurn: 0,
selectedId: undefined,
gameComplete: false,
imageNumber: 1,
tiles: [],
size: undefined, // number of rows/columns in the puzzle matrix
gameId: undefined,
gameName: undefined
};
// The reducer for the game
// State is an object with game status and an array of tiles
// The array represents a size*size matrix with a unique
// numerical value 0...size*size-1 per tile
// A tile is an object with these properties:
// {
// id: number, // the number/value for the tile
// top: number, // pixel offset for the image that is projected on the tile
// left: number // pixel offset for the image that is projected on the tile
// }
//
function tileGame(state = initialState, action) {
switch (action.type) {
case INIT_GAME: {
const size = gameConfigs[action.gameId].size
return Object.assign({}, initialState,
{
gameId: action.gameId,
size,
gameName: gameConfigs[action.gameId].name,
imageNumber: action.imageNumber,
tiles: generateTileSet(size)
});
}
case SELECT_TILE: {
if (state.gameComplete) {
return state;
}
if (action.id < 0 || action.id > (state.size * state.size - 1)) {
return state;
}
const numClicks = state.numClicksWithinTurn + 1;
if (numClicks === 1) {
const newTiles = state.tiles.map(t => t);
return Object.assign({}, state, {
selectedId: action.id,
numClicksWithinTurn: numClicks,
gameComplete: false,
tiles: newTiles
});
}
const newTiles = state.tiles.map(t => t);
if (action.id === state.selectedId) {
return Object.assign({}, state, {
selectedId: undefined,
numClicksWithinTurn: 0,
tiles: newTiles
});
}
const setWithSwappedTiles = swapTilesInSet(newTiles, state.selectedId, action.id);
const gameComplete = allTilesAreAligned(setWithSwappedTiles);
return Object.assign({}, state, {
selectedId: undefined,
numClicksWithinTurn: 0,
gameComplete,
turnNo: state.turnNo + 1,
tiles: setWithSwappedTiles
});
}
case SHUFFLE_TILES: {
const newTiles = shuffleTileSet(state.tiles);
return Object.assign({}, state, { tiles: newTiles });
}
default:
return state;
}
}
export default tileGame;
view raw tile-game-reducer.js hosted with ❤ by GitHub

The puzzle is rendered with a functional React component that subscribes to changes from the Redux store and dispatches a SELECT_TILE action when a tile is clicked:

import React from 'react';
import { connect } from 'react-redux'
import TileView from './TileView'
import { selectTile } from '../reducers/actions';
import PropTypes from 'prop-types';
const Puzzle = (props) => {
const width = Math.min(window.innerWidth, window.innerHeight);
const tileWidth = width / props.size;
const tileWrapperStyle = {
width: `${props.size * tileWidth}px`
}
const tileContainerStyle = {
gridTemplateColumns: `repeat(${props.size},${tileWidth}px)`
}
return (
<div>
<div className='tile-wrapper' style={tileWrapperStyle}>
<div className='tile-container' style={tileContainerStyle}>
{
props.tiles.map((t, idx) =>
<TileView key={idx}
id={t.id}
correctPos={t.id === idx}
imageNumber={props.imageNumber}
onClick={props.onTileClicked}
tileWidth={tileWidth}
size={props.size}
selected={props.selectedId === t.id}
width={width}
/>)
}
</div>
</div>
</div>
);
}
Puzzle.propTypes = {
onTileClicked: PropTypes.func,
size: PropTypes.number,
tiles: PropTypes.array,
imageNumber: PropTypes.number,
selectedId: PropTypes.number
};
const mapStateToProps = state => {
return {
size: state.size,
tiles: state.tiles,
imageNumber: state.imageNumber,
selectedId: state.selectedId
}
}
const mapDispatchToProps = dispatch => {
return {
onTileClicked: id => {
dispatch(selectTile(id));
}
}
}
const PuzzleView = connect(
mapStateToProps,
mapDispatchToProps
)(Puzzle)
export default PuzzleView;
view raw PuzzleView.js hosted with ❤ by GitHub

You can find the complete code for this game here: https://github.com/LarsBergqvist/image-puzzle and there is a deployed demo of the game here: https://larsbergqvist.github.io/image-puzzle/

Sliding puzzle implementation

The next game I implemented was a version of the classic sliding puzzle. I based it on the previous image puzzle but this time you should slide the tiles into place. There is always one empty tile regardless of the puzzle size and you click on a non-empty tile to swap place with the adjacent empty tile. The goal is to align all tiles with as few moves as possible:

Sliding puzzle game in action

To be sure that the generated scrambled tile set gives a solvable solution I used this excellent description on how to make a solvable checker.

You can run the game with 3×3, 4×4 or 5×5 tiles. Regardless of the size of the grid, the hard part of the game is to get the bottom right 3×3 tiles into place. There are simple strategies for this. See if you can figure it out!

As with the previous image puzzle, the puzzle view is made up of a grid of div-elements with background images and offsets. I simplified the state of the tiles so that a tile is only represented by a number 1…N. The image offset is instead calculated in each TileView-component.

The reducer for the game handles the INIT_GAME and MOVE_TILE actions.

const initialState = {
moves: 0,
gameComplete: false,
imageNumber: 1,
tiles: [],
size: undefined,
gameId: undefined,
gameName: undefined
};
// The reducer for the game
// The state is an object with game state and an array of tiles
// A tile is a number 1-N and the blank tile is represented by 0
function tileGame(state = initialState, action) {
switch (action.type) {
case INIT_GAME: {
return Object.assign({}, initialState, {
gameId: action.gameId,
size: gameConfigs[action.gameId].size,
gameName: gameConfigs[action.gameId].name,
imageNumber: action.imageNumber,
tiles: generateTileSet(gameConfigs[action.gameId].size, action.doShuffling)
});
}
case MOVE_TILE: {
if (action.id === 0) {
// selected blank tile
return state;
}
if (state.gameComplete) {
return state;
}
if (action.id < 0 || action.id > (state.size * state.size - 1)) {
return state;
}
if (!hasEmptyTileOnSides(state.size, action.id, state.tiles)) {
return state;
}
//
// Move the tile
//
const newTiles = state.tiles.map(t => t);
const setWithSwappedTiles = swapTilesInSet(newTiles, 0, action.id);
//
// Check result
//
const gameComplete = allTilesAreAligned(setWithSwappedTiles);
return Object.assign({}, state, {
gameComplete,
moves: state.moves + 1,
tiles: setWithSwappedTiles
});
}
default:
return state;
}
}
export default tileGame;

You can find the code for this game here: https://github.com/LarsBergqvist/sliding-image-puzzle and there is a deployed demo of the game here: https://larsbergqvist.github.io/sliding-image-puzzle/

Snake game implementation

The final game in this exercise was to implement a version of the classic snake game using React + Redux. The GameView is based on a square-grid of div-elements that shows the border, the snake and the ‘food’. The input to this grid is a GridViewModel from the store that is calculated by a reducer. The GridViewModel is an array with numbers where the numbers represent the border, the food or parts of the snake. The game reducer handles three actions:

  • INIT_GAME. Creates the initial state for the game. The state contains game status, a calculated GridViewModel, the snake (an array of coordinates) and the current food position.
  • CHANGE_DIRECTION. Dispatched when a user changes direction of the snake’s head. The directions are put in a fixed size queue so that the new direction can be applied when the next MOVE_SNAKE action is applied.
  • MOVE_SNAKE. This action is dispatched at an interval by a thunk, runGameLoop, as long as the game is running. The action handler picks up any new direction from the queue or keeps moving in the old direction. The snake ‘moves’ by using unshift(nextPos) on the snake array. A check is made to see if the next position of the snake’s head collides with the border or the food item. Finally a new GridViewModel is calculated from the snake, border and food item.
Snake game in action

The user can change direction by using the arrow keys on a keyboard or swiping up, down, left, right on the screen. For the swipe implementation I use the react-swipable package that provides a useSwipable hook.

This is likely not the best way to create a snake game, but it was fun implementing it.

You can find the complete code for this game here: https://github.com/LarsBergqvist/snake-game and a demo of the game is available here: https://larsbergqvist.github.io/snake-game/

Creating a high score service

To get my kids interested in evaluating these games I realized that a leader board was needed for gamification. I designed a generic high score service based on an ASP.NET 5 web api where you can create new high score lists, add results and fetch all lists or a list for a particular game. The swagger description for the api looks like this:

Each high score list definition has a unique id that is typically mapped to a specific game or game mode (sliding puzzle with 3×3, 4×4 tiles etc). When creating a new high score list definition you provide these properties:

  • name: The name for the list. Can be the name of the game where the list is used.
  • lowIsBest: Depending on if the goal of the game is to get maximum number of points of minimum number of moves etc, you can define how the list is ordered.
  • unit: A unit for the high score lists. Could be e.g. “Score”, “Turns”, “Moves” etc.
  • maxSize: The maximum size of the list.

The data is stored in a MongoDB collection. For my demos I host the api in Azure as an app service and use a free tier in MongoDB Atlas for hosting the database.

With this api integrated with the games, if a you make it into the leaderboard, a name can be submitted:

and you will appear in the hall of fame:

The code for this high score service is available from GitHub: https://github.com/LarsBergqvist/highscore-service

For the integration code, see each game implementation.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s