I made a Tic Tac Toe game of two types in Javascript. One is 3x3, and the other one is 10x10.
I am using Minimax algorithm with Alpha Beta Pruning to solve both the games. In 3x3, where the game tree is really small, the algorithm works fine.
But in 10x10, It takes too much time. The code cannot even make a single move in 10 mintues. I ran the algorithm, wait for 10 minutes, still, it was calculating, then I just closed the browser tab. (It might take even hours,days,weeks if I have let the code run lol)
I read in a few articles, that Minimax with Alpha Beta Pruning, can solve Tic Tac Toe 10x10 or bigger, easily. Is it false, or My code is bad?
Here is my code, but I think, it will be hard to undestand it. But code does not matter, I guess. I applied Minimax + Alpha Beta Pruning. What else can I do?
function makeBotMove(newBoard, availMoves, XorO, firstCall) { // newBoard stores board state in an array. availMoves stores Available moves in an array (0-99). XorO store either "X" or "O" depending on whoes turn it is. firstCall is used to find out If the call is made inside the function or not. I need it for Alpha Beta Pruning. It helps in storing the length of the total available moves when the call was made for
if (firstCall)
{
var originalAvailMovesLength = availMoves.length;
if (originalAvailMovesLength == board.length)
var maxPossibleResult = 0.5; // OriginalAvailMoves will be only 100, if it is the first move. And if it is first move, it is impossible to get reward of 1. The best the computer can do is, draw (0.5 reward).
else
var maxPossibleResult = 1;
}
availMoves = getAvailableMoves(newBoard);
var result = checkResult(newBoard, false); // It can return 4 values. 1 = Win, 0.5 = Draw, 0 = Game is on, -1 = Lose.
if (result != 0)
return [result];
var movesIndex = [];
var movesScore = [];
for (var i = 0; i < availMoves.length; i++)
{
var move = availMoves[i];
newBoard[move] = XorO;
availMoves.splice(availMoves.indexOf(Number(move)),1);
if (XorO == "O") // 1.) Yes
var reward = makeBotMove(newBoard, availMoves, "X", false);
else
var reward = makeBotMove(newBoard, availMoves, "O", false);
newBoard[move] = "-";
availMoves.push(move);
availMoves.sort();
movesIndex.push(move);
movesScore.push(reward[0]);
var bestMove = [];
if (originalAvailMovesLength == availMoves.length && Math.max(...movesScore) == maxPossibleResult)
{
bestMove[0] = Math.max(...movesScore);
bestMove[1] = movesScore.indexOf(bestMove[0]);
bestMove[1] = movesIndex[bestMove[1]];
return bestMove;
}
}
if (XorO == "O")
bestMove[0] = Math.max(...movesScore);
else
bestMove[0] = Math.min(...movesScore);
bestMove[1] = movesScore.indexOf(bestMove[0]);
bestMove[1] = movesIndex[bestMove[1]];
return bestMove;
}
If minimax algorithem, cannot do the job. Which algorithm do you guys recommend? It must not be very complex, i am not that good coder till now.
Edit: In 10x10, player need to place 5 moves in a row to win instead of 3.
Your code shows that you continue to do recursive calls until you have a win/loss or the board is full. As making 5-in-a-row is not trivial in a game between experts, this search would potentially have to visit most of the drawing positions, which I estimate would amount to some 10100 positions on a 10x10 board, given that 100! is almost 10158 (but we need to subtract from those all wins and losses). Anyway, such a number of boards is not realistic to search in, as the number of atoms in the visible universe is less than that. So don't wait for your code to finish. It will not in your lifetime.
There are two generic ways you can reduce the time spent on calculating a good move:
For the first action you could define a hard-coded maximum depth of your recursive search. If you arrive at that depth and the game is not over, then call an evaluation function that should give a score to the current board, without playing more moves. So it should look at some easy patterns, like a 3-in-a-row, and let those contribute to a final score. This a heuristic, meaning it is a (hopefully) good guess: the value should be somewhere between the two extremes of winning and losing.
For the second action you should limit the number of moves you will investigate further. Candidate moves to leave unvisited are moves that are relatively far away from the already played squares.
Furthermore, you could make a hashtable (new after each really played move), that stores boards that you have already evaluated, so you don't do that work again in case you arrive there through exchange of moves of one player in your search tree. Make sure that the hashtable also catches mirrored or turned boards, which will give a reduction in first couple of moves of the game.
There are many other techniques, like keeping track of "killer" moves during the search. If in one branch of the search tree it turns out that there is a move that can bring a win or avoid a loss, then try this move first in alternative branches as well. It may lead to quick pruning by the alpha-beta mechanism. In more general terms it is important to visit your moves in descending order of "quality". Of course, you don't know how good a move is until you analyse it, but again, there are some static things you can note about moves. A move in the corner of the board is certainly not as good as one in the centre, ...etc.
Some variants of searches first do a 1-depth search and use the result to sort the moves by that evaluation result. Then a 2-depth search is done, and again the moves are sorted by that (more accurate) result, ...etc, until the final depth is reached. This may look like a lot of work, but the alpha-beta pruning will give the most benefit when moves are ordered optimally, and that will be a more determining factor for the overall efficiency.