I am trying to implement minimax with alpha-beta pruning for a checkers game in Java. My minimax algorithm works perfectly. My code runs with the alpha-beta code in place. Unfortunately, when I play 1000 games vs the standard minimax algorithm, the alpha-beta algorithm always comes out behind by 50 games or so.
Since alpha-beta pruning should not be reducing the quality of the moves, just the time it takes to achieve them, something has to be wrong. However, I have taken out pen and paper and drawn hypothetical leaf node values and used my algorithm to predict whether it will calculate the correct best move, and there doesn't appear to be any logic errors. I used the tree from this video: Alpha-Beta Pruning to trace my algorithm. It logically should make all of the same choices, and therefore be a functioning implementation.
I have also put print statements into the code (they have been removed to reduce the clutter), and values are being returned correctly it appears and pruning does happen. Despite my best efforts I have been unable to find where the logic error lies. This is my third different attempt at implementing this and all of them have had the same issue.
I can't post the full code here, it's much too long, so I have included the methods that are relevant to the error. I'm not certain, but I suspect the problem may likely be in the non-recursive move() method, though I can't find a logical error in it so I'd just be thrashing around in it more, probably making things worse rather than better without having a rhyme or reason.
Is there a trick to recovering multiple integer values from recursive calls in a for loop? It works fine with both my minimax and negamax implementations, but alpha-beta pruning seems to produce some strange results.
@Override
public GameState move(GameState state)
{
int alpha = -INFINITY;
int beta = INFINITY;
int bestScore = -Integer.MAX_VALUE;
GameTreeNode gameTreeRoot = new GameTreeNode(state);
GameState bestMove = null;
for(GameTreeNode child: gameTreeRoot.getChildren())
{
if(bestMove == null)
{
bestMove = child.getState();
}
alpha = Math.max(alpha, miniMax(child, plyDepth - 1, alpha, beta));
if(alpha > bestScore)
{
bestMove = child.getState();
bestScore = alpha;
}
}
return bestMove;
}
private int miniMax(GameTreeNode currentNode, int depth, int alpha, int beta)
{
if(depth <= 0 || terminalNode(currentNode.getState()))
{
return getHeuristic(currentNode.getState());
}
if(currentNode.getState().getCurrentPlayer().equals(selfColor))
{
for(GameTreeNode child: currentNode.getChildren())
{
alpha = Math.max(alpha, miniMax(child, depth - 1, alpha, beta));
if(alpha >= beta)
{
return beta;
}
}
return alpha;
}
else
{
for(GameTreeNode child: currentNode.getChildren())
{
beta = Math.min(beta, miniMax(child, depth - 1, alpha, beta));
if(alpha >= beta)
{
return alpha;
}
}
return beta;
}
}
//Checks to see if the node is terminal
private boolean terminalNode(GameState state)
{
if(state.getStatus().equals(win) || state.getStatus().equals(lose) || state.getStatus().equals(draw))
{
return true;
}
else
{
return false;
}
}
The Alpha-beta pruning to a standard minimax algorithm returns the same move as the standard algorithm does, but it removes all the nodes which are not really affecting the final decision but making algorithm slow. Hence by pruning these nodes, it makes the algorithm fast.
Both algorithms should give the same answer. However, their main difference is that alpha-beta does not explore all paths, like minimax does, but prunes those that are guaranteed not to be an optimal state for the current player, that is max or min. So, alpha-beta is a better implementation of minimax.
Two-player Games The standard algorithm for these problems is minimax searchwith static evaluation, described below, and alpha-beta pruning, a technique that makes minimax search much more efficient. The states of the problem are the legal board positions, and the operators are the legal moves of the game.
I noticed you said you found the problem but shouldnt the minimax alpha beta pruning be
if it is MAX's turn to move
for child in children
result = alphaBetaMinimax(child, alpha, beta)
if result > alpha
alpha = result
if node is root
bestMove = operator of child
if alpha >= beta
return alpha
return alpha
if it is MIN's turn to move
for child in children
result = alphaBetaMinimax(child, alpha, beta)
if result < beta
beta = result
if node is root
bestMove = operator of child
if beta <= alpha
return beta
return beta
you wrote:
if alpha >= beta
return beta
return alpha
On March 16, 2013, sage88 asked:
Is there a trick to recovering multiple integer values from recursive calls in a for loop? It works fine with both my minimax and negamax implementations, but alpha-beta pruning seems to produce some strange results.
In alpha beta pruning, the only output value of interest is a node's score: the final value of beta in a min node is considered for the alpha value of its parent max node; likewise, the final value of alpha in a max node is considered for the beta value of its parent min node. Therefore:
The answer to your question is the algorithm itself, as it's the most relevant trick.
That said, there are two errors in your implementation: 1) As Adrian Blackburn originally pointed out, it's incorrectly returning alpha from a min node and vice-versa, thereby skewing its accuracy; 2) It's giving up pruning opportunities by prematurely considering the parent alpha or beta in the current node's value. This version fixes the return values and maximizes pruning:
private int miniMax(GameTreeNode currentNode, int depth, int alpha, int beta) {
if (depth <= 0 || terminalNode(currentNode.getState())) {
return getHeuristic(currentNode.getState());
}
if (currentNode.getState().getCurrentPlayer().equals(selfColor)) {
int currentAlpha = -INFINITY;
for (GameTreeNode child : currentNode.getChildren()) {
currentAlpha = Math.max(currentAlpha, miniMax(child, depth - 1, alpha, beta));
alpha = Math.max(alpha, currentAlpha);
if (alpha >= beta) {
return alpha;
}
}
return currentAlpha;
}
int currentBeta = INFINITY;
for (GameTreeNode child : currentNode.getChildren()) {
currentBeta = Math.min(currentBeta, miniMax(child, depth - 1, alpha, beta));
beta = Math.min(beta, currentBeta);
if (beta <= alpha) {
return beta;
}
}
return currentBeta;
}
Thanks for contributing a fun and interesting question :)
For more fun, here's a clarification of your move()
method, removing a redundant call to Math.max()
:
@Override
public GameState move(GameState state) {
GameState bestMove = null;
int bestScore = -INFINITY;
GameTreeNode gameTreeRoot = new GameTreeNode(state);
for (GameTreeNode child : gameTreeRoot.getChildren()) {
int alpha = miniMax(child, plyDepth - 1, bestScore, INFINITY);
if (alpha > bestScore || bestMove == null) {
bestMove = child.getState();
bestScore = alpha;
}
}
return bestMove;
}
Finally (even more fun), just a suggestion, a method name change to clarify the intent of terminalNode()
, though I would move this into GameState
so it could be called with no parameters:
private boolean isTerminal(GameState state) {
//return Is.any(state.getStatus(), win, lose, draw);
return state.getStatus().equals(win)
|| state.getStatus().equals(lose)
|| state.getStatus().equals(draw);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With