I would like to suggest that you completely re-factor your code. The problem with looking at your code (and why there haven't been many responses here) is that it's hard to follow and hard to debug. For instance, what is gs.getRemainingLines
and what does it do exactly? (Why remaining lines and not all legal lines?)
But, with some simplifications it will be much easier to figure out what is going on and to fix it.
At an abstract level minimax is just this procedure:
float minimax_max(GameState g)
{
if (g is terminal or max depth reached)
return eval(g);
float bestVal = -inf;
bestMove = null;
moves = g->getLegalMoves();
for (m : moves)
{
ApplyMove(m);
if (g->nextPlayer == maxPlayer)
nextVal = minimax_max(g);
else
nextVal = minimax_min(g);
if (nextVal > bestVal)
{
bestVal = nextVal;
bestMove = m;
}
UndoMove(m);
}
return bestVal;
}
I haven't shown exactly how to get/use the last move at the end, but it isn't that hard. You also need another procedure for minimax_min
, or you can put an if statement into the code.
If you look at your code, you've written it close to this, but you've left a lot of game specific details in the code. But, you shouldn't have to think about those things to get minimax working correctly.
In particular, most games can be reasoned with abstractly if you provide functions for GetMoves()
, ApplyMove()
, UndoMove()
, and eval()
, which evaluates a state. (Further search enhancements would require more functions, but this will get you a long ways.)
Some reasons why you might want to re-factor in this way:
You can now test minimax and your other code separately.
You can test your dots and boxes code by validating that all legal moves are generated and that after applying a move you have a legal state with the correct player moving next. (You can play and undo long sequences of random moves to help validate that you always end up back in the start state afterwards.)
You can test your evaluation function easily on individual states to make sure it works properly. (In practice you can't usually search to the end of the game to determine the winner.)
You can test minimax by using a simple evaluation function and testing to see if the right moves are made. (e.g. if you prefer moves on the edges, a 1-ply search should return a move on the edge)
Other people can read your code more easily. We can look at each piece of code and see if it is correct on its own, instead of having to mix the game-specific implementation details into the minimax-specific details.
If you can apply and undo moves properly, you don't need to make copies of the game states. This will make the code much more efficient.
While you could try to fix your code without refactoring (e.g. just find the first place it returns null, and that will point out where your error is), in the long term your code will be hard to debug and improve without these changes.