Writing a bot for a game as simple as tic-tac-toe is not that hard. As there are only 255,168 possible games, you can easily simulate all of them to search for good moves.
So the first thing the bot does is generating all possible game endings based on the current situation. The resulting situations can be stored in a so called Game Tree:
To generate the game tree, we will use a RecursiveAction to speed things up:
classSimulationTreeNodeextendsRecursiveAction{privatestaticintFORK_THRESHOLD=6;SimulationTreeNode[]possibleMoves;privateGameGridgameGrid;/** * Last position set that let to the situation now */privatebytechosenPosition;/** * Player who should act now */privateFieldactivePlayer;protectedbooleanisComputersTurn(){returnactivePlayer==playersField;}@Overrideprotectedvoidcompute(){if(gameGrid.getWinner()!=null||gameGrid.countFreeFields()==0){return;}intfreeFields=gameGrid.countFreeFields();possibleMoves=newSimulationTreeNode[freeFields];try{intcounter=0;for(bytei=0;i<9;++i){if(gameGrid.getField(i)==Field.EMPTY){SimulationTreeNodechild=newSimulationTreeNode(gameGrid,i,otherPlayer(activePlayer));possibleMoves[counter++]=child;if(freeFields<FORK_THRESHOLD){child.compute();}}}if(freeFields>=FORK_THRESHOLD){invokeAll(Arrays.asList(possibleMoves));}}catch(EngineExceptione){e.printStackTrace();}}}
Each SimulationTreeNode starts a new thread to generate all possible moves. If less then 6 free fields are left, no new threads are started (wouldn’t improve performance anymore). The generated sub-situations are stored in an array.
Now when it is the computers turn, it can build the game tree or search for the new situation in the existing game tree (after players move):
You may noticed the getBestMove call to the simulation tree. This method searches the best possible move. Often there are multiple moves with the same “score”. A random one is returned in that situation:
What does this “score” mean? The higher, the better: A defeat would be bad for us, so it has score -1. A draw would be OK, so it has score 0. Winning would be good, so it has score 1:
To calculate the score of situations which are not yet completed (no winner and still empty fields), we use the Minimax algorithm.
When it is the other players turn, we assume that he will try to do the best he can. So we must assume the lowest score of all possible moves for the situation. Because when we end up with that situation, the player could do this move that would hurt us.
When it is the computers turn, it will do the best it can. That’s why we can assume the highes score of all possible moves. Because when we end up with that situation, we may have a chance to do the move with that good score.
We end up with something like this:
SimulatingBot$SimulationTreeNode
123456789101112131415161718192021222324252627
privatebytecalculateScore(){if(gameGrid.getWinner()==null){if(gameGrid.countFreeFields()==0){return0;// draw}else{bytescore=(byte)(isComputersTurn()?-1:1);for(SimulationTreeNodechild:possibleMoves){if(isComputersTurn()){// computer will move, so search for good choiceif(child.getScore()>score){score=child.getScore();}}else{// other player will move and tries to hurt us!if(child.getScore()<score){score=child.getScore();}}}returnscore;}}elseif(gameGrid.getWinner()==playersField){return1;// win}else{return-1;// defeated}}