private Score InnerSearch(Position position, int depth, bool isIid, Score alpha, Score beta, int ply) { // do it every 64? hmm.. if (_enteredCount % 64 == 0 && _timeStrategy.ShouldStop(_statistics)) { return(0); // dummy value won't be used } // It's important we increment _after_ checking, because if we're stopping, we don't want entered count to be increasing _enteredCount++; // by nature of this being called, we know this is a non-root node _statistics.NormalNonRootNodes++; // Note: we don't return draw on 50 move counter; if the engine doesn't know how to make progress in 50 moves, telling the engine it's about to draw can only induce mistakes. if (position.RepetitionNumber >= 3) { _statistics.TerminalNodes++; // TODO: contempt return(0); // draw } if (depth <= 0) { // Note: don't increase ply, as it's evaluating this position (same as current ply) // We don't need to count this, as qsearch does its own node counting return(_qSearch.Search(position, alpha, beta, ply)); } var moveList = _moveListCache.Get(ply); moveList.Clear(); _moveGenerator.Generate(moveList, position); var cachedPositionObject = _positionCache.Get(ply); if (!_moveGenerator.OnlyLegalMoves) { // remove all illegal moves for (int i = moveList.Count - 1; i >= 0; i--) { var move = moveList[i]; var testingPosition = Position.MakeMove(cachedPositionObject, move, position); if (testingPosition.MovedIntoCheck()) { moveList.QuickRemoveAt(i); } } } if (!moveList.Any()) { _statistics.TerminalNodes++; if (position.InCheck()) { return(Score.GetMateScore(position.GamePly)); } else { return(0); // draw; TODO: contempt } } // if depth is 1 or 2, then we'd literally just be searching twice if (depth > 1) { SortWithIid(position, moveList, depth, alpha, beta, ply); } bool raisedAlpha = false; int moveNumber = 0; foreach (var move in moveList) { var nextPosition = Position.MakeMove(cachedPositionObject, move, position); moveNumber++; if (!isIid) { _pvTable.Add(move, ply); } bool lateMove = moveNumber > 5; // TODO: disabled; enable this int reduction = lateMove ? 1 : 0; var eval = -InnerSearch(nextPosition, depth - 1 - reduction, isIid, -beta, -alpha, ply + 1); if (eval >= beta) { _statistics.NormalNonLeafNodes++; _statistics.NormalCutNodes++; _statistics.NormalCutMoveMisses += moveNumber - 1; // don't include the current move in the move misses calculation return(eval); // fail soft, but shouldn't matter for this naive implementation } if (eval > alpha) { raisedAlpha = true; alpha = eval; if (!isIid) { _pvTable.Commit(ply); } } } _statistics.NormalNonLeafNodes++; if (raisedAlpha) { _statistics.NormalPVNodes++; } else { _statistics.NormalAllNodes++; } return(alpha); }
public (Move move, Statistics statistics) Search(Position position, ITimeStrategy timeStrategy, Statistics.PrintInfoDelegate printInfoDelegate) { // setup _timeStrategy = timeStrategy; _enteredCount = 0; _statistics = new Statistics(); _statistics.Timer.Start(); _statistics.SideCalculating = position.SideToMove; _pvTable = new TriangularPVTable(); // TODO: should we be passing this in instead? _qSearch.StartSearch(_timeStrategy, _pvTable, _statistics); Move bestMove = Move.Null; var moveList = new List <Move>(); _moveGenerator.Generate(moveList, position); var cachedPositionObject = new Position(); if (!_moveGenerator.OnlyLegalMoves) { // remove all illegal moves for (int i = moveList.Count - 1; i >= 0; i--) { var move = moveList[i]; var testingPosition = Position.MakeMove(cachedPositionObject, move, position); if (testingPosition.MovedIntoCheck()) { moveList.QuickRemoveAt(i); } } } var scoredMoveList = new List <ScoredMove>(); foreach (var move in moveList) { scoredMoveList.Add(new ScoredMove(move, 0)); } // TODO: instead of looping here, why don't we only loop in InnerSearch and get the best value from the PV table? // That would simplify things a lot. // However, if we have aspiration windows and we get a beta cutoff, how do we retrieve the best move? Or is that even required? // The PV table would probably need to handle that case. for (int depth = 1;; depth++) { _statistics.NormalNonLeafNodes++; Score alpha = Score.MinValue; Score beta = Score.MaxValue; for (int i = 0; i < scoredMoveList.Count; i++) { var move = scoredMoveList[i].Move; var nextPosition = Position.MakeMove(cachedPositionObject, move, position); _pvTable.Add(move, 0); _statistics.CurrentDepth = depth; var nextEval = -InnerSearch(nextPosition, depth - 1, false, -beta, -alpha, 1); if (nextEval == alpha) { nextEval -= 1; } scoredMoveList[i] = new ScoredMove(move, nextEval); if (timeStrategy.ShouldStop(_statistics)) { _statistics.Timer.Stop(); return(bestMove, _statistics); } if (nextEval > alpha) { alpha = nextEval; bestMove = move; // this is safe, because we search the best move from last pass first in the next pass _pvTable.Commit(0); _statistics.BestLine = _pvTable.GetBestLine(); if (position.SideToMove == Color.White) { _statistics.CurrentScore = alpha; } else { _statistics.CurrentScore = -alpha; } printInfoDelegate(_statistics); } } // if we don't do this, we'll never return from a terminal position (with no moves) if (timeStrategy.ShouldStop(_statistics)) { _statistics.Timer.Stop(); return(bestMove, _statistics); } // sort the moves for next pass scoredMoveList.Sort((m1, m2) => (int)(m2.Score - m1.Score)); } }
public (Move move, Statistics statistics) Search(Position position, ITimeStrategy timeStrategy, Statistics.PrintInfoDelegate printInfoDelegate) { // setup _timeStrategy = timeStrategy; _enteredCount = 0; _statistics = new Statistics(); _statistics.Timer.Start(); _statistics.SideCalculating = position.SideToMove; _pvTable = new TriangularPVTable(); // TODO: should we be passing this in instead? _qSearch.StartSearch(_timeStrategy, _pvTable, _statistics); Move bestMove = Move.Null; var moveList = new List <Move>(); _moveGenerator.Generate(moveList, position); // TODO: instead of looping here, why don't we only loop in InnerSearch and get the best value from the PV table? // That would simplify things a lot. // However, if we have aspiration windows and we get a beta cutoff, how do we retrieve the best move? Or is that even required? // The PV table would probably need to handle that case. var tmpBestMove = Move.Null; for (int depth = 1;; depth++) { _statistics.NormalNonLeafNodes++; Score alpha = Score.MinValue; Score beta = Score.MaxValue; var cachedPositionObject = new Position(); foreach (var move in moveList) { var nextPosition = Position.MakeMove(cachedPositionObject, move, position); if (!_moveGenerator.OnlyLegalMoves && nextPosition.MovedIntoCheck()) { continue; } _pvTable.Add(move, 0); _statistics.CurrentDepth = depth; var nextEval = -InnerSearch(nextPosition, depth - 1, -beta, -alpha, 1); if (timeStrategy.ShouldStop(_statistics)) { _statistics.Timer.Stop(); return(bestMove, _statistics); } if (nextEval > alpha) { alpha = nextEval; tmpBestMove = move; _pvTable.Commit(0); } } // only committing best move after a full search // TODO: this will go away once we're no longer doing a search at this level bestMove = tmpBestMove; _statistics.BestLine = _pvTable.GetBestLine(); if (position.SideToMove == Color.White) { _statistics.CurrentScore = alpha; } else { _statistics.CurrentScore = -alpha; } // if we don't do this, we'll never return from a terminal position (with no moves) if (timeStrategy.ShouldStop(_statistics)) { _statistics.Timer.Stop(); return(bestMove, _statistics); } // if we didn't return, let's print some info! printInfoDelegate(_statistics); } }