private async Task <Move> GetBestMoveAsync(GameBoard gameBoard, int maxDepth, TimeSpan maxTime, int maxHelperThreads, CancellationToken token) { if (null == gameBoard) { throw new ArgumentNullException(nameof(gameBoard)); } if (maxDepth < 0) { throw new ArgumentOutOfRangeException("maxDepth"); } if (maxTime < TimeSpan.Zero) { throw new ArgumentOutOfRangeException("maxTime"); } if (maxHelperThreads < 0) { throw new ArgumentOutOfRangeException("maxHelperThreads"); } if (gameBoard.GameIsOver) { throw new Exception("Game is over."); } BestMoveParams bestMoveParams = new BestMoveParams() { MaxSearchDepth = maxDepth, MaxSearchTime = maxTime, MaxHelperThreads = maxHelperThreads, }; EvaluatedMoveCollection evaluatedMoves = await EvaluateMovesAsync(gameBoard, bestMoveParams, token); if (evaluatedMoves.Count == 0) { throw new Exception("No moves after evaluation!"); } // Make sure at least one move is reported OnBestMoveFound(bestMoveParams, evaluatedMoves.BestMove); return(bestMoveParams.BestMove.Move); }
private async Task <EvaluatedMoveCollection> EvaluateMovesToDepthAsync(GameBoard gameBoard, int depth, IEnumerable <EvaluatedMove> movesToEvaluate, CancellationToken token) { double alpha = double.NegativeInfinity; double beta = double.PositiveInfinity; int color = gameBoard.CurrentTurnColor == PlayerColor.White ? 1 : -1; double alphaOriginal = alpha; double?bestValue = null; EvaluatedMoveCollection evaluatedMoves = new EvaluatedMoveCollection(); bool firstMove = true; foreach (EvaluatedMove moveToEvaluate in movesToEvaluate) { bool updateAlpha = false; if (token.IsCancellationRequested) { // Cancel return(new EvaluatedMoveCollection(movesToEvaluate, false)); } gameBoard.TrustedPlay(moveToEvaluate.Move); double?value = null; if (firstMove) { // Full window search value = -1 * await PrincipalVariationSearchAsync(gameBoard, depth - 1, -beta, -alpha, -color, OrderType.Default, token); updateAlpha = true; firstMove = false; } else { // Null window search value = -1 * await PrincipalVariationSearchAsync(gameBoard, depth - 1, -alpha - double.Epsilon, -alpha, -color, OrderType.Default, token); if (value.HasValue && value > alpha && value < beta) { // Research with full window value = -1 * await PrincipalVariationSearchAsync(gameBoard, depth - 1, -beta, -alpha, -color, OrderType.Default, token); updateAlpha = true; } } gameBoard.UndoLastMove(); if (!value.HasValue) { // Cancel occurred during evaluation return(new EvaluatedMoveCollection(movesToEvaluate, false)); } EvaluatedMove evaluatedMove = new EvaluatedMove(moveToEvaluate.Move, value.Value, depth); evaluatedMoves.Add(evaluatedMove); if (updateAlpha) { alpha = Math.Max(alpha, value.Value); } if (!bestValue.HasValue || value >= bestValue) { bestValue = value; } if (bestValue >= beta) { // A winning move has been found, since beta is always infinity in this function break; } } ulong key = gameBoard.ZobristKey; TranspositionTableEntry tEntry = new TranspositionTableEntry(); if (bestValue <= alphaOriginal) { // Losing move since alphaOriginal os negative infinity in this function tEntry.Type = TranspositionTableEntryType.UpperBound; } else { // Move is a lower bound winning move if bestValue >= beta (always infinity in this function), otherwise it's exact tEntry.Type = bestValue >= beta ? TranspositionTableEntryType.LowerBound : TranspositionTableEntryType.Exact; tEntry.BestMove = evaluatedMoves.BestMove.Move; } tEntry.Value = bestValue.Value; tEntry.Depth = depth; TranspositionTable.Store(key, tEntry); return(evaluatedMoves); }
private async Task <EvaluatedMoveCollection> EvaluateMovesAsync(GameBoard gameBoard, BestMoveParams bestMoveParams, CancellationToken token) { EvaluatedMoveCollection movesToEvaluate = new EvaluatedMoveCollection(); EvaluatedMove bestMove = null; // Try to get cached best move if available ulong key = gameBoard.ZobristKey; if (TranspositionTable.TryLookup(key, out TranspositionTableEntry tEntry) && null != tEntry.BestMove) { bestMove = new EvaluatedMove(tEntry.BestMove, tEntry.Value, tEntry.Depth); OnBestMoveFound(bestMoveParams, bestMove); } if (null != bestMove && double.IsPositiveInfinity(bestMove.ScoreAfterMove)) { // Winning move, don't search movesToEvaluate.Add(bestMove); return(movesToEvaluate); } List <EvaluatedMove> validMoves = GetPreSortedValidMoves(gameBoard, bestMove); movesToEvaluate.Add(validMoves, false); if (movesToEvaluate.Count <= 1 || bestMoveParams.MaxSearchDepth == 0) { // No need to search return(movesToEvaluate); } // Iterative search int depth = 1 + Math.Max(0, movesToEvaluate.BestMove.Depth); while (depth <= bestMoveParams.MaxSearchDepth) { // Start LazySMP helper threads CancellationTokenSource helperCTS = new CancellationTokenSource(); Task[] helperThreads = StartHelperThreads(gameBoard, depth, bestMoveParams.MaxHelperThreads, helperCTS); // "Re-sort" moves to evaluate based on the next iteration movesToEvaluate = await EvaluateMovesToDepthAsync(gameBoard, depth, movesToEvaluate, token); // End LazySMP helper threads EndHelperThreads(helperThreads, helperCTS); // Fire BestMoveFound for current depth OnBestMoveFound(bestMoveParams, movesToEvaluate.BestMove); if (double.IsInfinity(movesToEvaluate.BestMove.ScoreAfterMove)) { // The best move ends the game, stop searching break; } // Prune game-losing moves if possible movesToEvaluate.PruneGameLosingMoves(); if (movesToEvaluate.Count <= 1) { // Only one move, no reason to keep looking break; } if (token.IsCancellationRequested) { // Cancelled, stop searching break; } depth = 1 + Math.Max(depth, movesToEvaluate.BestMove.Depth); } return(movesToEvaluate); }