/// <summary> /// Recursively calculates the value of the current position, up to the given depth. /// </summary> /// <param name="move">The last move. Win is checked in its neighborhood.</param> /// <param name="depth">The remaining available depth.</param> /// <param name="color">The color of the player to move. 1 ~ black, -1 ~ white.</param> /// <param name="alpha">The lower boundary on "interesting" scores.</param> /// <param name="beta">The upper boundary on "interesting" scores.</param> private int negamax(Square move, int depth, int color, int alpha, int beta) { Square localBestMove = move; int origAlpha = alpha; //TT lookup TTEntry ttEntry; if (tt.TryGetValue(currentHash, out ttEntry)) { localBestMove = ttEntry.BestMove; //for ordering purposes, just take the best move regardless of depth. For the rest, depth does matter. if (ttEntry.Depth >= depth) { switch (ttEntry.Flag) { case TTFlag.Exact: return(ttEntry.Score); case TTFlag.Upper: if (ttEntry.Score < beta) { beta = ttEntry.Score; } break; case TTFlag.Lower: if (ttEntry.Score > alpha) { alpha = ttEntry.Score; } break; default: break; } if (alpha >= beta) { return(ttEntry.Score); } } } //if state is terminal, return: if (checkWin(move, color == -1)) { return(-12345678); //someone won, so the score [calculated always for black] is +inf * (previous color). We return (color to move)*(score for black) --> always -inf } if (moveCount >= Gamestate.maxMoves) { return(0); //draw, return 0 } if (depth <= 0) { int result = color * evaluate(); //not a win, not a draw, but reached maximum depth (0 remaining depth), return evaluation return(result); } //otherwise keep searching children int value = -12345678; int candidateValue; foreach (Square child in GenerateShuffledMoves(localBestMove)) { if (stopSearch) { return(value); //do not visit any more children if search is stopped } incrementBoard(child, color == 1); candidateValue = -negamax(child, depth - 1, -color, -beta, -alpha); decrementBoard(child); if (candidateValue > value) { value = candidateValue; localBestMove = child; } if (value > alpha) { alpha = value; } if (alpha >= beta) { break; } } if (stopSearch) { return(value); //do not update TT if search is stopped } //TT update TTFlag flag; if (value <= origAlpha) { flag = TTFlag.Upper; } else if (value >= beta) { flag = TTFlag.Lower; } else { flag = TTFlag.Exact; } ttEntry = new TTEntry(currentHash, localBestMove, depth, value, flag); tt.Write(ttEntry); return(value); }
/// <summary> /// Check if there's a 5-in-a-row on the current board containing the given square for the given player. /// </summary> private bool checkWin(Square move, bool black) { byte color; if (black) { color = 2; } else { color = 1; } int boardX = move.x + 4; //to accomodate board padding int boardY = move.y + 4; //copypasta & padding usage to optimize compared to how Gamestate checks for win //horizontal int counter = 0; for (int i = -4; i <= 4; i++) { if (board[boardX + i, boardY] == color) { counter++; } else { counter = 0; } if (counter == 5) { return(true); } } //vertical counter = 0; for (int i = -4; i <= 4; i++) { if (board[boardX, boardY + i] == color) { counter++; } else { counter = 0; } if (counter == 5) { return(true); } } //asc diagonal counter = 0; for (int i = -4; i <= 4; i++) { if (board[boardX + i, boardY + i] == color) { counter++; } else { counter = 0; } if (counter == 5) { return(true); } } //desc diagonal counter = 0; for (int i = -4; i <= 4; i++) { if (board[boardX + i, boardY - i] == color) { counter++; } else { counter = 0; } if (counter == 5) { return(true); } } return(false); }
public void UndoMove(Square sq) { PauseThink(); decrementBoard(sq); ResumeThink(); }
/// <summary> /// Assigns to "bestMove" the result of a search of fixed max depth and returns its score. /// </summary> /// <param name="depth">Depth must be greater than 0.</param> public int DepthLimitedSearch(int depth) { //it is like a negamax step but we work with the 'bestMove' variable and disregard terminal state and omit TT writing int color; if (moveCount % 2 == 0) { color = 1; } else { color = -1; } int value = -123456789; int alpha = -123456789; int beta = 123456789; //TT lookup TTEntry ttEntry; if (tt.TryGetValue(currentHash, out ttEntry)) { if (ttEntry.Depth >= depth) { bestMove = ttEntry.BestMove; //as opposed to "inside the negamax", we only rewrite bestMove with a "deeper" recorded value! switch (ttEntry.Flag) { case TTFlag.Exact: return(ttEntry.Score); case TTFlag.Upper: beta = ttEntry.Score; break; case TTFlag.Lower: alpha = ttEntry.Score; break; default: break; } } } int candidateValue; foreach (Square child in GenerateShuffledMoves(bestMove)) { incrementBoard(child, color == 1); candidateValue = -negamax(child, depth - 1, -color, -beta, -alpha); decrementBoard(child); if (stopSearch) { break; // do not overwrite bestMove with a result that came from an interrupted branch } if (candidateValue > value) { value = candidateValue; if (value > alpha) { alpha = value; } bestMove = child; } } //first move of the game goes in the middle, there are no 'considered moves' in such case anyway. if (moveCount == 0) { bestMove = new Square(Gamestate.sizeX / 2, Gamestate.sizeY / 2); return(0); } return(value); }
//before every move update, the search must be paused (returned to root node) because otherwise the engine's internal board and/or search algo would crash and burn public void DoMove(Square sq) { PauseThink(); incrementBoard(sq, moveCount % 2 == 0); //mark the move from outside ResumeThink(); }