Adding Bots to a Simple Game

We have previously seen how we can create a fairly simple game in a short amount of time. In the closing post of the series, I mentioned one possible direction for evolving the code was adding the option to “play against the computer”, i.e. have the option for an automated player, or even two.

This is not only interesting from the coding point of view. It also allows us to explore how to code heuristics and/or “AI” into the game, compare strategies, etc.

In this post, I will describe the necessary changes made in the code in order to incorporate these bots into the game. This is not meant to be anyway authoritative source on how to write good AI for games, or how to implement it efficiently. It’s just one way to add this kind of feature.

If you want to see the end result, have a look at the deployed game instance, and follow the instructions for specifying automated players.

Enabling Automated Players

The first order of business is to facilitate introduction of AI players (“bots”). So far, the code we built was responding to mouse clicks in the browser, in other words, driven by human interaction. We’d like to allow for moves to be decided by code, and for the game to invoke such code and proceed with the game, without any human interaction.

Keeping to the spirit of the original design, we’re still building the automated players as part of the Javascript, browser-based code. We don’t want to introduce another separate runtime component to the system.

So the next two commits (6a4c509, 5489462) introduce this facility:

const PLAYER = {
one : {
toString : () => "ONE"
, theOtherOne : () => PLAYER.two
, number : 1
, ai : () => PLAYER.one._aiPlayer
, _aiPlayer : None
}
, two : {
toString : () => "TWO"
, theOtherOne : () => PLAYER.one
, number : 2
, ai : () => PLAYER.two._aiPlayer
, _aiPlayer : None
}
}
class MancalaGame
{
constructor(gameSize,cnvsELID,_updatePlayerCallback,_showMsgCallback,requestedAIPlayers)
{
this._setupAIPlayersIfRequested(requestedAIPlayers);
}
_setupAIPlayersIfRequested(requestedAIPlayers)
{
dbg("Setting AI Players…");
PLAYER.one._aiPlayer = maybe(determineAIPlayer(requestedAIPlayers.p1))
PLAYER.two._aiPlayer = maybe(determineAIPlayer(requestedAIPlayers.p2))
dbg("AI Players: P1: " + PLAYER.one._aiPlayer + ", P2: " + PLAYER.two._aiPlayer);
function determineAIPlayer(requestedAI)
{
return requestedAI ? new SimpleAIPlayer() : null;
}
}
start()
{
this._makeNextMoveIfCurrentPlayerIsAI();
}
handleCellClick(boardCell)
{
this._makeNextMoveIfCurrentPlayerIsAI()
}
_makeNextMoveIfCurrentPlayerIsAI()
{
if (!this.gameDone)
{
this.player.ai().ifPresent(aiPlayer => {
let aiMove = aiPlayer.nextMove(this.board,this.player.number)
setTimeout(() => { this._makeMove(aiMove)}, 200) //artifical wait, so we can "see" the ai playing
})
}
}
}
view raw game.js hosted with ❤ by GitHub
const P2_PARAM_NAME = "p2";
const P1_PARAM_NAME = "p1";
function setup()
{
game = new main.MancalaGame(resolveGameSize(),'cnvsMain',updateCurrentPlayer,showMsg, resolveRequestedAI());
game.start();
}
function resolveRequestedAI()
{
let params = new URLSearchParams(window.location.search);
let p2 = params.has(P2_PARAM_NAME) ? params.get(P2_PARAM_NAME) : "";
let p1 = params.has(P1_PARAM_NAME) ? params.get(P1_PARAM_NAME) : "";
return { p1 : p1, p2 : p2}
}
view raw index.html hosted with ❤ by GitHub
class SimpleAIPlayer
{
constructor()
{
}
/**
* Given a board and side to play, return the cell to play
* @param {Board} board The current board to play
* @param {number} side The side to player, either 1 or 2
*
* @returns The board cell to play
*/
nextMove(board,side)
{
requires(board != null, "Board can't be null for calculating next move")
requires(side == 1 || side == 2,"Side must be either 1 or 2")
//Simple heuristic: choose the cell with the largest number of stones.
var maxStoneCell = side == 1 ? 1 : board.totalCellCount()-1;
switch (side)
{
case 1 : board.forAllPlayer1Cells(c => { if (board.stonesIn(c) > board.stonesIn(maxStoneCell)) maxStoneCell = c })
case 2 : board.forAllPlayer2Cells(c => { if (board.stonesIn(c) > board.stonesIn(maxStoneCell)) maxStoneCell = c })
}
dbg("Playing cell: " + maxStoneCell + " with " + board.stonesIn(maxStoneCell) + " for player " + side);
return maxStoneCell;
}
}
module.exports = {
SimpleAIPlayer
}
Mancala Bots

You can see that the setup of the AI players is done during the construction of the MancalaGame class (game.js, line 24). The requested AI player type is passed using URL parameters (index.html, lines 6,10-16) which are then parsed and used for initializing the bots. Note that the necessary player class is setup in the global PLAYER objects1. At this point we have only one type of player – SimpleAIPlayer.

The invocation of the AI player happens when responding to the player’s click (game.js, line 50). The _makeNextMoveIfCurrentPlayerIsAI function (game.js lines 53-62) simply checks that the game isn’t done, and whether the current player is an AI one. If it is, we invoke the nextMove function which is the main function in the AI player’s interface. The result of this function is simply the cell to play. The function then invokes the makeMove function, which is also used for human players from the UI.

Another small addition is the start function for the MancalaGame class (game.js, lines 42-45). Which simply tests if player one (the current player at the beginning of the game), is an AI, and makes a move. This is used to kickstart a game where the first player is a bot. The 2nd player being a bot, and further moves of the 1st player, is handled through the mechanism described above.

We Start Simple

The first implementation of a bot in this game (SimpleAIPlayer.js) is dead simple – it’s based on a very naïve heuristic: play the cell with the most cells in it. To be honest, it’s only here to serve as a straw man for testing the whole bot mechanism; there’s not a lot of wisdom in this heuristic. It can also serve as a benchmark for testing other strategies.

The implementation of the nextMove method – the only method in the bot interface – is pretty straightforward: based on the current player playing (the side parameter), simply search for the cell with the most stones on that side of the board (lines 21-26 in SimpleAIPlayer.js). Note that this strategy is deterministic – given a board state, it will always choose the same cell2. This is important when testing this, and also for testing this against other bot strategies.

Mancala Bot Wars

The next commit adds another simple AI bot player – Random AI Player, which simply picks a cell to play at random. This in itself isn’t very interesting. What is interesting3 is that now we have two types of bots, and we can run experiments on what strategy is better – we just pit the two bots against each other.

In order to do that, we’ll write some code that enables us to run the game with two bots and record the result. The next commit takes care of that:

program
.version('0.0.1')
.usage("Mancala Bot War: Run Mancala AI Experiment")
.option('-p1, –player1 <AI 1 type>', "The bot to use for player 1")
.option('-p2, –player2 <AI 2 type>', "The bot to use for player 2")
.option('-r, –rounds <rnds>', "Number of games to play")
.option('-o, –out <dir>', "output file")
.parse(process.argv)
range(1,program.rounds*1).forEach(round => {
dbg(`Round ${round}`)
let game = new MancalaGame(14,'',_ => {},gameMsg,{p1 : program.player1, p2:program.player2},results => {
dbg (`Round ${round} Results: ${results}`)
let line = []
line.push(results.player1StoneCount)
line.push(results.player2StoneCount)
line.push(results.winner || 0) //write the player who one or 0 for draw
fs.appendFileSync(program.out,line.join(',') + "\n",{flag :'a'})
})
game.start();
})
view raw bots.js hosted with ❤ by GitHub
class MancalaGame
{
constructor(gameSize,cnvsELID,_updatePlayerCallback,_showMsgCallback,requestedAIPlayers,_gameOverCallback)
{
this.gameOverCallback = _gameOverCallback
if (cnvsELID)
{
this.boardUI = maybe(new BoardUI(cnvsELID,this.cellCount,this))
this.boardUI.ifPresent(bui => bui.initializeBoardDrawing());
}
else this.boardUI = None; //headless mode
}
_redraw()
{
this.boardUI.ifPresent(_ => _.drawBoardState(this.board,this));
}
_gameOver()
{
let player1StoneCount = this.board.player1StoneCount();
let player2StoneCount = this.board.player2StoneCount();
let results = { player1StoneCount : player1StoneCount, player2StoneCount : player2StoneCount}
switch (true)
{
case player1StoneCount > player2StoneCount : results.winner = 1; break;
case player2StoneCount > player1StoneCount : results.winner = 2; break;
default : results.isDraw = true; break;
}
this.gameOverCallback(results);
}
togglePlayer()
{
this.player = this.player.theOtherOne();
this.boardUI.ifPresent(_ => _.toggleHighlights(this.player.number));
return this.player;
}
}
view raw game.js hosted with ❤ by GitHub
<script>
function setup()
{
game = new main.MancalaGame(resolveGameSize(),'cnvsMain',updateCurrentPlayer,showMsg, resolveRequestedAI(),gameOver);
game.start();
}
function gameOver(results)
{
let a = ["Game Over","# Stones P1:" + results.player1StoneCount,"# Stones P2: " + results.player2StoneCount];
if (!results.isDraw)
a.push(`Player ${results.winner} Wins!`)
else
a.push('Draw!')
showMsg(a.join('<br/>'))
}
</script>
view raw index.html hosted with ❤ by GitHub
Mancala Bot Wars!

First, you’ll note the bots.js file, which is a simple script, invoked from the command line, that simply runs several rounds of the game and writes the results to a file. Every round creates a new game instance with the chosen player types, and calls game.start. It’s a script with some parameters, nothing fancy.

In order to support this, we need to refactor the MancalaGame class a bit. Specifically, we need it to be able to run w/o UI. This means two things: the game needs to run without a UI, and delivering the results has to be decoupled from the UI as well.

This is what happens in the game.js file. In lines 3,6 you can see we add a callback to announce that the game is over, and transmit the result. This callback is used in the _gameOver method, in line 35. Note that the code for showing the game over message moved out from this class (as it should); look for it in the index.html file.

In addition, we enable the client of the MancalaGame class to pass a null value for the cnvsELID constructor parameter, which signals to us that there’s no canvas UI to be used for this game instance. The boardUI member then becomes optional (lines 8-13) and whenever we access the boardUI, we need to make sure it’s present (lines 18,41).

We now have a game that can be run without human intervention, and play out solely with bots. We can now start experimenting with different game parameters, and see what strategy plays out better.

Prime geeky goodness.


Later commits clean up the bot experiment script, and add features. We also add other types of both player, with different heuristics (e.g. greedy capture, minimax player) so we can compare more strategies.

Notes

  1. this is not good, it violates the encapsulation of the MancalaGame class; but it works in this setup (but should be refactored)
  2. Try to figure out what happens if several cells have the same number. This could be a place to inject some randomness into the heuristic
  3. at least to me

Leave a Reply

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