1

I have some javascript code while trying to code a Tic tac toe game.
So the AI (Artificial Intelligence) plays "X" and respectively "Human" player is "O";
For test I put a board as
['e', 'e', 'o',
'x',  'o', 'e',
'e',  'e', 'e']

It is AI turn to move. So obviously the best move for AI (Artificial Intelligence) is
['e', 'e', 'o',
'x',  'o', 'e',
'x',  'e', 'e'].

But it returns me
['x', 'e', 'o',
'x',  'o', 'e',
'e',  'e', 'e']
variant.
I would be appreciated, for good hints, that would redirect me in the right way.
And yes I've read for a week a bunch of articles about Minimax. Personally I used as a prototype this tutorial http://blog.circuitsofimagination.com/2014/06/29/MiniMax-and-Tic-Tac-Toe.html.
So please have a look at my code:

var board = ['e', 'e', 'o', 'x', 'o', 'e', 'e', 'e', 'e'];
var signPlayer = 'o';
var signAI = (signPlayer === 'x') ? 'o' : 'x';

//Circuits Of Imagination

game = {
    over: function(board) {
        for (var i = 0; i < board.length; i += 3) {
            if (board[i] === board[i + 1] && board[i + 1] === board[i + 2]) {
                return board[i] !== 'e' ? board[i] : false;
            }
        }
        for (var j = 0; j < board.length; j++) {
            if (board[j] === board[j + 3] && board[j + 3] === board[j + 6]) {
                return board[j] !== 'e' ? board[j] : false;
            }
        }
        if ((board[4] === board[0] && board[4] === board[8]) || 
        (board[4] === board[2] && board[4] === board[6])) {
            return board[4] !== 'e' ? board[4] : false;
        }
        var element;
        if (board.every(function(element) {
            return element !== 'e';
        })) {
            return true;
        }
    },
    winner: function(board) {
        return game.over(board);
    },
    possible_moves: function(board, sign) {
        var testBoard = [], 
        nextBoard;
        for (var i = 0; i < board.length; i++) {
            nextBoard = board.slice();
            if (nextBoard[i] === 'e') {
                nextBoard[i] = sign;
                testBoard.push(nextBoard);
            }
        }
        return testBoard;
    }
}

function score(board) {
    if (game.winner(board) === signPlayer) {
        return -10;
    } else if (game.winner(board) === signAI) {
        return +10;
    } else {
        return 0;
        //Game is a draw
    }
}

function max(board) {

    if (game.over(board)) {
        return score(board);
    }
    var newGame = [];
    var best_score = -10;
    var movesArray = game.possible_moves(board, signAI);

    for (var i = 0; i < movesArray.length; i++) {
        newGame = movesArray[i].slice();
        score = min(newGame);
        if (score > best_score) {
            best_score = score;
        }
        console.log('maxnewGame', newGame);
        return best_score;
    }
}

function min(board) {

    if (game.over(board)) {
        return score(board);
    }
    var newGame = [];
    var worst_score = 10;
    var movesArray = game.possible_moves(board, signPlayer);

    for (var i = 0; i < movesArray.length; i++) {
        newGame = movesArray[i].slice();
        score = max(newGame);
        if (score < worst_score) {
            worst_score = score;
        }
        console.log('minnewGame', newGame);
        return worst_score;
    }
}
max(board);
Taras Yaremkiv
  • 3,440
  • 7
  • 32
  • 54
  • 1
    Okay, that's definitely code... Did you have a question? Does the code work but not in the way you want? Does the code not work? Is there an error in the console? – Heretic Monkey Sep 19 '16 at 20:29
  • 1
    See also [this implementation](https://stackoverflow.com/questions/64882717/solving-tictactoe-with-minimax-algorithm-in-javascript/65417503#65417503). – trincot Dec 23 '20 at 00:27

1 Answers1

2

There are a few bugs in your code:

  1. You had return best/worst_score inside the for loop, which terminated the search prematurely. Bring it out of the loop.
  2. score was redefined in the loop to a number, when it should be a function. Rename it to moveScore.
  3. max/minnewGame was not set correctly.
  4. best/worse_score was initiated to +/-10 which means that sometimes no max was found.

The fixed code is below. However note that X still does not pick the "obvious" move. This is actually working as intended for your minimax algorithm. This is due to the algorithm assuming that the opponent is playing optimally. In your given board state, an optimal opponent would make it impossible to win as X. Therefore, the algorithm "gives up" and picks the first possible move.

This is one of the biggest properties of a minmax algorithm: The algorithm can only make decisions that are as good as its opponent. If the opponent is simulated optimally, the algorithm would not consider the possibility of a mistake and give up. To possibly make the algorithm pick what is in your opinion, the obvious move, you would have to make the algorithm also take into account turns until loss, so it will try to lose as late in the game as possible (this can be done by giving losing moves -10+turns where turns is the number of turns it takes to lose). Another approach is to make the score based on possible game over states, and favour the move with more possible states where X wins.

var board = ['e', 'e', 'o', 'x', 'o', 'e', 'e', 'e', 'e'];
var signPlayer = 'o';
var signAI = (signPlayer === 'x') ? 'o' : 'x';

//Circuits Of Imagination

game = {
  over: function(board) {
    for (var i = 0; i < board.length; i += 3) {
      if (board[i] === board[i + 1] && board[i + 1] === board[i + 2]) {
        return board[i] !== 'e' ? board[i] : false;
      }
    }
    for (var j = 0; j < board.length; j++) {
      if (board[j] === board[j + 3] && board[j + 3] === board[j + 6]) {
        return board[j] !== 'e' ? board[j] : false;
      }
    }
    if ((board[4] === board[0] && board[4] === board[8]) ||
      (board[4] === board[2] && board[4] === board[6])) {
      return board[4] !== 'e' ? board[4] : false;
    }
    var element;
    if (board.every(function(element) {
      return element !== 'e';
    })) {
      return true;
    }
  },
  winner: function(board) {
    return game.over(board);
  },
  possible_moves: function(board, sign) {
    var testBoard = [],
      nextBoard;
    for (var i = 0; i < board.length; i++) {
      nextBoard = board.slice();
      if (nextBoard[i] === 'e') {
        nextBoard[i] = sign;
        testBoard.push(nextBoard);
      }
    }
    return testBoard;
  }
}

function score(board) {
  if (game.winner(board) === signPlayer) {
    return -10;
  } else if (game.winner(board) === signAI) {
    return +10;
  } else {
    return 0;
    //Game is a draw
  }
}

function max(board) {

  if (game.over(board)) {
    return score(board);
  }
  var newGame = [];
  var moveScore, maxnewGame;
  var best_score = -Infinity;
  var movesArray = game.possible_moves(board, signAI);

  for (var i = 0; i < movesArray.length; i++) {
    newGame = movesArray[i].slice();
    moveScore = min(newGame);
    if (moveScore > best_score) {
      best_score = moveScore;
      maxnewGame = newGame;
    }
  }
  console.log('maxnewGame', maxnewGame);
  return best_score;
}

function min(board) {

  if (game.over(board)) {
    return score(board);
  }
  var newGame = [];
  var moveScore, minnewGame;
  var worst_score = Infinity;
  var movesArray = game.possible_moves(board, signPlayer);

  for (var i = 0; i < movesArray.length; i++) {
    newGame = movesArray[i].slice();
    moveScore = max(newGame);
    if (moveScore < worst_score) {
      worst_score = moveScore;
      minnewGame = newGame;
    }
  }
  console.log('minnewGame', minnewGame);
  return worst_score;
}
console.log(max(board));
tcooc
  • 20,629
  • 3
  • 39
  • 57
  • Thank you for such a detail answer, but one more thing: How do I return a board with best move? – Taras Yaremkiv Sep 20 '16 at 07:28
  • @TarasYaremkiv Just return the board along with the score. For example, `return [worse_score, minnewGame]'`. Then when calling the function, do `result = min(board); moveScore = result[0]; moveBoard = result[1]`. – tcooc Sep 20 '16 at 14:11
  • It's defenitely something wrong with my algorithm ( implementation. In the snippet above, AI best move is always inserted in the very first empty cell. – Taras Yaremkiv Sep 20 '16 at 16:05
  • @TarasYaremkiv that is because it made the best move, defined as "the move with the highest chance of success, against a perfect opponent". – tcooc Sep 20 '16 at 16:19
  • Here is the sequence I got inputing board values one by one. in this code https://gist.github.com/Y-Taras/e2e7cf74a5b262d211a8ba25cf460d6b So in the end player wins and AI looses ['e', 'e', 'e', 'e', **'o',** 'e', 'e', 'e', 'e']; [**'x',** 'e', 'e', 'e', **'o',** 'e', 'e', 'e', 'e']; [**'x','o',** 'e', 'e', **'o',** 'e', 'e', 'e', 'e']; [**'x', 'o', 'x'**, 'e', **'o',** 'e', 'e', 'e', 'e']; [**'x', 'o', 'x',** 'e', **'o',** 'e', 'e', **'o',** 'e']; – Taras Yaremkiv Sep 20 '16 at 19:52
  • @TarasYaremkiv Try https://jsfiddle.net/9486vod0/1/. The AI seems to always force a draw, or win if possible. – tcooc Sep 20 '16 at 20:04
  • oh I can't believe but it works!) thanks again, now I 'm going to look what have you changed) – Taras Yaremkiv Sep 20 '16 at 20:16
  • @TarasYaremkiv Didn't change any logic, just fixed a bug with the `if (game.over(board)) {...}` returning the wrong value. Other than that, just a bit of refactor. – tcooc Sep 20 '16 at 20:38