Building a Simple Game – Part 5

Last time we started really adding meat – the logic of the game rules. Today we’re going to look into some more logic, and how we wrap it up.

Ending a Game

It’s a game, but it does have to end at some point. This commit take care of exactly that – identifying the condition that ends the game and stops processing new moves:

class MancalaGame
{
constructor(cnvsELID,_updatePlayerCallback,_showMsgCallback)
{
this.gameDone = false;
}
handleCellClick(boardCell)
{ //todo: clean this up
if (this.gameDone)
{
dbg("Game over, get outta here");
return;
}
let currentPlayerHasNoMoreMoves = this.player1Playing(this.board.allPlayer1Cells(c => this.board.stonesIn(c) <= 0)) ||
this.player2Playing(this.board.allPlayer2Cells(c => this.board.stonesIn(c) <= 0))
if (currentPlayerHasNoMoreMoves)
this.gameOver();
this.canvas.ifPresent(cnvs => {
drawBoardState(cnvs,this.board,this)
})
}
}
gameOver()
{
let player1StoneCount = this.board.player1StoneCount();
let player2StoneCount = this.board.player2StoneCount();
let a = ["Game Over","# Stones P1:" + player1StoneCount,"# Stones P2: " + player2StoneCount];
switch (true)
{
case player1StoneCount > player2StoneCount : a.push("Player 1 Wins!"); break;
case player2StoneCount > player1StoneCount : a.push("Player 2 Wins!"); break;
default : a.push("Draw!"); break;
}
this.showMsg(a.join("<br/>"));
this.setGameOver();
}
setGameOver() { this.gameDone = true; }
}
view raw game.js hosted with ❤ by GitHub
Game over functionality

The implementation is pretty straightforward: we add a simple flag (gameOver) to the MancalaGame class (line 6) and consult it before processing any move (line 12). Of course, we have to take care of setting the flag properly, which happens at lines 18-21.

The gameOver function (lines 30-45) takes care of 3 main things1:

  1. Determining the winner (lines 36-41)
  2. Notifying the UI (lines 35,43)
  3. Setting the gameOver flag (line 44).

The implementation is pretty simple and straightforward, but admittedly, this function does a bit too much. We’ll take care of that right away.

Also, taking care of the message displayed to the user is not ideal thing to do here, but it’s not terrible, in my opinion, in this case.

Some Cleanup Is In Order

At this point, it has become quite clear that the handleCellClick function was becoming convoluted. It was quickly approaching the point of being unmanageable, doing too many things and operating at different levels of abstraction; e.g. encoding game rules while also taking care of UI messages. It was time for some cleaning.

The next commit 2 did exactly that:

class MancalaGame
{
constructor(cnvsELID,_updatePlayerCallback,_showMsgCallback)
{
this._initializeBoardDrawing();
}
_initializeBoardDrawing()
{
this.canvas.ifPresent(cnvs => {
})
}
handleCellClick(boardCell)
{
if (!this.gameDone)
{
this._resetGameMessagePanel();
if (this.isValidMove(boardCell))
this._makeMove(boardCell)
else
this.showMsg("Invalid Move")
}
else dbg("Game over, stop procrastinating")
}
_makeMove(boardCell)
{
let lastCell = this.playCell(boardCell);
this._togglePlayerOrExtraTurn(lastCell)
this._checkAndDeclareGameOverIfNecessary()
this._redraw();
}
_resetGameMessagePanel()
{
this.showMsg(" ")
}
_togglePlayerOrExtraTurn(lastCell)
{
let lastCellIsHomeOfCurrentPlayer = this.player1Playing(this.board.isPlayer1Home(lastCell)) ||
this.player2Playing(this.board.isPlayer2Home(lastCell))
if (!lastCellIsHomeOfCurrentPlayer)
this.updatePlayerCallback(this.togglePlayer());
else
this.showMsg("Extra Turn")
}
_checkAndDeclareGameOverIfNecessary()
{
let currentPlayerHasNoMoreMoves = this.player1Playing(this.board.allPlayer1Cells(c => this.board.stonesIn(c) <= 0)) ||
this.player2Playing(this.board.allPlayer2Cells(c => this.board.stonesIn(c) <= 0))
if (currentPlayerHasNoMoreMoves)
this._gameOver();
}
_redraw()
{
this.canvas.ifPresent(cnvs => {
drawBoardState(cnvs,this.board,this)
})
}
playCell(boardCell)
{
let _ = this.board;
let targetCells = range(1,_.stonesIn(boardCell)).map(steps => _.cellFrom(boardCell,steps))
let lastCell = targetCells[targetCells.length-1];
let lastCellWasEmpty = _.stonesIn(lastCell) == 0;
targetCells.forEach(c => _.addStoneTo(c));
this._checkAndCaptureIfNecessary(lastCell,lastCellWasEmpty);
_.setCellStoneCount(boardCell,0);
return lastCell;
}
_checkAndCaptureIfNecessary(lastCell,lastCellWasEmpty)
{
let _ = this.board;
let isLastCellAHomeCell = _.isPlayer1Home(lastCell) || _.isPlayer2Home(lastCell);
let lastCellBelongsToCurrentPlayer = this.player1Playing(_.isPlayer1Cell(lastCell)) ||
this.player2Playing(_.isPlayer2Cell(lastCell))
if (lastCellWasEmpty && !isLastCellAHomeCell && lastCellBelongsToCurrentPlayer)
{ //capture the stones from the other player
let acrossCell = _.totalCellCount() – lastCell;
dbg("Capturing stones from " + acrossCell + " to " + lastCell)
_.setCellStoneCount(lastCell,_.stonesIn(lastCell) + _.stonesIn(acrossCell));
_.setCellStoneCount(acrossCell,0);
}
}
}
view raw game.js hosted with ❤ by GitHub
Cleaning up

The gist of what we’re doing here is a simple refactoring of “extract method”: taking a few lines of code, extracting them into a separate function/method and invoking that function in the right location. The main motivation is breaking down a long function into more digestible pieces, making the code more readable. There’s not even a lot of code reuse going on around here, which might be another motivation for extracting a piece of code into another function. The resulting code is, in my opinion, easier to follow, and troubleshoot in the future. Function logic is expressed more succinctly and in a more consistent level of abstraction.

Take for example the new handleCellClick function (lines 16-27 above). If you read it, its functionality can be summarized in one sentence: “reset the message panel, then assuming the game isn’t over and the move is valid, make the move”.

Similarly, the _makeMove function (lines 29-35 above), that is being called from the handleCellClick function, can be summarized in one sentence: “play the cell passed, then considering the last cell reached, decide if an extra turn is in place; check if we reached the end of the game and redraw the board.”3

This mental exercise of trying to describe a function’s implementation in a sentence or two4 is an important one when trying to assess the readability of the code, which from my experience is a crucial quality factor. I believe it’s hard to assess readability with an objective criteria, but when writing and reading my code, this is how I try to assess it.

Bug Fixing and a UI Improvement

The next two commits, affectionately known as 38feec5 and 8879f315, take care of fixing a bug, and making a small (but significant) addition to the user interface:

_checkAndCaptureIfNecessary(lastCell,lastCellWasEmpty)
{
let _ = this.board;
let isLastCellAHomeCell = _.isPlayer1Home(lastCell) || _.isPlayer2Home(lastCell);
let lastCellBelongsToCurrentPlayer = this.player1Playing(_.isPlayer1Cell(lastCell)) ||
this.player2Playing(_.isPlayer2Cell(lastCell))
if (lastCellWasEmpty && !isLastCellAHomeCell && lastCellBelongsToCurrentPlayer)
{ //capture the stones from the other player
let acrossCell = _.totalCellCount() – lastCell;
let targetHome = this.player == PLAYER.one ? _.player1Home() : _.player2Home();
let totalCapturedStones = _.stonesIn(lastCell) + _.stonesIn(acrossCell);
dbg("Capturing stones from " + acrossCell + " and " + lastCell + " to " + targetHome + ". Total: " + totalCapturedStones )
_.setCellStoneCount(targetHome,_.stonesIn(targetHome) + totalCapturedStones);
_.setCellStoneCount(acrossCell,0);
_.setCellStoneCount(lastCell,0);
}
}
view raw game.js hosted with ❤ by GitHub
Properly Capturing Stones

The logic for capturing the stones is pretty straightforward. One thing to note is that the bug fix is localized in one place6 – a testament to good code structure; though, to be fair, this isn’t really a cross-cutting concern. Another thing to note is the calculation of the cell across from the last cell – acrossCell (line 9) – the simple calculation can be done because we rely on array indices. A better implementation would have deferred this to the Board class, and exposed a method named something like getAcrossCell(fromCell), so we can let this implementation detail remain in the Board class; this is an example of an abstraction leak (anyone up for a pull request to fix this?).

The second commit takes care of creating and toggling the highlighting of the current player:

function createHighlights(cellCount)
{
let w = (_boardWidthInCells -2)* CELL_SIZE;
let t = TOP_LEFT.y;
let l = TOP_LEFT.x + CELL_SIZE;
let boardHeightInCells = 3;
p1Highlight = new fabric.Line([l,t,l+w,t],{selectable:false,stroke:'red',strokeWidth:3})
p2Highlight = new fabric.Line([l,t+(boardHeightInCells*CELL_SIZE),l+w,t+(boardHeightInCells*CELL_SIZE)],{selectable:false,stroke:'red',strokeWidth:3})
}
function toggleHighlights(canvas,player)
{
requires(player == 1 || player == 2,"Invalid player when switching highlights")
switch (player)
{
case 1 :
canvas.add(p1Highlight);
canvas.remove(p2Highlight);
break;
case 2 :
canvas.add(p2Highlight);
canvas.remove(p1Highlight);
break;
}
}
view raw drawing.js hosted with ❤ by GitHub
_initializeBoardDrawing()
{
toggleHighlights(cnvs,this.player.number)
}
togglePlayer()
{
this.canvas.ifPresent( cnvs => {
toggleHighlights(cnvs,this.player.number);
})
}
view raw game.js hosted with ❤ by GitHub
Highlighting the Current Player

Technically, the highlight itself is simply drawing a red line on the border of the current player’s side of the board. We create 2 instances of fabric.Line in the drawing module, and then simply add and remove them to the canvas when necessary. Note that the toggleHighlights function in the drawing module (line 13) receives the player’s number and queries it directly. I don’t see this as a case of using magic numbers since the numbers themselves are clearly representative of the player object they’re representing, 1 for player 1, and 2 for player 2. I preferred avoiding exposing directly the the PLAYER objects in the game module (game.js).


We’re almost done. Next time we’re going to add a feature, cleanup a bit, and fix a bug; at which point we should have a working version of the game.

Notes

  1. Some implementation helper functions are in the board module.
  2. To be honest, there was another more administrative commit in the middle, since it doesn’t add anything to the story, I conveniently skipped it
  3. Ok, that’s actually two sentences, but it’s still not much; you get the idea.
  4. and eventually trying to replicate that description in the code
  5. we tried humpty and dumpty, but it didn’t stick
  6. No other functions were harmed in the fixing of this bug

Leave a Reply

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