Building a Simple Game – Part 6

Last time we implemented the ending of the game, and cleaned up the code a bit.

In this post we’ll look into an enhancement that allows us to change the size of the game, some more refactoring and bug fixes. At the end of this post we’ll be at the point where we have a playable game.

Altering the Game Size

One feature we’d like to add is the ability to play variable size of the Mancala game. We treat the total number of cells as the “size” of the game. Since each player has the same number of cells, this needs to be an even number, and cells are split evenly between players.

The next commit adds this feature:

class MancalaGame
{
constructor(gameSize,cnvsELID,_updatePlayerCallback,_showMsgCallback)
{
this.cellCount = gameSize;
this.board = new Board(this.cellCount);
}
_initializeBoardDrawing()
{
drawBoard(cnvs,this.cellCount);
}
}
view raw game.js hosted with ❤ by GitHub
<script>
const SIZE_PARAM_NAME = 'size';
const DEFAULT_GAME_SIZE = 14;
const GAME_SIZE_UPPER_BOUND = 28;
const GAME_SIZE_LOWER_BOUND = 6;
function setup()
{
game = new main.MancalaGame(resolveGameSize(),'cnvsMain',updateCurrentPlayer,showMsg);
}
function resolveGameSize()
{
let params = new URLSearchParams(window.location.search);
let size = (params.has(SIZE_PARAM_NAME) && !isNaN(params.get(SIZE_PARAM_NAME))) ?
params.get(SIZE_PARAM_NAME)*1 : DEFAULT_GAME_SIZE;
let ret = Math.max( //we bound the game size between the minimum and the upper bound
Math.min(size
,GAME_SIZE_UPPER_BOUND || DEFAULT_GAME_SIZE)
,GAME_SIZE_LOWER_BOUND);
if (ret % 2 != 0) ret -= 1; //we have to make sure it's a divisble by 2.
console.log("Game size: " + ret);
return ret;
}
</script>
view raw index.html hosted with ❤ by GitHub
Adding option for variable game size

The implementation is pretty straightforward. Instead of having a constant (CELL_COUNT) in the controller module (game.js), we pass a parameter in the constructor (game.js, line 3), that is then fed to the Board data structure. The Board class already had this parameter, so it was already ready to work with any size, and did not assume a constant size.

What remains is simply having a way for the user to specify this. I decided to go with a simple URL parameter. This is simple enough to pass. So resolveGameSize in index.html (lines 15-27) takes care of reading the parameter from the URL query string and validating it. We then initialize the MancalaGame instance with this number (index.html line 12).

Again, Some Cleanup

The next two commits are really about a small but important refactoring. We’re encapsulating the board drawing in a class that takes care of all the drawing business, essentially encapsulating how the board is drawn and the user interaction with the board. The changes in drawing.js is mostly re-organization of different functions into class method (the BoardUI class). The more interesting part is how it’s actually used:

const {BoardUI} = require("./drawing.js")
class MancalaGame
{
constructor (…)
{
this.boardUI = new BoardUI(cnvsELID,this.cellCount,this)
this.boardUI.initializeBoardDrawing();
}
_redraw()
{
this.boardUI.drawBoardState(this.board,this);
}
togglePlayer()
{
this.player = this.player.theOtherOne();
this.boardUI.toggleHighlights(this.player.number);
return this.player;
}
}
view raw game.js hosted with ❤ by GitHub
Encapsulating the drawing code

The controller now doesn’t maintain a pointer to the canvas instance. Instead, it works through API provided by the BoardUI class, which is at a higher level of abstraction – initializeBoardDrawing, drawBoardState, toggleHighlights. The motivation here is really better encapsulation of the UI implementation.

The last commit for today takes care of one bug fix – making sure we skip the opponent’s mancala when making a move:

class Board
{
/**
* Retrieve the cell number for the home (Mancala) of the given player.
* @param {numer} player The number of the player whose Mancala we're seeking (1 or 2)
* @returns The cell number for the given player's Mancala ( either 0 or totalCellCount/2)
*/
homeOf(player)
{
requires(player == 1 || player == 2,"Player number can be either 1 or 2");
return player == 1 ? this.player1Home() : this.player2Home();
}
}
view raw board.js hosted with ❤ by GitHub
class MancalaGame
{
playCell(boardCell)
{
let targetCells = this._calculateTargetCellsForMove(boardCell);
}
_calculateTargetCellsForMove(fromCell)
{
let _ = this.board;
let stepCount = _.stonesIn(fromCell);
dbg("Playing " + stepCount + " stones from cell " + fromCell)
let targetCells = range(1,stepCount)
.map(steps => _.cellFrom(fromCell,steps,this.player.number))
.filter(c => c != _.homeOf(this.player.theOtherOne().number)) //remove, if applicable, the cell of the other player's mancala
while (targetCells.length < stepCount) //add any cells, until we reach a situation where we have enough holes to fill (per the stone count in the played cell)
{
let addedCell = _.cellFrom(targetCells[targetCells.length-1],1)
if (addedCell == _.homeOf(this.player.theOtherOne().number))
targetCells.push(_.cellFrom(addedCell,1))
else
targetCells.push(addedCell)
}
return targetCells;
}
}
view raw game.js hosted with ❤ by GitHub
Fixing a bug – skipping the opponents Mancala (home)

There’s nothing too fancy here. The gist of the fix is in lines 17-19 in the MancalaGame class1.

Incorporating this fix, however, prompted me to extract the calculation of the target cells to a different function (_calculateTargetCellsForMove), so the playCell function remains at the same level of abstraction, and is still readable2.

And We’re Done …

At this point, one should be able to build the code (npm run build) and point his browser to the resulting index.html. Look for it in the dist directory.

A working version of the game is available here, embedded here for your convenience:

Admittedly, it’s not much to look in terms of UI design. But it’s a simple game we did in a few hours time, and it works. If you followed along so far, give yourself a pat on the shoulder.

… But Wait, There’s More

The story of how to create a simple game is pretty much done. But there’s more that can be said and done here, more features to build, tweaks to do.

Concrete examples for more directions include better UI design, more features (undo/redo, saving game states, showing a log, etc.). I welcome any more suggestions, and of course pull requests.

If you look at the repo, you’ll see that I went in another direction for enhancement. I was more interested in incorporating bots (“AI”) into the game as a feature; so you could play against a bot, or even have two bots play against each other. Stay tuned for more on that front.

Notes

  1. line numbers refer to the snippet above
  2. or at least as readable as it was, and not worse

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.