// depth first search of longest walkable path, returns path length and the sequence of moves and boards in reverse order private Tuple <int, List <Tuple <Board, BoardSpace> > > LongestPathInReverseOrder(Board board, Func <Board, IEnumerable <BoardSpace> > moveGetter, Action <Board, BoardSpace> makeMove) { var longestPathLength = 0; var path = new List <Tuple <Board, BoardSpace> >(); foreach (var move in moveGetter(board)) { // bail if we're timing out if (_timer.Timeout() || _cancelToken.IsCancellationRequested) { break; } var newBoard = board.Copy(); makeMove(newBoard, move); var child = LongestPathInReverseOrder(newBoard, moveGetter, makeMove); var pathLength = child.Item1 + 1; if (pathLength > longestPathLength) { longestPathLength = pathLength; path = new List <Tuple <Board, BoardSpace> >(child.Item2) { Tuple.Create(newBoard, move) }; } } return(Tuple.Create(longestPathLength, path)); }
// recursive alpha beta private BestMoveResultWithStats BestMoveInternal(Board board, int depth, int alpha, int beta, CancellationToken cancelToken) { // if we reached the bottom, return if (depth == 0) { _numNodesAtDepthLimit++; return(new BestMoveResultWithStats(_config.Heuristic.Evaluate(board), null)); } var isMaxTurn = board.MyPlayer == board.PlayerToMove; var validMoves = board.GetValidMoves(); // if we hit game over before the depth limit, return infinity/-infinity if it's our/their turn if (!validMoves.Any()) { return(new BestMoveResultWithStats(isMaxTurn ? int.MinValue : int.MaxValue, null)); } BoardSpace bestMove = null; // generate new boards for each move and evaluate them so we can sort var validMovesWithBoard = validMoves.Select(x => { var newBoard = board.Copy().Move(x); _nodesGeneratedByDepth[depth]++; var score = _config.Heuristic.Evaluate(newBoard); return(new { move = x, newBoard, score }); }); // if we're maxing, sort with largest first, otherwise sort with smallest first if (isMaxTurn) { validMovesWithBoard = validMovesWithBoard.OrderByDescending(x => x.score); } else { validMovesWithBoard = validMovesWithBoard.OrderBy(x => x.score); } // evaluate this board because we'll need to for quiessence search var boardScore = _config.Heuristic.Evaluate(board); foreach (var move in validMovesWithBoard) { BestMoveResultWithStats childResult; // if we're doing a quiessence search, check to see if heuristic score change is interesting if (IsInterestingMove(boardScore, move.score)) { // extend search depth because this move looks interesting _numNodesQuiessenceSearched++; childResult = BestMoveInternal(move.newBoard, depth, alpha, beta, cancelToken); } else { // normal evaluation childResult = BestMoveInternal(move.newBoard, depth - 1, alpha, beta, cancelToken); } // if we're near timeout or asked to cancel, just bail :( if (_timer.Timeout() || cancelToken.IsCancellationRequested) { _timedOut = true; break; } if (isMaxTurn) // if it's a max turn, we want to check alpha { if (childResult.Score > alpha) { alpha = childResult.Score; bestMove = move.move; } } else // else it's a min turn, so we want to check beta { if (childResult.Score < beta) { beta = childResult.Score; bestMove = move.move; } } // alpha-beta trim if (alpha >= beta) { break; } } // if we didn't find anything good, just return the first one if (bestMove == null) { bestMove = validMoves.First(); } return(new BestMoveResultWithStats(isMaxTurn ? alpha : beta, bestMove)); }