Reputation: 61
The algorithm is running fine without any errors but the AI is not smart at all and seems to be making random moves. I've been working on this for two days and can't figure out where I went wrong. Can someone please help to find the bug thats causing it to not make the right move?
When starting the game, the AI should always make the move at index 4 (middle square) unless I take that spot, but it doesn't do that and also it doesn't try to win at all.
$(document).ready(function() {
let X = {
isComputer: false,
symbol: "x",
marker: "<img src='img/x.png'>",
winMarker: "<img src='img/xWin.png'>"
}
let O = {
isComputer: false,
symbol: "o",
marker: "<img src='img/o.png'>",
winMarker: "<img src='img/oWin.png'>"
}
let game = {
board: [0,1,2,3,4,5,6,7,8],
firstTurn: X,
xScore: 0,
oScore: 0,
turnNumber: 0,
started: false
}
let winningCombos = [
[0,1,2], [3,4,5], [6,7,8],
[0,3,6], [1,4,7], [2,5,8],
[0,4,8], [2,4,6]
];
let theWinningCombo;
let player = X;
let clearBoardTimeoutID;
let ai1;
let ai2;
function clearBoardForNextGame() {
clearBoardTimeoutID =
setTimeout(function() {
$('.square').empty();
game.firstTurn = game.firstTurn == X ? O : X;
game.turnNumber = 0;
game.board = [0,1,2,3,4,5,6,7,8];
game.started = true;
}, 1500);
}
function thisPlayerWon(board, symbol) {
for (let i = 0; i < winningCombos.length; i++) {
let counter = 0;
for (let j = 0; j < winningCombos[i].length; j++) {
if (board[winningCombos[i][j]] == symbol) {
counter++;
}
if (counter == 3) {
theWinningCombo = winningCombos[i];
return true;
}
}
}
return false;
}
function showWinnerAndUpdateScore(combo, player) {
game.started = false;
combo.forEach(index => $('#' + index).html(player.winMarker));
player == X ? (game.xScore++, $('#score1').text(game.xScore)) : (game.oScore++, $('#score2').text(game.oScore))
}
function AImove(AIplayer, board) {
AIplayer = !(game.turnNumber % 2) ? game.firstTurn : (game.firstTurn == X ? O : X);
let opponent = AIplayer == X ? O : X;
let bestMove = minimax(AIplayer, board, 0);
board[bestMove] = AIplayer.symbol;
$('#' + bestMove).html(AIplayer.marker);
game.turnNumber++;
function minimax(player, board, depth) {
let spotsNotMarked = emptyBoardSpots(board);
if (thisPlayerWon(board, AIplayer.symbol)) {
return 10-depth;
}
else if (thisPlayerWon(board, opponent.symbol)) {
return depth-10;
}
else if (spotsNotMarked.length == 0) {
return 0;
}
let moves = [];
let scores = [];
for (let i = 0; i < spotsNotMarked.length; i++) {
let index = spotsNotMarked[i];
let score;
board[index] = player.symbol;
if (player == X) {
score = minimax(O, board, depth+1);
}
else {
score = minimax(X, board, depth+1);
}
scores.push(score);
board[index] = index;
moves.push(index);
}
if (player == AIplayer) {
return moves[scores.indexOf(Math.max(...scores))];
}
else {
return moves[scores.indexOf(Math.min(...scores))];
}
}
}
function emptyBoardSpots(board) {
return board.filter(square => !isNaN(square));
}
$('.AI-Switch').on('change', function() {
if (!game.started) {
this.id == "one" ? (X.isComputer = !(X.isComputer), ai1 = X) : (O.isComputer = !(O.isComputer), ai2 = O);
}
});
$('#resetButton').on('click', function() {
clearTimeout(clearBoardTimeoutID);
$('.square').empty();
$('.scores').text("0");
game.board = [0,1,2,3,4,5,6,7,8];
game.firstTurn = X;
game.xScore = 0;
game.oScore = 0;
game.turnNumber = 0;
game.started = false;
});
$('#startButton').on('click', function() {
game.started = true;
});
$('.square').on('click', function() {
if (game.started && !isNaN(game.board[this.id])) {
player = !(game.turnNumber % 2) ? game.firstTurn : (game.firstTurn == X ? O : X);
this.innerHTML = player.marker;
game.board[this.id] = player.symbol;
game.turnNumber++;
if (game.turnNumber > 3 && thisPlayerWon(game.board, player.symbol)) {
showWinnerAndUpdateScore(theWinningCombo, player);
clearBoardForNextGame();
}
else if (game.turnNumber == 9) {
clearBoardForNextGame();
}
if (O.isComputer && player == X) {
AImove(player, game.board);
}
else if (X.isComputer && player == O) {
AImove(player, game.board);
}
}
});
});
Upvotes: 0
Views: 291
Reputation: 350137
The issue is the return value of minimax
: is it a score or is it a move?
You reason that the recursive call should return a score, except for the first call when it should return a move. This would indeed be handy, but it is not what is happening:
This means that half way down the recursion tree (not yet at a leaf), you are also getting moves back, ... but you treat them as scores one level up inside the recursion tree. Obviously that makes any result meaningless.
Let your minimax
function return a score, but also a move (when relevant) at the same time. This you can do by returning an object with the two pieces of information.
Here is your AImove
function with just a few modifications to implement that idea. The modified lines are marked with ***
:
function AImove(AIplayer, board) {
AIplayer = !(game.turnNumber % 2) ? game.firstTurn : (game.firstTurn == X ? O : X);
let opponent = AIplayer == X ? O : X;
let bestMove = minimax(AIplayer, board, 0).move; // *** Get move part of return value
board[bestMove] = AIplayer.symbol;
$('#' + bestMove).html(AIplayer.marker);
game.turnNumber++;
function minimax(player, board, depth) {
if (depth===2) console.log('step');
let spotsNotMarked = emptyBoardSpots(board);
if (thisPlayerWon(board, AIplayer.symbol)) {
return { score: 10-depth }; // *** Object with score (there's no move)
}
else if (thisPlayerWon(board, opponent.symbol)) {
return { score: depth-10 }; // *** idem
}
else if (spotsNotMarked.length == 0) {
return { score: 0 }; // *** idem
}
let moves = [];
let scores = [];
for (let i = 0; i < spotsNotMarked.length; i++) {
let index = spotsNotMarked[i];
let score;
board[index] = player.symbol;
if (player == X) {
score = minimax(O, board, depth+1).score; // *** Get score part
}
else {
score = minimax(X, board, depth+1).score; // *** idem
}
scores.push(score);
board[index] = index;
moves.push(index);
}
let score = (player == AIplayer ? Math.max : Math.min)(...scores); // *** Get score
return { score, move: moves[scores.indexOf(score)] }; // *** Return both move & score
}
}
Upvotes: 2