// Wait till all code has been run befor starting
setTimeout(()=>game.state = game.states.start,0);
const options = {
startPause : 2000,
statePause : 1000,
text : {
start: `Tic Tac Toe` ,
win : "## wins!", // ## is replaced with player name
draw : "It's a draw.",
turn : "Player ##'s turn", // ## is replaced with player name
moves : {
block : "blocking move",
win : "winning move",
random : "move",
},
playerA : "CPU X",
playerB : "CPU O",
}
}
/*===================================================================*/
/* Game rules in logic form */
const ticTacToe = {
layout : "876543210",
get positions() { return [1,2,4,8,16,32,64,128,256] },
wins : [7,56,448,273,84,292,146,73],
moveStr(move) { return " @ " + (3-(Math.log2(move) % 3)) + "," + (3-(Math.log2(move)/3 | 0)) },
isWin(moves) { return ticTacToe.wins.some(win => (moves & win) === win ) },
}
/*===================================================================*/
/* Manages players and their turns */
const player = (name, as) => ({name, as, moves : 0, toString() { return this.name } });
const players = {
playing : [player(options.text.playerA, "X"), player(options.text.playerB, "O")],
turn : 0,
get next() { return this.playing[(++this.turn) % 2] },
get current() { return this.playing[this.turn % 2] },
get opponant() { return this.playing[(this.turn + 1) % 2] },
reset() {
this.playing[0].moves = 0;
this.playing[1].moves = 0;
this.turn = Math.random() * 2 | 0;
},
}
/*===================================================================*/
/* handles board related stuff */
const board = {
reset() { board.moves = ticTacToe.positions },
get full() { return board.moves.length === 0 },
get randomMove() { return board.moves.splice(Math.random() * board.moves.length | 0, 1)[0] },
show(players) {
const p1 = players.playing[0], p2 = players.playing[1], m1 = p1.moves, m2 = p2.moves
return ticTacToe.layout.replace(/[0-9]/g, i => m1 & (1 << i) ? p1.as : m2 & (1 << i) ? p2.as : ".");
},
winningMove(moves){
var index = -1;
ticTacToe.wins.some(win =>(index = board.moves.findIndex(move => ((moves | move) & win) === win)) > -1);
if(index > -1) { return board.moves.splice(index,1)[0] }
return -1;
},
}
/*===================================================================*/
/* Plays the game */
const game = {
states : { start: 1, nextMove: 2, win: 3, draw: 4, humansTurn: 5, waitForNext: 6},
set humanPlaying(val) {
this.humanFoe = true;
board.reset();
players.playing[0].name = "Man X";
players.playing[0].isHuman = true;
log("board", board.show(players));
},
set state(state) {
clearTimeout(this.timerHdl);
var next,time = options.statePause;
switch(state){
case game.states.humansTurn:
time = 100000;
next = game.states.humansTurn;
break;
case game.states.waitForNext:
next = game.states.nextMove;
time = options.startPause / 3;
break;
case game.states.nextMove:
next = game.playTurn();
break;
case game.states.start:
log("status", options.text.start);
players.reset()
board.reset();
next = game.states.nextMove;
break;
case game.states.win:
log("status", options.text.win.replace("##",players.current));
next = game.states.start;
break;
case game.states.draw:
log("status", options.text.draw);
next = game.states.start;
}
this.timerHdl = setTimeout(() => game.state = next, time);
},
set humanPlays(move){
const player = players.current
if(player.isHuman){
const moveIdx = board.moves.indexOf(move);
if(moveIdx === -1) {
log("status","INVALID move Man stupid... forfits game.");
players.next;
clearTimeout(this.timerHdl);
this.timerHdl = setTimeout(() => game.state = game.states.win, 2000);
return;
}
board.moves.splice(moveIdx,1);
log("info", ticTacToe.moveStr(move))
player.moves += move;
log("board", board.show(players));
var nextState = game.states.waitForNext;
if (ticTacToe.isWin(player.moves)) { nextState = game.states.win }
if (board.full) { nextState = game.states.draw }
game.state = nextState;
} else {
log("status","Man caught cheating! forfits game.");
players.next;
clearTimeout(this.timerHdl);
this.timerHdl = setTimeout(()=> game.state = game.states.win, 2000);
}
},
playTurn() {
const player = players.next;
if(player.isHuman) {
log("board", board.show(players));
log("status", options.text.turn.replace("##",player));
return game.states.humansTurn;
}
log("status", options.text.turn.replace("##",player));
var moveStr = player + "'s ";
var move = board.winningMove(player.moves);
if (move === -1) {
move = board.winningMove(players.opponant.moves);
if (move === -1) {
moveStr += options.text.moves.random;
move = board.randomMove;
} else { moveStr += options.text.moves.block }
} else { moveStr += options.text.moves.win }
log("info", moveStr + ticTacToe.moveStr(move))
player.moves += move;
log("board", board.show(players));
if (ticTacToe.isWin(player.moves)) { return game.states.win }
if (board.full) { return game.states.draw }
return game.states.nextMove;
},
}
/*===================================================================*/
/* Default display interface is to the console */
/* You need to config the ticTacToe.layout string for what you need */
const log = (type, ...args) => {
if (type === "register") {
log.board = args[0];
log.status = args[1];
log.info = args[2];
} else if (type === "board") {
log.board(...args);
} else if (type === "info") {
log.info(...args);
} else {
log.status(...args);
}
}
log.info = log.status = log.board = console.log;
/*===================================================================*/
/* An interface to the display as the game is writen for the console */
log("register",showBoard, showStatus, showInfo); // point logger to new display
const displayTable = [b0,b1,b2,b3,b4,b5,b6,b7,b8];
function showBoard(posString) {
var i = 9
while (i--) { displayTable[i].textContent = posString[i] }
}
function showStatus(str) { statusContainer.textContent = str }
function showInfo(str) { infoContainer.textContent = str }
addEventListener("click",(e) => {
if(game.humanFoe) {
if(e.target.id[0] === "b") {
game.humanPlays = 1 << (8-Number(e.target.id[1]));
}
} else {
toPlay.textContent = "Man V Machine";
game.state = game.states.start;
game.humanPlaying = true;
}
});
code {
font-size : 20px;
text-align: center;
}
#toPlay {
position: absolute;
bottom: 0px;
font-size : 12px;
}
table {
position: absolute;
left: 42%;
top: 60px;
border-spacing: 0px;
font-size : 28px;
background : #eef;
}
td {
padding : 0px 8px 0px 8px;
cursor: pointer;
}
#infoContainer {
font-size : 12px;
}
.allB {
border : 1px solid black;
}
.topB {
border-top : 0px;
}
.botB {
border-bottom : 0px;
}
.leftB {
border-left : 0px;
}
.rightB {
border-right : 0px;
}
<code>
<div id="statusContainer"></div>
<table>
<tr><td id="b0" class="allB topB leftB">-</td><td id="b1" class="allB topB">-</td><td id="b2" class="allB topB rightB">-</td></tr>
<tr><td id="b3" class="allB leftB">-</td><td id="b4" class="allB">-</td><td id="b5" class="allB rightB">-</td></tr>
<tr><td id="b6" class="allB leftB botB">-</td><td id="b7" class="allB botB">-</td><td id="b8" class="allB rightB botB">-</td></tr>
</table>
<div id="infoContainer"></div>
<div id="toPlay">Click board to play!</div>
</code>