Skip to content

Instantly share code, notes, and snippets.

@jackfirth
Created December 15, 2014 02:17
Show Gist options
  • Save jackfirth/13264618f57e3a869ac8 to your computer and use it in GitHub Desktop.
Save jackfirth/13264618f57e3a869ac8 to your computer and use it in GitHub Desktop.
Tic Tac Toe
<link href='http://fonts.googleapis.com/css?family=Fredoka+One' rel='stylesheet' type='text/css'>
<div data-ng-app="TicTacToeApp">
<div class="tic-tac-toe" data-ng-controller="TicTacToeController">
<div class="turn-header" data-ng-show = "!isGameOver">
{{activePlayer}} to move
</div>
<div class="turn-header" data-ng-show = "isGameOver && isTie">
tie game
</div>
<div class="turn-header" data-ng-show = "isGameOver && !isTie">
{{winner}} won the game
</div>
<div class="board">
<div class="row group" data-ng-repeat="row in board">
<div class="cell" data-ng-repeat="cell in row.cells" data-ng-click="clickCell(cell)" data-ng-class="{'cell-player-won': cell.winner && isX(winner), 'cell-player-lost': cell.winner && isO(winner)}">
<span class="cell-value">
{{cell.value}}
</span>
</div>
</div>
</div>
<div class="button-footer">
<button class="large" data-ng-disabled="!isGameOver" data-ng-click="resetGame()">
replay
</button>
</div>
<div class="button-footer">
<button data-ng-disabled="AI.active" data-ng-click="setActiveAI(AI)" data-ng-repeat="AI in AIs" >
{{AI.name}}
</button>
</div>
</div>
</div>
'use strict';
(function() {
var contractsEnabled = false;
/* String helpers */
function toStringWithQuotes(v) {
return typeof v === "string"
? '"' + v.toString() + '"'
: typeof v === "undefined"
? "undefined"
: v.toString();
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
/* Function wrapping and metadata manipulation */
Function.prototype.getName = function getName() {
return typeof this.wrappedName === "undefined" ? this.name : this.wrappedName;
}
Function.prototype.getLength = function getLength() {
return typeof this.wrappedName === "undefined" ? this.length : this.wrappedLength;
}
Function.prototype.wraps = function wraps(f) {
this.wrappedLength = f.getLength();
this.wrappedName = f.getName();
return this;
}
Function.prototype.renamed = function renamed(name) {
this.wrappedName = name;
return this;
}
/* Helper predicates - no contracts because they're used in building the contract functions */
var isOfType = (function isOfType(typeString) {
return (function (v) {
return typeof v === typeString;
}).renamed("is" + capitalizeFirstLetter(typeString));
});
var isBoolean = isOfType("boolean");
var isObject = isOfType("object");
var isFunction = isOfType("function");
var isUndefined = isOfType("undefined");
var isString = isOfType("string");
var isNumber = isOfType("number");
var isAny = (function isAny(v) {
return true;
});
var isInteger = (function isInteger(v) {
return typeof v === "number"
&& v % 1 === 0;
});
/* Arity checker */
Function.prototype.arity = function arity() {
var f = this;
return function() {
if (arguments.length != f.getLength()) {
throw new Error("arity mismatch in " + f.getName() + ": expected " + f.getLength() + " arguments, got " + arguments.length);
} else {
return f.apply(this, arguments);
}
}.wraps(f);
}
/* Function return value contract */
Function.prototype.post = (function post(p) {
var f = this;
return (function() {
var ret = f.apply(this, arguments);
if (p(ret)) {
return ret;
} else {
throw new Error(f.getName() + " broke it's contract: promised " + p.getName() + " but returned " + toStringWithQuotes(ret));
}
}).wraps(f);
}).arity();
/* Function arguments contract */
Function.prototype.pre = function pre() {
var f = this;
var ps = arguments;
return (function () {
for (var i=0; i < ps.length; i+=1) {
if (!ps[i](arguments[i])) {
throw new Error("contract error in " + f.getName() + ": expected " + ps[i].getName() + " at position " + i + " but received " + toStringWithQuotes(arguments[i]));
}
}
return f.apply(this, Array.prototype.slice.call(arguments, 0));
}).wraps(f);
};
Function.prototype.contract = function contract() {
var f = this;
if (f.getLength() !== arguments.length - 1) {
throw new Error("Contract construction error in " + f.getName() + " - number of argument predicates provided does not match number of function arguments - provided " + (arguments.length - 1) + " argument predicates");
}
if (!contractsEnabled)
return f;
var prePreds = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
var postPred = arguments[arguments.length - 1];
if (arguments.length > 1) {
return f
.arity()
.pre.apply(f, prePreds)
.post(postPred);
}
if (arguments.length == 1) {
return f
.arity()
.post(postPred);
}
else {
throw new Error("Contract construction error in " + f.getName() + " - no predicates provided");
}
};
/* Custom predicate helpers */
var isObjectOf = (function isObjectOf(name, propPredicates) {
return (function (v) {
if (!isObject(v)) {
return false;
}
for (var prop in propPredicates) {
if (propPredicates.hasOwnProperty(prop)) {
if (isUndefined(v[prop])) {
return false;
}
}
}
return true;
}).contract(isAny, isBoolean).renamed("is" + capitalizeFirstLetter(name));
}).contract(isString, isObject, isFunction);
var isValue = (function isValue(value) {
return (function (v) {
return v === value;
}).contract(isAny, isBoolean);
}).contract(isAny, isFunction);
var isNot = (function isNot(p) {
return (function (v) {
return !p(v);
}).contract(isAny, isBoolean);
}).contract(isFunction, isFunction);
var isOneOf = (function isOneOf() {
var ps = arguments;
return (function (v) {
for (var i = 0; i < ps.length; i += 1) {
if (ps[i](v)) {
return true;
}
}
return false;
}).contract(isAny, isBoolean);
});
var isEach = (function isEach() {
var ps = arguments;
return (function (v) {
for (var i = 0; i < ps.length; i += 1) {
if (!ps[i](v)) {
return false;
}
}
return true;
}).contract(isAny, isBoolean);
});
var hasPropertyOf = (function hasPropertyOf(name, p) {
return (function (v) {
return !isUndefined(v[name]) && p(v[name]);
}).contract(isAny, isBoolean);
}).contract(isString, isFunction, isFunction);
var isNumberInRange = (function isNumberInRange(low, high) {
return (function (v) {
return isNumber(v) && low <= v && v <= high;
}).contract(isAny, isBoolean).renamed("isNumberInRange" + low + "to" + high);
}).contract(isNumber, isNumber, isFunction);
var isDefined = isNot(isUndefined).renamed("isDefined");
/* FP helpers */
var buildArray = (function buildArray(length, f) {
var a = new Array();
for (var i=0; i<length; i++) {
a[i] = f(i);
}
return a;
}).contract(isInteger, isFunction, Array.isArray);
var zip = (function zip(f, as, bs) {
var arr = new Array();
for (var i = 0; i < as.length; i += 1) {
arr[i] = f(as[i], bs[i]);
}
return arr;
}).contract(isFunction, Array.isArray, Array.isArray, Array.isArray);
var foldOr = (function foldOr(f, vs) {
for (var i = 0; i < vs.length; i+=1) {
if (f(vs[i])) {
return true;
}
}
return false;
}).contract(isFunction, Array.isArray, isBoolean);
var foldAnd = (function foldAnd(f, vs) {
for (var i = 0; i < vs.length; i+=1) {
if (!f(vs[i])) {
return false;
}
}
return true;
}).contract(isFunction, Array.isArray, isBoolean);
/* Board data definitions */
var cellValues = {empty: "", x: "X", o: "O"}
var isEmptyCellValue = isValue(cellValues.empty).renamed("isEmptyCellValue");
var isX = isValue(cellValues.x).renamed("isX");
var isO = isValue(cellValues.o).renamed("isO");
var isPlayerValue = isOneOf(isX, isO).renamed("isPlayerValue");
var isCellValue = isOneOf(isEmptyCellValue, isPlayerValue).renamed("isCellValue");
var isCell = isObjectOf("cell", {
value: isCellValue,
row: isInteger,
column: isInteger,
winning: isBoolean
});
var isEmptyCell = isEach(isCell, hasPropertyOf("value", isEmptyCellValue)).renamed("isEmptyCell");
var isCellOrUndefined = isOneOf(isUndefined, isCell).renamed("isCellOrUndefined");
var isRow = isObjectOf("row", { cells: Array.isArray });
var isColumnIndex = isEach(isInteger, isNumberInRange(0, 2)).renamed("isColumnIndex");
var isRowIndex = isEach(isInteger, isNumberInRange(0, 2)).renamed("isRowIndex");
var isBoard = Array.isArray.contract(isAny, isBoolean).renamed("isBoard");
var isMoveAndValue = isObjectOf("moveAndValue", {
cell: isEmptyCell,
value: isInteger
});
/* Querying boards */
var getCell = (function getCell(board, row, column) {
return board[row].cells[column];
}).contract(isBoard, isRowIndex, isColumnIndex, isCell);
var getCellValue = (function getCellValue(board, row, column) {
return getCell(board, row, column).value;
}).contract(isBoard, isRowIndex, isColumnIndex, isCellValue);
var isPlayerAt = (function isPlayerAt(board, player, row, column) {
return isValue(player)(getCellValue(board, row, column));
}).contract(isBoard, isPlayerValue, isRowIndex, isColumnIndex, isBoolean);
var isPlayerAtEach = (function isPlayerAtEach(board, player, rows, columns) {
return foldAnd(
function(indices) { return isPlayerAt(board, player, indices.row, indices.column); },
zip(function(r, c) { return { row: r, column: c }; }, rows, columns));
}).contract(isBoard, isPlayerValue, Array.isArray, Array.isArray, isBoolean);
/* Board terminality predicates */
var rowFull = (function rowFull(row) {
return !foldOr(isEmptyCell, row.cells);
}).contract(isRow, isBoolean);
var isNotFullBoard = (function isNotFullBoard(board) {
return isBoard(board) && !foldAnd(rowFull, board);
}).contract(isAny, isBoolean);
var isRowWonBy = (function isRowWonBy(board, player, row) {
return isPlayerAtEach(board, player, [row, row, row], [0, 1, 2]);
}).contract(isBoard, isPlayerValue, isRowIndex, isBoolean);
var isColumnWonBy = (function isColumnWonBy(board, player, column) {
return isPlayerAtEach(board, player, [0, 1, 2], [column, column, column]);
}).contract(isBoard, isPlayerValue, isColumnIndex, isBoolean);
var anyRowsWonBy = (function anyRowsWonBy(board, player) {
return isRowWonBy(board, player, 0)
|| isRowWonBy(board, player, 1)
|| isRowWonBy(board, player, 2);
}).contract(isBoard, isPlayerValue, isBoolean);
var anyColumnsWonBy = (function anyColumnsWonBy(board, player) {
return isColumnWonBy(board, player, 0)
|| isColumnWonBy(board, player, 1)
|| isColumnWonBy(board, player, 2);
}).contract(isBoard, isPlayerValue, isBoolean);
var isTopLeftDiagonalWonBy = (function isTopLeftDiagonalWonBy(board, player) {
return isPlayerAtEach(board, player, [0, 1, 2], [0, 1, 2]);
}).contract(isBoard, isPlayerValue, isBoolean);
var isTopRightDiagonalWonBy = (function isTopLeftDiagonalWonBy(board, player) {
return isPlayerAtEach(board, player, [0, 1, 2], [2, 1, 0]);
}).contract(isBoard, isPlayerValue, isBoolean);
var isBoardWonBy = (function isBoardWonBy(board, player) {
return anyRowsWonBy(board, player)
|| anyColumnsWonBy(board, player)
|| isTopLeftDiagonalWonBy(board, player)
|| isTopRightDiagonalWonBy(board, player);
}).contract(isBoard, isPlayerValue, isBoolean);
var isTerminalBoard = (function isTerminalBoard(board) {
return isBoard(board)
&& (isBoardWonBy(board, cellValues.x)
|| isBoardWonBy(board, cellValues.o)
|| !isNotFullBoard(board));
}).contract(isAny, isBoolean);
var isNotTerminalBoard = isNot(isTerminalBoard).renamed("isNotTerminalBoard");
var otherPlayer = (function otherPlayer(v) {
return v === cellValues.x ? cellValues.o : cellValues.x;
}).contract(isPlayerValue, isPlayerValue);
/* Construction of data structures */
var makeEmptyCell = (function makeEmptyCell(row) {
return (function makeEmptyRowCell(column) {
return {
value: cellValues.empty,
row: row,
column: column,
winning: false
};
}).contract(isInteger, isEmptyCell);
}).contract(isInteger, isFunction);
var makeEmptyRow = (function makeEmptyRow(row) {
return { cells: buildArray(3, makeEmptyCell(row)) };
}).contract(isInteger, isRow);
var makeEmptyBoard = (function makeEmptyBoard() {
return buildArray(3, makeEmptyRow);
}).contract(Array.isArray);
var copyBoard = (function copiedBoard(board) {
var copied = makeEmptyBoard();
for (var row = 0; row < 3; row += 1) {
for (var column = 0; column < 3; column += 1) {
copied[row].cells[column].value = board[row].cells[column].value;
}
}
return copied;
}).contract(isBoard, isBoard);
var getBoardWithMove = (function getBoardWithMove(board, row, column, player) {
var copiedBoard = copyBoard(board);
copiedBoard[row].cells[column].value = player;
return copiedBoard;
}).contract(isNotFullBoard, isRowIndex, isColumnIndex, isPlayerValue, isBoard);
/* Random move AI */
var randomInArray = (function randomInArray(array) {
return array[Math.floor(Math.random() * array.length)];
}).contract(Array.isArray, isAny);
var randomRowCell = (function randomRowCell(row) {
return randomInArray(row.cells);
}).contract(isRow, isCell);
var randomRow = (function randomRow(board) {
return randomInArray(board);
}).contract(isBoard, isRow);
var randomMove = (function randomMove(board, movingPlayer) {
var cell = randomRowCell(randomRow(board));
return isEmptyCell(cell) ? cell : randomMove(board, movingPlayer);
}).contract(isNotTerminalBoard, isPlayerValue, isEmptyCell);
/* Optimal move AI */
var minMove = (function minMove(board, movingPlayer, lastCell) {
var bestMove = maxMove(board, movingPlayer, lastCell);
return {
cell: bestMove.cell,
value: bestMove.value * -1
};
}).contract(isBoard, isPlayerValue, isCell, isMoveAndValue);
var maxMove = (function maxMove(board, movingPlayer, lastCell) {
if (isBoardWonBy(board, movingPlayer)) {
return { cell: lastCell, value: 1 };
} if (isBoardWonBy(board, otherPlayer(movingPlayer))) {
return { cell: lastCell, value: -1 };
} if (!isNotFullBoard(board)) {
return { cell: lastCell, value: 0 };
}
var bestMove = false;
for (var row = 0; row < 3; row += 1) {
for (var column = 0; column < 3; column += 1) {
var currentCell = board[row].cells[column];
if (!isEmptyCell(currentCell)) {
continue;
}
var boardWithMove = getBoardWithMove(board, row, column, movingPlayer);
var boardMinMove = minMove(boardWithMove, otherPlayer(movingPlayer), currentCell);
if (!bestMove || boardMinMove.value > bestMove.value) {
bestMove = { cell: currentCell, value: boardMinMove.value };
}
if (bestMove.value === 1) {
return bestMove;
}
}
}
return bestMove;
}).contract(isBoard, isPlayerValue, isCellOrUndefined, isMoveAndValue);
var bestMove = (function bestMove(board, movingPlayer) {
if (isEmptyCellValue(getCellValue(board, 1, 1))) {
return getCell(board, 1, 1);
} else {
return maxMove(board, movingPlayer, window.undefined).cell;
}
}).contract(isNotTerminalBoard, isPlayerValue, isEmptyCell);
/* Optimal move AI with mistakes */
var bestMoveWithMistakes = (function bestMoveWithMistakes(board, movingPlayer) {
if (Math.random() < .2) {
return randomMove(board, movingPlayer);
} else {
return bestMove(board, movingPlayer);
}
}).contract(isNotTerminalBoard, isPlayerValue, isEmptyCell);
/* App construction */
angular
.module('TicTacToeApp', [])
.controller(
'TicTacToeController',
['$scope', '$timeout',
function($scope, $timeout) {
$scope.board = makeEmptyBoard();
$scope.activePlayer = cellValues.x;
$scope.isGameOver = false;
$scope.winner = undefined;
$scope.isTie = undefined;
$scope.isX = isX;
$scope.isO = isO;
$scope.nextTurn = function nextTurn() {
$scope.activePlayer = otherPlayer($scope.activePlayer);
};
$scope.AIfunc = randomMove;
$scope.AIs = [
{
active: true,
name: "easy",
func: randomMove
},
{
active: false,
name: "normal",
func: bestMoveWithMistakes
},
{
active: false,
name: "hard",
func: bestMove
}
];
$scope.setActiveAI = function setActiveAI(AI) {
for (var i = 0; i < $scope.AIs.length; i+=1) {
$scope.AIs[i].active = false;
}
AI.active = true;
$scope.AIfunc = AI.func;
$scope.resetGame();
};
$scope.clickCell = function clickCell(cell) {
if (!$scope.isGameOver && isEmptyCell(cell) && isX($scope.activePlayer)) {
cell.value = cellValues.x;
if ($scope.gameOver()) {
$scope.endGame();
} else {
$scope.nextTurn();
$timeout($scope.takeAITurn, 500);
}
}
};
$scope.takeAITurn = function() {
$scope.AIfunc($scope.board, $scope.activePlayer).value = $scope.activePlayer;
if ($scope.gameOver()) {
$scope.endGame();
} else {
$scope.nextTurn();
}
};
$scope.gameOver = function gameOver() {
return isTerminalBoard($scope.board);
};
$scope.endGame = function endGame() {
$scope.isGameOver = true;
$scope.isTie = false;
if (isBoardWonBy($scope.board, cellValues.x)) {
$scope.winner = cellValues.x;
} else if (isBoardWonBy($scope.board, cellValues.o)) {
$scope.winner = cellValues.o;
} else if (!isNotFullBoard($scope.board)) {
$scope.isTie = true;
} else {
throw new Error("endGame method was called while board was not in a terminal state");
}
if (!$scope.isTie) {
for (var i = 0; i < 3; i += 1) {
if (isRowWonBy($scope.board, $scope.winner, i)) {
for (var j = 0; j < 3; j += 1) {
getCell($scope.board, i, j).winner = true;
}
}
if (isColumnWonBy($scope.board, $scope.winner, i)) {
for (var j = 0; j < 3; j += 1) {
getCell($scope.board, j, i).winner = true;
}
}
}
if (isTopLeftDiagonalWonBy($scope.board, $scope.winner)) {
getCell($scope.board, 0, 0).winner = true;
getCell($scope.board, 1, 1).winner = true;
getCell($scope.board, 2, 2).winner = true;
}
if (isTopRightDiagonalWonBy($scope.board, $scope.winner)) {
getCell($scope.board, 0, 2).winner = true;
getCell($scope.board, 1, 1).winner = true;
getCell($scope.board, 2, 0).winner = true;
}
}
};
$scope.resetGame = (function resetGame() {
$scope.board = makeEmptyBoard();
$scope.isGameOver = false;
$scope.winner = undefined;
$scope.tie = undefined;
$scope.activePlayer = cellValues.x;
});
}]);
})();
/* box-sizing */
html {
box-sizing: border-box;
font-family: 'Fredoka One', cursive;
background: #1A1A1A;
height: 100%;
}
*, *:before, *:after {
box-sizing: inherit;
}
/* clearfix */
.group:after {
content: "";
display: table;
clear: both;
}
.tic-tac-toe {
width: 400px;
margin: 50px auto;
}
.board {
height: 400px;
}
.turn-header {
text-align: center;
font-size: 300%;
color: darkgrey;
padding-bottom: 20px;
}
.row {
height: 33.3333%
}
.row:not(:last-of-type) {
border-bottom: solid 2px darkgrey;
}
.cell {
float: left;
width: 33.3333%;
height: 100%;
color: darkgrey;
position: relative;
font-size: 600%;
}
.cell:not(:last-of-type) {
border-right: solid 2px darkgrey;
}
.cell-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.cell-player-won {
animation-name: winner-glow;
animation-duration: .5s;
animation-iteration-count: infinite;
}
@keyframes winner-glow {
0% {
color: darkgrey;
}
50% {
color: darkgreen;
}
100% {
color: darkgrey;
}
}
.cell-player-lost {
animation-name: loser-glow;
animation-duration: .5s;
animation-iteration-count: infinite;
}
@keyframes loser-glow {
0% {
color: darkgrey;
}
50% {
color: darkred;
}
100% {
color: darkgrey;
}
}
.button-footer {
margin: 20px auto;
text-align: center;
}
button {
border: solid 2px #666;
border-radius: 5px;
font-family: inherit;
color: darkgrey;
padding: 5px 10px;
font-size: 150%;
background: #333;
}
button.large {
font-size: 200%;
}
button:disabled {
background: #222;
color: #444;
border-color: #444;
}
button:not(:last-of-type) {
border-right: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
margin-right: 0px;
}
button:not(:first-of-type) {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
margin-left: 0px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment