I've written a web-based Tic Tac Toe game and implemented the Minimax algorithm for the AI to choose its moves. Everything seems to be working, but there's a long delay between when the human player clicks on the first square and when the player's token (X or O) appears. It seems like it waits for all the recursions of the minimax
function to complete before updating the board even though I'm calling the updateBoard
function before the first call to minimax
.
This seems like a problem with asynchronous operations, meaning it doesn't have time to update the HTML before it's blocked by the recursive calls to minimax
. I tried adding a callback argument to the updateBoard
function and executing minimax
when updateBoard
completed, but I didn't notice any difference.
I'm not sure this problem is actually related to my Minimax implementation. That seems to work properly... The computer never loses. I think it's probably something I'm misunderstanding about synchronous operations in Javascript.
Can someone take a look, particularly in the handler for $squares.click()
and tell me if you see what's wrong?
You can look at it on Codepen and I've reproduced it below: http://codepen.io/VAggrippino/pen/ZBqLxO
Update, seconds later:
That's weird. When I "Run Code Snippet" here on SO, it doesn't present the same problem. Have I stumbled upon a problem with CodePen?
$(function(){
var humanToken = 'X';
var computerToken = 'O';
var gameFinished = false;
var $squares = $(".square");
/*
$squares.each(function() {
var id = $(this).attr("id");
$(this).html("<span style='font-size: 1rem;'>" + id + "</style>");
});
*/
var board = [
[null,null,null],
[null,null,null],
[null,null,null],
];
// Give the human player a choice between "X" and "O".
var chooseToken = function(e) {
var $button = $(e.target);
humanToken = $button.html();
computerToken = (humanToken === 'X' ? 'O' : 'X');
$(this).dialog("close");
gameFinished = false;
};
function reset(){
$squares.html("");
board = [
[null,null,null],
[null,null,null],
[null,null,null],
];
$("#tokenDialog").dialog("open");
}
function checkWinner(board) {
var allFull = true;
var tokens = [humanToken, computerToken];
for (var t in tokens) {
var diagonalWin1 = true;
var diagonalWin2 = true;
var token = tokens[t];
/* Since the squares are associated with a two-
dimensional array, these coordinates shouldn't be
thought of as x/y coordinates like a grid.
*/
for (var i = 0; i < 3; i++) {
// Checking 0,0; 1,1; 2,2... top left to bottom right
if (board[i][i] !== token) {
diagonalWin1 = false;
}
// Checking 2,0; 1,1; 0,2... bottom left to top right
if (board[2-i][i] !== token) {
diagonalWin2 = false;
}
var verticalWin = true;
var horizontalWin = true;
for (var j = 0; j < 3; j++) {
/* Checking:
0,0; 0,1; 0,2... horizontal top
1,0; 1,1; 1,2... horizontal middle
2,0; 2,1; 2,2... horizontal bottom
*/
if (board[i][j] !== token) {
horizontalWin = false;
}
/* Checking:
0,0; 1,0; 2,0... vertical left
0,1; 1,1; 2,1... vertical middle
0,2; 1,2; 2,2... vertical right
*/
if (board[j][i] !== token) {
verticalWin = false;
}
// If there are any empty squares, set allFull to
// false, indicating a tie.
if (board[i][j] === null) {
allFull = false;
}
}
if (horizontalWin || verticalWin) {
return token;
}
}
if (diagonalWin1 || diagonalWin2) {
return token;
}
}
// If all the squares are full and we didn't find a
// winner, it's a tie. Return -1.
if (allFull) return -1;
// No winner yet.
return null;
}
function updateBoard() {
// The layout of the board represents the layout of a
// Javascript array. So, these should thought of as row
// and column instead of x/y coordinates of a grid.
for (var r = 0; r < 3; r++) {
for (var c = 0; c < 3; c++) {
var squareId = "#s" + r + '' + c;
$(squareId).html(board[r][c]);
}
}
var result = "";
var winner = checkWinner(board);
if (winner !== null) {
if (winner === humanToken) {
result = "You Win!";
} else if (winner === computerToken) {
result = "The Computer Wins!";
} else if (winner === -1) {
result = "It's a tie!";
}
var $resultDialog = $("#resultDialog");
$resultDialog.dialog("option", "title", result);
$resultDialog.dialog("open");
}
}
$("#reset").click(reset);
$squares.click(function(){
// If the game is already finished, we're just looking at
// the results, so don't do anything.
if (gameFinished) return false;
var $square = $(this);
var boardPosition = $square.attr("id").match(/(\d)(\d)/);
var row = boardPosition[1];
var col = boardPosition[2];
var currentValue = board[row][col];
// If there's not already a token in the clicked square,
// place one and check for a winner.
if (currentValue === null) {
board[row][col] = humanToken;
updateBoard();
board = minimax(board, computerToken)[1];
updateBoard();
}
});
function minimax(board, token) {
// Check the current layout of the board for a winner.
var winner = checkWinner(board);
// The computer wins.
if (winner === computerToken) {
return [10, board];
// The human wins.
} else if (winner === humanToken) {
return [-10, board];
// It's a tie
} else if (winner === -1) {
return [0, board];
// There's no winner yet
} else if (winner === null) {
var nextScore = null;
var nextBoard = null;
// Add a token to the board and check it with a
// recursive call to minimax.
for (var r = 0; r < 3; r++) {
for (var c = 0; c < 3; c++) {
if (board[r][c] === null) {
// Play the current players token, then call
// minimax for the other player.
board[r][c] = token;
var score;
if (token === humanToken) {
score = minimax(board, computerToken)[0];
} else {
score = minimax(board, humanToken)[0];
}
/* This is the computer player trying to win.
If the current player is the computer and the
score is positive or the current player is
human and the score is negative assign a copy
of the board layout as the next board.
*/
if ((token === computerToken && (nextScore === null || score > nextScore)) ||
(token === humanToken && (nextScore === null || score < nextScore)) )
{
nextBoard = board.map(function(arr) {
return arr.slice();
});
nextScore = score;
}
board[r][c] = null;
}
}
}
return [nextScore, nextBoard];
} else {
console.log("Something bad happened.");
console.log("winner: ");
console.log(winner);
}
}
$("#tokenDialog").dialog({
"title": "Choose Your Weapon",
"position": {
"my": "center",
"at": "center",
"of": "#board"
},
"autoOpen": false,
"buttons": {
"X": chooseToken,
"O": chooseToken,
},
"closeOnEscape": false,
"draggable": false,
"modal": true,
"resizable": false,
"show": true,
"classes": {
"ui-dialog-buttonpane": "tokenDialog-buttonpane",
},
});
$("#resultDialog").dialog({
"position": {
"my": "center",
"at": "center",
"of": "#board"
},
"autoOpen": false,
"closeOnEscape": false,
"draggable": false,
"modal": true,
"resizable": false,
"show": true,
"buttons": {
"Play Again": function(){
$(this).dialog("close");
reset();
}
},
"classes": {
}
});
});
body, html {
height: 100%;
margin: 0;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: linear-gradient(45deg, #dca 12%, transparent 0, transparent 88%, #dca 0),
linear-gradient(135deg, transparent 37%, #a85 0, #a85 63%, transparent 0),
linear-gradient(45deg, transparent 37%, #dca 0, #dca 63%, transparent 0) #753;
background-size: 25px 25px;
}
div#board {
background-color: white;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/29229/gimp_pattern_marble1.png);
background-repeat: repeat;
width: 21rem;
padding: 0.25rem;
margin: 1rem;
border: 10px solid transparent;
border-radius: 2rem;
position: relative;
background-clip: content-box;
box-shadow: 0px 5px 10px 3px rgba(0, 0, 0, 0.75);
}
div#board::after {
content: "";
z-index: -1;
position: absolute;
top: -10px;
right: -10px;
bottom: -10px;
left: -10px;
border-radius: 2rem;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/29229/gimp_pattern_leather.png);
}
#reset {
font-size: 2em;
border-radius: 0.5rem;
}
div.square {
width: 7rem;
height: 7rem;
font-size: 7rem;
font-family: sans-serif;
float: left;
border-width: 0 2px 2px 0;
border-color: black;
border-style: solid;
box-sizing: border-box;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
#s02, #s12, #s22 {
border-right: none;
}
#s20, #s21, #s22 {
border-bottom: none;
}
.tokenDialog-buttonset {
width: 100%;
display: flex;
justify-content: center;
}
.ui-dialog-buttonpane.tokenDialog-buttonpane {
padding: 0.5em;
}
.no-close .ui-dialog-titlebar-close {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/ui-darkness/jquery-ui.css">
<!--
The board configuration represents the layout of a Javascript
array, not a typical grid. so, the square IDs should not be
thought of as x/y coordinates.
-->
<div id="board">
<div class="row">
<div class="square" id="s00"></div>
<div class="square" id="s01"></div>
<div class="square" id="s02"></div>
</div>
<div class="row">
<div class="square" id="s10"></div>
<div class="square" id="s11"></div>
<div class="square" id="s12"></div>
</div>
<div class="row">
<div class="square" id="s20"></div>
<div class="square" id="s21"></div>
<div class="square" id="s22"></div>
</div>
</div>
<div id="buttons">
<button id="reset">Reset Game</button>
</div>
<div id="tokenDialog"></div>
<div id="resultDialog"></div>