private SearchResult Negamax(int currentDepth, Grid state, int alpha, int beta, bool allowNullWindow = true) { int currentPlayer = currentDepth & 1; int originalAlpha = alpha; totalNodesSearched++; ulong validMovesMask = state.GetValidMovesMask(); ulong playerThreats = state.GetThreats(currentPlayer); // If the player can win on this move then return immediately, // no more evaluation is needed. ulong currentPlayerThreats = validMovesMask & playerThreats; if (currentPlayerThreats != 0) { endNodesSearched++; if (currentDepth == moveNumber) { finalMove = GetFirstColumnOfThreatBoard(currentPlayerThreats); } return new SearchResult(infinity, currentDepth); } ulong opponentThreats = state.GetThreats(1 - currentPlayer); // If the opponent could win in one move on this position, then the // current player must play that move instead. ulong currentOpponentThreats = validMovesMask & opponentThreats; if (currentOpponentThreats != 0) { int forcedMove = GetFirstColumnOfThreatBoard(currentOpponentThreats); if (currentDepth == moveNumber) { finalMove = forcedMove; } // Remove the forced move from the threat board. currentOpponentThreats &= ~(0x3FUL << (forcedMove * (state.Height + 1))); // If there is another threat return the loss immediately, otherwise // play the forced move. if (currentOpponentThreats != 0) { endNodesSearched++; return new SearchResult(-infinity, currentDepth + 1); } // Take the single forced move. state.Move(forcedMove, currentPlayer); SearchResult childResult = -Negamax(currentDepth + 1, state, -beta, -alpha, allowNullWindow); state.UndoMove(forcedMove, currentPlayer); return childResult; } // This must be a draw if there are no forced moves and only two // moves left in the game. if (currentDepth >= maxMoves - 1) { endNodesSearched++; return new SearchResult(0, currentDepth); } // Will be set to the best move of the lookup. int entryBestMove = -1; // Check if the position or the flipped position should be used for the lookup. ulong hash = Math.Min(state.Hash, state.FlippedHash); bool usingFlippedPosition = hash != state.Hash; // Check if this state has already been visited. ulong entry; if (transpositionTable.Lookup(hash, out entry)) { // If the state has been visited, then return immediately or // improve the alpha and beta values depending on the node type. tableLookups++; // The score is stored in bits 8 to 15 of the entry. int encoded = (int)(((entry >> 8) & 0xFF) - 128); SearchResult lookupResult = new SearchResult { score = encoded }; // The type is stored in bits 6 to 7 of the entry. int entryType = (int)((entry >> 6) & 0x3); // Flip the best move if necessary. entryBestMove = (int)((entry >> 16) & 0x7); if (usingFlippedPosition) { entryBestMove = 6 - entryBestMove; } switch (entryType) { // If the score is exact, there is no need to check anything // else, so return the score of the entry. case NodeTypeExact: if (currentDepth == moveNumber) { finalMove = entryBestMove; } return lookupResult; // If the entry score is an upper bound on the actual score, // see if the current upper bound can be reduced. case NodeTypeUpper: beta = Math.Min(beta, lookupResult.GetValue()); break; // If the entry score is a lower bound on the actual score, // see if the current lower bound can be increased. case NodeTypeLower: alpha = Math.Max(alpha, lookupResult.GetValue()); break; } // At this point alpha or beta may have been improved, so check if // this is a cuttoff. if (alpha >= beta) { alphaBetaCutoffs++; if (currentDepth == moveNumber) { finalMove = entryBestMove; } return lookupResult; } } // A bitmap where a 1 means the corresponding move has already been // checked. Initialised with a 1 at all invalid moves. ulong checkedMoves = state.GetInvalidMovesMask(); SearchResult result = new SearchResult { score = int.MinValue }; int bestMove = -1; int index = -1; bool isFirstChild = true; // If no parent of this position has tried a null-window search yet, // try guessing the score of this position. if (allowNullWindow) { int guess = (currentPlayer == 0) ? GuessScore(0, playerThreats, opponentThreats) : GuessScore(1, opponentThreats, playerThreats); // Check if there is a guess for this position. if (guess != 0) { int nullAlpha; int nullBeta; int expectedScore; // If this is likely to be a win, search this position with a // null window centered at +infinity. if (guess == 1) { nullAlpha = infinity; nullBeta = infinity + 1; expectedScore = infinity; } // If this is likely to be a loss. else { nullAlpha = -infinity - 1; nullBeta = -infinity; expectedScore = -infinity; } allowNullWindow = false; SearchResult nullResult = Negamax( currentDepth, state, nullAlpha, nullBeta, allowNullWindow); // If the guess is correct, return immediately. if (nullResult.GetValue() == expectedScore) { correctGuesses++; return nullResult; } incorrectGuesses++; } } // Find the best move recursively. // checkMoves will be equal to bottomRow when there are no more valid // moves. while (checkedMoves != Grid.bottomRow) { int move = GetNextMove(ref index, ref checkedMoves, entryBestMove, validMovesMask, playerThreats); if (isFirstChild) { bestMove = move; } // Apply the move and recurse. state.Move(move, currentPlayer); SearchResult childResult = -Negamax(currentDepth + 1, state, -beta, -alpha, allowNullWindow); state.UndoMove(move, currentPlayer); if (childResult.score > result.score) { result = childResult; bestMove = move; if (result.GetValue() > alpha) { alpha = result.GetValue(); // Check if this a cutoff. if (alpha >= beta) { betaCutoffs++; if (isFirstChild) { betaCutoffsOnFirstChild++; } if (index <= 0) { betaCutoffsOnOrderedChildren++; } break; } } } isFirstChild = false; } Debug.Assert(bestMove != -1); // Determine the type of node, so the score can be used correctly // after a lookup. int flag; if (result.score <= originalAlpha) { flag = NodeTypeUpper; allNodes++; } else if (result.score >= beta) { flag = NodeTypeLower; cutNodes++; } else { flag = NodeTypeExact; pvNodes++; } // Store the score in the t-table in case the same state is reached later. int moveToStore = (usingFlippedPosition) ? 6 - bestMove : bestMove; transpositionTable.Add(currentDepth, moveToStore, hash, result.score, flag); if (currentDepth == moveNumber) { finalMove = bestMove; } return result; }