I played around with your code and there is quite a bit to say
bug
first of all there is a bug. I don't think your code actually works for a 3x3 board. The problem is the location you revert
the move you add to the board. You do this at the end of the newminimax47
method exactly once, even though in the method you add moves to the board inside a for
loop. This means that calling the method does not only compute something but also changes the board state, and the rest of the code expects it not to.
So remove the revert
where it is now and in revert as soon as you can:
setX(x);
currentScore = newminimax47(x)[0];
revert(x);
this also implies you don't need the lastAdded
variable.
play
It's a lot easier to see what is happening if you actually play against your own algorithm. Add a method to your State class
public void dump() {
for (int y = 0; y < n; y++) {
for (int x = 0; x < n; x++) {
System.out.print(state[y * n + x]);
}
System.out.println();
}
}
and in you main something like
public void play() {
State s=new State(3);
Scanner in = new Scanner (System.in);
while (s.isGameOver().equals("Not Gameover")) {
int[] options = s.getAvailableMoves();
s.dump();
System.out.println ("Your options are " + Arrays.toString(options));
int move = in.nextInt();
s.setX(move);
int [] ScoreAndRecommendedMove=new int[2];
ScoreAndRecommendedMove=s.newminimax47(0);
System.out.println("Score: "+ScoreAndRecommendedMove[0]+" Position: "+ ScoreAndRecommendedMove[1]);
s.setO(ScoreAndRecommendedMove[1]);
}
s.dump();
}
and you can actually play against it. On a 3x3 board this works just fine for me. Unfortunately I estimated computing the first move on a 4x4 takes my computer approximately 48 hours.
data types
Your choice of data types is often a bit... strange. If you want to remember a single character, use char
instead of String
. If you want to return a yes/no decision, try use boolean
. There are also some parts of the program that could be replaces by less code doing the same. But that wasn't your question, so on to...
algorithm
Ok, so what's wrong with minimax to solve this problem?
Suppose the first four moves are X5, O8, X6 O7. Another possibility is to start the game with X5, O7, X6, O8. Yet another one is X6, O7, X5, O8. And finally there's X6, O8, X5, O7.
All four of those possibilities for the first four moves of the game lead to the exact same gamestate. But minimax will not recognise they're the same (basically there is no memory of parallel branches) so it will compute them all four. And the number of times each board state is computed will increase rapidly if you search deeper.
The number of possible games vastly outnumbers the number of possible board states. To estimate the number of games: at first there are 16 possible moves, then 15, then 14, 13, ... and so on. A rough estimation is 16!, although minimax won't have to compute all of them, because many of them will have finished before the 16th move.
An estimation for the number of game states is: every square on the board can be either empty, or an X, or an O. So that's 3^16 boards. Not all of them are actually valid boards, because the number of Xs on the board can be at most one more then the number of Os, but still it's close to 3^16.
16! possible games is about half a million times more then 3^16 possible board states. Which means we're approximately computing every board half a milion times in stead of just once.
The solution is to start remembering every gamestate you compute. Every time the recursive function is called first check if you already know the answer, and if so just return the old answer. It's a technique called memoization.
Memoization
I'll describe how to add memoization while using the data structures you already choose (even though I don't agree with them). To do memoization you need a collection on which you can do quick adds and quick lookups. A list (for example ArrayList
) won't do us any good. It's fast to add values but to do a lookup is very slow in long lists. There are some options but the easiest one to use is HashMap
. In order to use HashMap
you need to create something that represents your state and you can use as a key. The most straight-forward is to just make a String
with all the X/O/- symbols in it that represent your board.
So add
Map<String,int[]> oldAnswers = new HashMap<String,int[]>();
to your State
object.
Then at the start of your newminimax47
method create the String that represents the State and check if we already know the answer:
String stateString = "";
for (String field : state) stateString += field;
int[] oldAnswer = oldAnswers.get(stateString);
if (oldAnswer != null) return oldAnswer;
Finally when you compute a new answer the end of newminimax47
you should not only return it, but also store it in the map:
int[] answer = {bestScore, bestPos};
oldAnswers.put (stateString, answer);
return answer;
With memoization in place I was able to play a 4x4 game against your code. The first move is still slow (20 seconds) but after that everything is computed and it's very fast. If you want to speed it up further, you could look into alpha beta pruning. But the improvement won't be near as much as memoization. Another option is using more efficient data types. It won't reduce the theoretical order of your algorithm, but could still easily make it 5 times faster.