Requires Underscore and jQuery
I'm looking for suggestions on how to make this a little more readable and additional suggestions on flow, logic, etc.
The game can be run in a browser by just copying and pasting the code(HTML/JS/CSS) below.
JavaScript: app/js/app.js
var Utils = {
rules: {
isHorizontal: function (array, move) {
return array[0] === move[0];
},
isVertical: function (array, move) {
return array[1] === move[1];
},
isDiagonalDown: function (array) {
return array[0] === array[1];
},
isDiagonalUp: function (array, boardSize) {
return (array[0] + array[1]) === (boardSize - 1);
}
},
isDuplicate: function (moves, move) {
var duplicateAttempt = false;
for (var x = 0, l = moves.length; x < l; x++) {
if ((moves[x][0] === move[0]) && (moves[x][1] === move[1])) {
duplicateAttempt = true;
}
}
return duplicateAttempt;
},
nextPlayerIndex: function (prevPlayer, players) {
if (prevPlayer === null) {
return Utils.randomPlayerIndex(players);
} else {
return (prevPlayer === players.length - 1) ? 0 : prevPlayer + 1;
}
},
randomPlayerIndex: function (players) {
return Math.floor(Math.random() * (players.length));
}
};
var Markup = {
template: function (players, size) {
var playerList = function () {
var list = '<ul>';
_.each(players, function (player) {
list = list + '<li>' + player.symbol + ' / ' + player.name;
});
return list;
},
table = '<table>',
thead = '<thead><tr><th colspan="' + size + '"> ' + playerList() + ' <tbody>'
row = '<tr>',
cell = '<td style="width: ' + (100 / size).toFixed(2) + '%;">';
table = table + thead;
for (var x = 0; x < size; x++) {
table = table + row;
for (var y = 0; y < size; y++) {
table = table + cell;
}
}
return $(table);
},
off: function (gameboard) {
gameboard.off('click', 'td').find('table').addClass('complete');
},
getCell: function (index, gameboard) {
return $('tbody tr', gameboard).eq(index[0]).children().eq(index[1]);
},
markCell: function (index, symbol, gameboard) {
var cell = Markup.getCell(index, gameboard);
cell.html(symbol ? symbol : 'x').addClass('played');
},
highlightCells: function (array, gameboard) {
_.each(array, function (index) {
var cell = Markup.getCell(index, gameboard);
cell.addClass('winning-cell');
});
},
markNextPlayer: function (gameboard, game) {
$('thead li', gameboard).removeClass('turn').eq(game.currentPlayer).addClass('turn');
}
};
var Game = function (players, size) {
this.data = {
players: [],
size: size || 3,
allMoves: [],
ended: false,
prevPlayer: null,
currentPlayer: null,
winningMoves: {},
winCondition: [],
status: {}
};
_.each(players, function (player) {
_.extend(player, { moves: [], isWinner: false });
this.data.players.push(player);
}, this);
this.data.currentPlayer = Utils.randomPlayerIndex(this.data.players);
};
_.extend(Game.prototype, {
play: function (player, move) {
var g = this.data,
p = g.players[g.currentPlayer];
g.winningMoves = { horizontal: [], vertical: [], diagonalDown: [], diagonalUp: [] };
g.status.wrongPlayer = g.currentPlayer !== player;
g.status.isDuplicate = Utils.isDuplicate(g.allMoves, move);
if (g.status.wrongPlayer) { return g; }
if (g.status.isDuplicate) { return g; }
g.allMoves.push(move);
p.moves.push(move);
if (p.moves.length >= g.size) {
_.each(p.moves, function (pmove) {
this.checkRules(pmove, move);
}, this);
g.winCondition = this.findWinner(g.winningMoves, g.size);
p.isWinner = true;
}
if (g.allMoves.length === (g.size * g.size) && !g.winCondition) {
_.each(g.players, function (pl) {
pl.isWinner = null;
});
g.winCondition = [];
}
g.prevPlayer = g.currentPlayer;
g.currentPlayer = Utils.nextPlayerIndex(g.prevPlayer, g.players);
return g;
},
checkRules: function (playerMoves, lastMove) {
if (Utils.rules.isHorizontal(playerMoves, lastMove)) {
this.data.winningMoves.horizontal.push(playerMoves);
}
if (Utils.rules.isVertical(playerMoves, lastMove)) {
this.data.winningMoves.vertical.push(playerMoves);
}
if (Utils.rules.isDiagonalDown(playerMoves)) {
this.data.winningMoves.diagonalDown.push(playerMoves);
}
if (Utils.rules.isDiagonalUp(playerMoves, this.data.size)) {
this.data.winningMoves.diagonalUp.push(playerMoves);
}
},
findWinner: function (moves, size) {
var winner = [];
_.each(moves, function (move) {
if (move.length >= size) {
winner = move;
}
}, this);
return winner;
}
});
$(function () {
var gameboard = $('.gameboard'),
players = [
{ name: 'John Doe', symbol: 'x' },
{ name: 'Peter Brown', symbol: 'o' }
],
startGame = function (gameboard, players) {
var game = new Game(players, 3);
gameboard.append(Markup.template(game.data.players, game.data.size));
Markup.markNextPlayer(gameboard, game.data);
gameboard.on('click', 'td', function () {
var move = [$(this).parent()[0].sectionRowIndex, this.cellIndex],
play = game.play(game.data.currentPlayer, move);
if (play.status.wrongPlayer) { console.log('Wrong player'); return; }
if (play.status.isDuplicate) { console.log('Position already played'); return; }
Markup.markCell(move, play.players[play.prevPlayer].symbol, gameboard);
if (_.isArray(play.winCondition) && play.winCondition.length) {
Markup.highlightCells(play.winCondition, gameboard);
Markup.off(gameboard);
}
if (!play.winCondition.length) {
Markup.markNextPlayer(gameboard, play);
}
});
},
clearGame = function (gameboard) {
$('table', gameboard).remove();
gameboard.off('click');
};
$('#startNewGame').click(function () {
clearGame(gameboard);
startGame(gameboard, players);
});
}());
HTML: /index.html
<html>
<head>
<title>Tic Tac Toe</title>
<link href="app/css/app.css" rel="stylesheet" media="screen" />
</head>
<body>
<button id="startNewGame">Start New Game</button>
<div class="gameboard"></div>
<script src="http://code.jquery.com/jquery.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="app/js/app.js"></script>
</body>
</html>
CSS: /app/css/app.css
table {
border: 0 solid #ccc;
margin: 1% auto;
width: 60%;
height:40em;
}
table.complete td:hover {
background-color:#707070;
}
table th {
font-family:Arial, Verdana;
height:3em;
background-color:#ffd800;
padding: 0 1em;
}
table td {
border: 0 solid #ccc;
text-align:center;
font-size: 10em;
line-height:0;
background-color: #707070;
color: #fff;
text-transform:uppercase;
}
table td.played:hover {
background-color:#707070;
}
table td:hover {
background-color:#878686;
}
table td.winning-cell:hover {
background-color:#000;
}
ul {
list-style:none;
margin:0;
padding: 0;
}
ul li {
float:left;
width:50%;
text-align:left;
color:#fff6c6;
}
li.turn {
color:#000;
}
.winning-cell {
background-color:#000;
}