/* CuckooChess - A java chess program. Copyright (C) 2011 Peter Ă–sterlund, peterosterlund2@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package chess; import chess.TranspositionTable.TTEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; public class Search { final static int plyScale = 8; // Fractional ply resolution Position pos; MoveGen moveGen; Evaluate eval; KillerTable kt; History ht; long[] posHashList; // List of hashes for previous positions up to the last "zeroing" move. int posHashListSize; // Number of used entries in posHashList int posHashFirstNew; // First entry in posHashList that has not been played OTB. TranspositionTable tt; TreeLogger log = null; private static final class SearchTreeInfo { UndoInfo undoInfo; Move hashMove; // Temporary storage for local hashMove variable boolean allowNullMove; // Don't allow two null-moves in a row Move bestMove; // Copy of the best found move at this ply Move currentMove; // Move currently being searched int lmr; // LMR reduction amount long nodeIdx; SearchTreeInfo() { undoInfo = new UndoInfo(); hashMove = new Move(0, 0, 0); allowNullMove = true; bestMove = new Move(0, 0, 0); } } SearchTreeInfo[] searchTreeInfo; // Time management long tStart; // Time when search started long minTimeMillis; // Minimum recommended thinking time long maxTimeMillis; // Maximum allowed thinking time boolean searchNeedMoreTime; // True if negaScout should use up to maxTimeMillis time. private long maxNodes; // Maximum number of nodes to search (approximately) int nodesToGo; // Number of nodes until next time check public int nodesBetweenTimeCheck = 5000; // How often to check remaining time // Reduced strength variables private int strength = 1000; // Strength (0-1000) boolean weak = false; // Set to strength < 1000 long randomSeed = 0; // Search statistics stuff long nodes; long qNodes; int[] nodesPlyVec; int[] nodesDepthVec; long totalNodes; long tLastStats; // Time when notifyStats was last called boolean verbose; public final static int MATE0 = 32000; public final static int UNKNOWN_SCORE = -32767; // Represents unknown static eval score int q0Eval; // Static eval score at first level of quiescence search public Search(Position pos, long[] posHashList, int posHashListSize, TranspositionTable tt, History ht) { this.pos = new Position(pos); this.moveGen = new MoveGen(); this.posHashList = posHashList; this.posHashListSize = posHashListSize; this.tt = tt; this.ht = ht; eval = new Evaluate(); kt = new KillerTable(); posHashFirstNew = posHashListSize; initNodeStats(); minTimeMillis = -1; maxTimeMillis = -1; searchNeedMoreTime = false; maxNodes = -1; final int vecLen = 200; searchTreeInfo = new SearchTreeInfo[vecLen]; for (int i = 0; i < vecLen; i++) { searchTreeInfo[i] = new SearchTreeInfo(); } } static final class StopSearch extends Exception { private static final long serialVersionUID = -5546906604987117015L; public StopSearch() { } public StopSearch(String msg) { super(msg); } } /** * Used to get various search information during search */ public interface Listener { void notifyDepth(int depth); void notifyCurrMove(Move m, int moveNr); void notifyPV(int depth, int score, int time, long nodes, int nps, boolean isMate, boolean upperBound, boolean lowerBound, ArrayList<Move> pv); void notifyStats(long nodes, int nps, int time); } Listener listener; public void setListener(Listener listener) { this.listener = listener; } private final static class MoveInfo { Move move; long nodes; MoveInfo(Move m, int n) { move = m; nodes = n; } public static final class SortByScore implements Comparator<MoveInfo> { public int compare(MoveInfo mi1, MoveInfo mi2) { if ((mi1 == null) && (mi2 == null)) return 0; if (mi1 == null) return 1; if (mi2 == null) return -1; return mi2.move.score - mi1.move.score; } } public static final class SortByNodes implements Comparator<MoveInfo> { public int compare(MoveInfo mi1, MoveInfo mi2) { if ((mi1 == null) && (mi2 == null)) return 0; if (mi1 == null) return 1; if (mi2 == null) return -1; long d = mi2.nodes - mi1.nodes; if (d < 0) return -1; else if (d > 0) return 1; else return 0; } } } final public void timeLimit(int minTimeLimit, int maxTimeLimit) { minTimeMillis = minTimeLimit; maxTimeMillis = maxTimeLimit; } final public void setStrength(int strength, long randomSeed) { if (strength < 0) strength = 0; if (strength > 1000) strength = 1000; this.strength = strength; weak = strength < 1000; this.randomSeed = randomSeed; } final public Move iterativeDeepening(MoveGen.MoveList scMovesIn, int maxDepth, long initialMaxNodes, boolean verbose) { tStart = System.currentTimeMillis(); // log = TreeLogger.getWriter("/home/petero/treelog.dmp", pos); totalNodes = 0; if (scMovesIn.size <= 0) return null; // No moves to search MoveInfo[] scMoves; { // If strength is < 10%, only include a subset of the root moves. // At least one move is always included though. boolean[] includedMoves = new boolean[scMovesIn.size]; long rndL = pos.zobristHash() ^ randomSeed; includedMoves[(int)(Math.abs(rndL) % scMovesIn.size)] = true; int nIncludedMoves = 1; double pIncl = (strength < 100) ? strength * strength * 1e-4 : 1.0; for (int mi = 0; mi < scMovesIn.size; mi++) { rndL = 6364136223846793005L * rndL + 1442695040888963407L; double rnd = ((rndL & 0x7fffffffffffffffL) % 1000000000) / 1e9; if (!includedMoves[mi] && (rnd < pIncl)) { includedMoves[mi] = true; nIncludedMoves++; } } scMoves = new MoveInfo[nIncludedMoves]; for (int mi = 0, len = 0; mi < scMovesIn.size; mi++) { if (includedMoves[mi]) { Move m = scMovesIn.m[mi]; scMoves[len++] = new MoveInfo(m, 0); } } } maxNodes = initialMaxNodes; nodesToGo = 0; Position origPos = new Position(pos); int bestScoreLastIter = 0; boolean firstIteration = true; Move bestMove = scMoves[0].move; this.verbose = verbose; if ((maxDepth < 0) || (maxDepth > 100)) { maxDepth = 100; } for (int i = 0; i < searchTreeInfo.length; i++) { searchTreeInfo[i].allowNullMove = true; } try { for (int depthS = plyScale; ; depthS += plyScale, firstIteration = false) { initNodeStats(); if (listener != null) listener.notifyDepth(depthS/plyScale); int aspirationDelta = (Math.abs(bestScoreLastIter) <= MATE0 / 2) ? 20 : 1000; int alpha = firstIteration ? -Search.MATE0 : Math.max(bestScoreLastIter - aspirationDelta, -Search.MATE0); int bestScore = -Search.MATE0; UndoInfo ui = new UndoInfo(); boolean needMoreTime = false; for (int mi = 0; mi < scMoves.length; mi++) { searchNeedMoreTime = (mi > 0); Move m = scMoves[mi].move; if ((listener != null) && (System.currentTimeMillis() - tStart >= 1000)) { listener.notifyCurrMove(m, mi + 1); } nodes = qNodes = 0; posHashList[posHashListSize++] = pos.zobristHash(); boolean givesCheck = MoveGen.givesCheck(pos, m); int beta; if (firstIteration) { beta = Search.MATE0; } else { beta = (mi == 0) ? Math.min(bestScoreLastIter + aspirationDelta, Search.MATE0) : alpha + 1; } int lmrS = 0; boolean isCapture = (pos.getPiece(m.to) != Piece.EMPTY); boolean isPromotion = (m.promoteTo != Piece.EMPTY); if ((depthS >= 3*plyScale) && !isCapture && !isPromotion) { if (!givesCheck && !passedPawnPush(pos, m)) { if (mi >= 3) lmrS = plyScale; } } /* long nodes0 = nodes; long qNodes0 = qNodes; System.out.printf("%2d %5s %5d %5d %6s %6s ", mi, "-", alpha, beta, "-", "-"); System.out.printf("%-6s...\n", TextIO.moveToUCIString(m)); */ pos.makeMove(m, ui); SearchTreeInfo sti = searchTreeInfo[0]; sti.currentMove = m; sti.lmr = lmrS; sti.nodeIdx = -1; int score = -negaScout(-beta, -alpha, 1, depthS - lmrS - plyScale, -1, givesCheck); if ((lmrS > 0) && (score > alpha)) { sti.lmr = 0; score = -negaScout(-beta, -alpha, 1, depthS - plyScale, -1, givesCheck); } long nodesThisMove = nodes + qNodes; posHashListSize--; pos.unMakeMove(m, ui); { int type = TTEntry.T_EXACT; if (score <= alpha) { type = TTEntry.T_LE; } else if (score >= beta) { type = TTEntry.T_GE; } m.score = score; tt.insert(pos.historyHash(), m, type, 0, depthS, UNKNOWN_SCORE); } if (score >= beta) { int retryDelta = aspirationDelta * 2; while (score >= beta) { beta = Math.min(score + retryDelta, Search.MATE0); retryDelta = Search.MATE0 * 2; if (mi != 0) needMoreTime = true; bestMove = m; if (verbose) System.out.printf("%-6s %6d %6d %6d >=\n", TextIO.moveToString(pos, m, false), score, nodes, qNodes); notifyPV(depthS/plyScale, score, false, true, m); nodes = qNodes = 0; posHashList[posHashListSize++] = pos.zobristHash(); pos.makeMove(m, ui); int score2 = -negaScout(-beta, -score, 1, depthS - plyScale, -1, givesCheck); score = Math.max(score, score2); nodesThisMove += nodes + qNodes; posHashListSize--; pos.unMakeMove(m, ui); } } else if ((mi == 0) && (score <= alpha)) { int retryDelta = Search.MATE0 * 2; while (score <= alpha) { alpha = Math.max(score - retryDelta, -Search.MATE0); retryDelta = Search.MATE0 * 2; needMoreTime = searchNeedMoreTime = true; if (verbose) System.out.printf("%-6s %6d %6d %6d <=\n", TextIO.moveToString(pos, m, false), score, nodes, qNodes); notifyPV(depthS/plyScale, score, true, false, m); nodes = qNodes = 0; posHashList[posHashListSize++] = pos.zobristHash(); pos.makeMove(m, ui); score = -negaScout(-score, -alpha, 1, depthS - plyScale, -1, givesCheck); nodesThisMove += nodes + qNodes; posHashListSize--; pos.unMakeMove(m, ui); } } if (verbose || ((listener != null) && !firstIteration)) { boolean havePV = false; String PV = ""; if ((score > alpha) || (mi == 0)) { havePV = true; if (verbose) { PV = TextIO.moveToString(pos, m, false) + " "; pos.makeMove(m, ui); PV += tt.extractPV(pos); pos.unMakeMove(m, ui); } } if (verbose) { /* System.out.printf("%2d %5d %5d %5d %6d %6d ", mi, score, alpha, beta, nodes-nodes0, qNodes-qNodes0); System.out.printf("%-6s\n", TextIO.moveToUCIString(m)); */ System.out.printf("%-6s %6d %6d %6d%s %s\n", TextIO.moveToString(pos, m, false), score, nodes, qNodes, (score > alpha ? " *" : ""), PV); } if (havePV && !firstIteration) { notifyPV(depthS/plyScale, score, false, false, m); } } scMoves[mi].move.score = score; scMoves[mi].nodes = nodesThisMove; bestScore = Math.max(bestScore, score); if (!firstIteration) { if ((score > alpha) || (mi == 0)) { alpha = score; MoveInfo tmp = scMoves[mi]; for (int i = mi - 1; i >= 0; i--) { scMoves[i + 1] = scMoves[i]; } scMoves[0] = tmp; bestMove = scMoves[0].move; } } if (!firstIteration) { long timeLimit = needMoreTime ? maxTimeMillis : minTimeMillis; if (timeLimit >= 0) { long tNow = System.currentTimeMillis(); if (tNow - tStart >= timeLimit) break; } } } if (firstIteration) { Arrays.sort(scMoves, new MoveInfo.SortByScore()); bestMove = scMoves[0].move; notifyPV(depthS/plyScale, bestMove.score, false, false, bestMove); } long tNow = System.currentTimeMillis(); if (verbose) { for (int i = 0; i < 20; i++) { System.out.printf("%2d %7d %7d\n", i, nodesPlyVec[i], nodesDepthVec[i]); } System.out.printf("Time: %.3f depth:%.2f nps:%d\n", (tNow - tStart) * .001, depthS/(double)plyScale, (int)(totalNodes / ((tNow - tStart) * .001))); } if (maxTimeMillis >= 0) { if (tNow - tStart >= minTimeMillis) break; } if (depthS >= maxDepth * plyScale) break; if (maxNodes >= 0) { if (totalNodes >= maxNodes) break; } int plyToMate = Search.MATE0 - Math.abs(bestScore); if (depthS >= plyToMate * plyScale) break; bestScoreLastIter = bestScore; if (!firstIteration) { // Moves that were hard to search should be searched early in the next iteration Arrays.sort(scMoves, 1, scMoves.length, new MoveInfo.SortByNodes()); } } } catch (StopSearch ss) { pos = origPos; } notifyStats(); if (log != null) { log.close(); log = null; } return bestMove; } private final void notifyPV(int depth, int score, boolean uBound, boolean lBound, Move m) { if (listener != null) { boolean isMate = false; if (score > MATE0 / 2) { isMate = true; score = (MATE0 - score) / 2; } else if (score < -MATE0 / 2) { isMate = true; score = -((MATE0 + score - 1) / 2); } long tNow = System.currentTimeMillis(); int time = (int) (tNow - tStart); int nps = (time > 0) ? (int)(totalNodes / (time / 1000.0)) : 0; ArrayList<Move> pv = tt.extractPVMoves(pos, m); listener.notifyPV(depth, score, time, totalNodes, nps, isMate, uBound, lBound, pv); } } private final void notifyStats() { long tNow = System.currentTimeMillis(); if (listener != null) { int time = (int) (tNow - tStart); int nps = (time > 0) ? (int)(totalNodes / (time / 1000.0)) : 0; listener.notifyStats(totalNodes, nps, time); } tLastStats = tNow; } private static final Move emptyMove = new Move(0, 0, Piece.EMPTY, 0); /** * Main recursive search algorithm. * @return Score for the side to make a move, in position given by "pos". */ final public int negaScout(int alpha, int beta, int ply, int depth, int recaptureSquare, final boolean inCheck) throws StopSearch { if (log != null) { SearchTreeInfo sti = searchTreeInfo[ply-1]; long idx = log.logNodeStart(sti.nodeIdx, sti.currentMove, alpha, beta, ply, depth/plyScale); searchTreeInfo[ply].nodeIdx = idx; } if (--nodesToGo <= 0) { nodesToGo = nodesBetweenTimeCheck; long tNow = System.currentTimeMillis(); long timeLimit = searchNeedMoreTime ? maxTimeMillis : minTimeMillis; if ( ((timeLimit >= 0) && (tNow - tStart >= timeLimit)) || ((maxNodes >= 0) && (totalNodes >= maxNodes))) { throw new StopSearch(); } if (tNow - tLastStats >= 1000) { notifyStats(); } } // Collect statistics if (verbose) { if (ply < 20) nodesPlyVec[ply]++; if (depth < 20*plyScale) nodesDepthVec[depth/plyScale]++; } final long hKey = pos.historyHash(); // Draw tests if (canClaimDraw50(pos)) { if (MoveGen.canTakeKing(pos)) { int score = MATE0 - ply; if (log != null) log.logNodeEnd(searchTreeInfo[ply].nodeIdx, score, TTEntry.T_EXACT, UNKNOWN_SCORE, hKey); return score; } if (inCheck) { MoveGen.MoveList moves = moveGen.pseudoLegalMoves(pos); MoveGen.removeIllegal(pos, moves); if (moves.size == 0) { // Can't claim draw if already check mated. int score = -(MATE0-(ply+1)); if (log != null) log.logNodeEnd(searchTreeInfo[ply].nodeIdx, score, TTEntry.T_EXACT, UNKNOWN_SCORE, hKey); moveGen.returnMoveList(moves); return score; } moveGen.returnMoveList(moves); } if (log != null) log.logNodeEnd(searchTreeInfo[ply].nodeIdx, 0, TTEntry.T_EXACT, UNKNOWN_SCORE, hKey); return 0; } if (canClaimDrawRep(pos, posHashList, posHashListSize, posHashFirstNew)) { if (log != null) log.logNodeEnd(searchTreeInfo[ply].nodeIdx, 0, TTEntry.T_EXACT, UNKNOWN_SCORE, hKey); return 0; // No need to test for mate here, since it would have been // discovered the first time the position came up. } int evalScore = UNKNOWN_SCORE; // Check transposition table TTEntry ent = tt.probe(hKey); Move hashMove = null; SearchTreeInfo sti = searchTreeInfo[ply]; if (ent.type != TTEntry.T_EMPTY) { int score = ent.getScore(ply); evalScore = ent.evalScore; int plyToMate = MATE0 - Math.abs(score); int eDepth = ent.getDepth(); hashMove = sti.hashMove; ent.getMove(hashMove); if ((beta == alpha + 1) && ((eDepth >= depth) || (eDepth >= plyToMate*plyScale))) { if ( (ent.type == TTEntry.T_EXACT) || (ent.type == TTEntry.T_GE) && (score >= beta) || (ent.type == TTEntry.T_LE) && (score <= alpha)) { if (score >= beta) { hashMove = sti.hashMove; if ((hashMove != null) && (hashMove.from != hashMove.to)) if (pos.getPiece(hashMove.to) == Piece.EMPTY) kt.addKiller(ply, hashMove); } sti.bestMove = hashMove; if (log != null) log.logNodeEnd(searchTreeInfo[ply].nodeIdx, score, ent.type, evalScore, hKey); return score; } } } int posExtend = inCheck ? plyScale : 0; // Check extension // If out of depth, perform quiescence search if (depth + posExtend <= 0) { q0Eval = evalScore; sti.bestMove.clear(); int score = quiesce(alpha, beta, ply, 0, inCheck); int type = TTEntry.T_EXACT; if (score <= alpha) { type = TTEntry.T_LE; } else if (score >= beta) { type = TTEntry.T_GE; } sti.bestMove.score = score; tt.insert(hKey, sti.bestMove, type, ply, depth, q0Eval); if (log != null) log.logNodeEnd(sti.nodeIdx, score, type, q0Eval, hKey); return score; } // Razoring if ((Math.abs(alpha) <= MATE0 / 2) && (depth < 4*plyScale) && (beta == alpha + 1)) { if (evalScore == UNKNOWN_SCORE) { evalScore = eval.evalPos(pos); } final int razorMargin = 250; if (evalScore < beta - razorMargin) { q0Eval = evalScore; int score = quiesce(alpha-razorMargin, beta-razorMargin, ply, 0, inCheck); if (score <= alpha-razorMargin) { emptyMove.score = score; tt.insert(hKey, emptyMove, TTEntry.T_LE, ply, depth, q0Eval); if (log != null) log.logNodeEnd(sti.nodeIdx, score, TTEntry.T_LE, q0Eval, hKey); return score; } } } // Reverse futility pruning if (!inCheck && (depth < 5*plyScale) && (posExtend == 0) && (Math.abs(alpha) <= MATE0 / 2) && (Math.abs(beta) <= MATE0 / 2)) { boolean mtrlOk; if (pos.whiteMove) { mtrlOk = (pos.wMtrl > pos.wMtrlPawns) && (pos.wMtrlPawns > 0); } else { mtrlOk = (pos.bMtrl > pos.bMtrlPawns) && (pos.bMtrlPawns > 0); } if (mtrlOk) { int margin; if (depth <= plyScale) margin = 204; else if (depth <= 2*plyScale) margin = 420; else if (depth <= 3*plyScale) margin = 533; else margin = 788; if (evalScore == UNKNOWN_SCORE) evalScore = eval.evalPos(pos); if (evalScore - margin >= beta) { emptyMove.score = evalScore - margin; tt.insert(hKey, emptyMove, TTEntry.T_GE, ply, depth, evalScore); if (log != null) log.logNodeEnd(sti.nodeIdx, evalScore - margin, TTEntry.T_GE, evalScore, hKey); return evalScore - margin; } } } // Try null-move pruning sti.currentMove = emptyMove; if ( (depth >= 3*plyScale) && !inCheck && sti.allowNullMove && (Math.abs(beta) <= MATE0 / 2)) { if (MoveGen.canTakeKing(pos)) { int score = MATE0 - ply; if (log != null) log.logNodeEnd(sti.nodeIdx, score, TTEntry.T_EXACT, evalScore, hKey); return score; } boolean nullOk; if (pos.whiteMove) { nullOk = (pos.wMtrl > pos.wMtrlPawns) && (pos.wMtrlPawns > 0); } else { nullOk = (pos.bMtrl > pos.bMtrlPawns) && (pos.bMtrlPawns > 0); } if (nullOk) { if (evalScore == UNKNOWN_SCORE) evalScore = eval.evalPos(pos); if (evalScore < beta) nullOk = false; } if (nullOk) { final int R = (depth > 6*plyScale) ? 4*plyScale : 3*plyScale; pos.setWhiteMove(!pos.whiteMove); int epSquare = pos.getEpSquare(); pos.setEpSquare(-1); searchTreeInfo[ply+1].allowNullMove = false; searchTreeInfo[ply+1].bestMove.clear(); int score = -negaScout(-beta, -(beta - 1), ply + 1, depth - R, -1, false); searchTreeInfo[ply+1].allowNullMove = true; pos.setEpSquare(epSquare); pos.setWhiteMove(!pos.whiteMove); if (score >= beta) { if (score > MATE0 / 2) score = beta; emptyMove.score = score; tt.insert(hKey, emptyMove, TTEntry.T_GE, ply, depth, evalScore); if (log != null) log.logNodeEnd(sti.nodeIdx, score, TTEntry.T_GE, evalScore, hKey); return score; } else { if ((searchTreeInfo[ply-1].lmr > 0) && (depth < 5*plyScale)) { Move m1 = searchTreeInfo[ply-1].currentMove; Move m2 = searchTreeInfo[ply+1].bestMove; // threat move if (relatedMoves(m1, m2)) { // if the threat move was made possible by a reduced // move on the previous ply, the reduction was unsafe. // Return alpha to trigger a non-reduced re-search. if (log != null) log.logNodeEnd(sti.nodeIdx, alpha, TTEntry.T_LE, evalScore, hKey); return alpha; } } } } } boolean futilityPrune = false; int futilityScore = alpha; if (!inCheck && (depth < 5*plyScale) && (posExtend == 0)) { if ((Math.abs(alpha) <= MATE0 / 2) && (Math.abs(beta) <= MATE0 / 2)) { int margin; if (depth <= plyScale) margin = 61; else if (depth <= 2*plyScale) margin = 144; else if (depth <= 3*plyScale) margin = 268; else margin = 334; if (evalScore == UNKNOWN_SCORE) evalScore = eval.evalPos(pos); futilityScore = evalScore + margin; if (futilityScore <= alpha) futilityPrune = true; } } if ((depth > 4*plyScale) && ((hashMove == null) || (hashMove.from == hashMove.to))) { boolean isPv = beta > alpha + 1; if (isPv || (depth > 8 * plyScale)) { // No hash move. Try internal iterative deepening. long savedNodeIdx = sti.nodeIdx; int newDepth = isPv ? depth - 2 * plyScale : depth * 3 / 8; negaScout(alpha, beta, ply, newDepth, -1, inCheck); sti.nodeIdx = savedNodeIdx; ent = tt.probe(hKey); if (ent.type != TTEntry.T_EMPTY) { hashMove = sti.hashMove; ent.getMove(hashMove); } } } // Start searching move alternatives MoveGen.MoveList moves; if (inCheck) moves = moveGen.checkEvasions(pos); else moves = moveGen.pseudoLegalMoves(pos); boolean seeDone = false; boolean hashMoveSelected = true; if (!selectHashMove(moves, hashMove)) { scoreMoveList(moves, ply); seeDone = true; hashMoveSelected = false; } UndoInfo ui = sti.undoInfo; boolean haveLegalMoves = false; int illegalScore = -(MATE0-(ply+1)); int b = beta; int bestScore = illegalScore; int bestMove = -1; int lmrCount = 0; for (int mi = 0; mi < moves.size; mi++) { if ((mi == 1) && !seeDone) { scoreMoveList(moves, ply, 1); seeDone = true; } if ((mi > 0) || !hashMoveSelected) { selectBest(moves, mi); } Move m = moves.m[mi]; if (pos.getPiece(m.to) == (pos.whiteMove ? Piece.BKING : Piece.WKING)) { moveGen.returnMoveList(moves); int score = MATE0-ply; if (log != null) log.logNodeEnd(sti.nodeIdx, score, TTEntry.T_EXACT, evalScore, hKey); return score; // King capture } int newCaptureSquare = -1; boolean isCapture = (pos.getPiece(m.to) != Piece.EMPTY); boolean isPromotion = (m.promoteTo != Piece.EMPTY); int sVal = Integer.MIN_VALUE; boolean mayReduce = (m.score < 53) && (!isCapture || m.score < 0) && !isPromotion; boolean givesCheck = MoveGen.givesCheck(pos, m); boolean doFutility = false; if (mayReduce && haveLegalMoves && !givesCheck && !passedPawnPush(pos, m)) { if ((Math.abs(alpha) <= MATE0 / 2) && (Math.abs(beta) <= MATE0 / 2)) { int moveCountLimit; if (depth <= plyScale) moveCountLimit = 3; else if (depth <= 2 * plyScale) moveCountLimit = 6; else if (depth <= 3 * plyScale) moveCountLimit = 12; else if (depth <= 4 * plyScale) moveCountLimit = 24; else moveCountLimit = 256; if (mi >= moveCountLimit) continue; // Late move pruning } if (futilityPrune) doFutility = true; } int score; if (doFutility) { score = futilityScore; } else { int moveExtend = 0; if (posExtend == 0) { final int pV = Evaluate.pV; if ((m.to == recaptureSquare)) { if (sVal == Integer.MIN_VALUE) sVal = SEE(m); int tVal = Evaluate.pieceValue[pos.getPiece(m.to)]; if (sVal > tVal - pV / 2) moveExtend = plyScale; } if ((moveExtend < plyScale) && isCapture && (pos.wMtrlPawns + pos.bMtrlPawns > pV)) { // Extend if going into pawn endgame int capVal = Evaluate.pieceValue[pos.getPiece(m.to)]; if (pos.whiteMove) { if ((pos.wMtrl == pos.wMtrlPawns) && (pos.bMtrl - pos.bMtrlPawns == capVal)) moveExtend = plyScale; } else { if ((pos.bMtrl == pos.bMtrlPawns) && (pos.wMtrl - pos.wMtrlPawns == capVal)) moveExtend = plyScale; } } } int extend = Math.max(posExtend, moveExtend); int lmr = 0; if ((depth >= 3*plyScale) && mayReduce && (extend == 0)) { if (!givesCheck && !passedPawnPush(pos, m)) { lmrCount++; if ((lmrCount > 3) && (depth > 3*plyScale) && !isCapture) { lmr = 2*plyScale; } else { lmr = 1*plyScale; } } } int newDepth = depth - plyScale + extend - lmr; if (isCapture && (givesCheck || (depth + extend) > plyScale)) { // Compute recapture target square, but only if we are not going // into q-search at the next ply. int fVal = Evaluate.pieceValue[pos.getPiece(m.from)]; int tVal = Evaluate.pieceValue[pos.getPiece(m.to)]; final int pV = Evaluate.pV; if (Math.abs(tVal - fVal) < pV / 2) { // "Equal" capture sVal = SEE(m); if (Math.abs(sVal) < pV / 2) newCaptureSquare = m.to; } } posHashList[posHashListSize++] = pos.zobristHash(); pos.makeMove(m, ui); nodes++; totalNodes++; sti.currentMove = m; /* long nodes0 = nodes; long qNodes0 = qNodes; if ((ply < 3) && (newDepth > plyScale)) { System.out.printf("%2d %5s %5d %5d %6s %6s ", mi, "-", alpha, beta, "-", "-"); for (int i = 0; i < ply; i++) System.out.printf(" "); System.out.printf("%-6s...\n", TextIO.moveToUCIString(m)); } */ sti.lmr = lmr; score = -negaScout(-b, -alpha, ply + 1, newDepth, newCaptureSquare, givesCheck); if (((lmr > 0) && (score > alpha)) || ((score > alpha) && (score < beta) && (b != beta) && (score != illegalScore))) { sti.lmr = 0; newDepth += lmr; score = -negaScout(-beta, -alpha, ply + 1, newDepth, newCaptureSquare, givesCheck); } /* if (ply <= 3) { System.out.printf("%2d %5d %5d %5d %6d %6d ", mi, score, alpha, beta, nodes-nodes0, qNodes-qNodes0); for (int i = 0; i < ply; i++) System.out.printf(" "); System.out.printf("%-6s\n", TextIO.moveToUCIString(m)); }*/ posHashListSize--; pos.unMakeMove(m, ui); } if (weak && haveLegalMoves) if (weakPlaySkipMove(pos, m, ply)) score = illegalScore; m.score = score; if (score != illegalScore) { haveLegalMoves = true; } bestScore = Math.max(bestScore, score); if (score > alpha) { alpha = score; bestMove = mi; sti.bestMove.from = m.from; sti.bestMove.to = m.to; sti.bestMove.promoteTo = m.promoteTo; } if (alpha >= beta) { if (pos.getPiece(m.to) == Piece.EMPTY) { kt.addKiller(ply, m); ht.addSuccess(pos, m, depth/plyScale); for (int mi2 = mi - 1; mi2 >= 0; mi2--) { Move m2 = moves.m[mi2]; if (pos.getPiece(m2.to) == Piece.EMPTY) ht.addFail(pos, m2, depth/plyScale); } } tt.insert(hKey, m, TTEntry.T_GE, ply, depth, evalScore); moveGen.returnMoveList(moves); if (log != null) log.logNodeEnd(sti.nodeIdx, alpha, TTEntry.T_GE, evalScore, hKey); return alpha; } b = alpha + 1; } if (!haveLegalMoves && !inCheck) { moveGen.returnMoveList(moves); if (log != null) log.logNodeEnd(sti.nodeIdx, 0, TTEntry.T_EXACT, evalScore, hKey); return 0; // Stale-mate } if (bestMove >= 0) { tt.insert(hKey, moves.m[bestMove], TTEntry.T_EXACT, ply, depth, evalScore); if (log != null) log.logNodeEnd(sti.nodeIdx, bestScore, TTEntry.T_EXACT, evalScore, hKey); } else { emptyMove.score = bestScore; tt.insert(hKey, emptyMove, TTEntry.T_LE, ply, depth, evalScore); if (log != null) log.logNodeEnd(sti.nodeIdx, bestScore, TTEntry.T_LE, evalScore, hKey); } moveGen.returnMoveList(moves); return bestScore; } /** Return true if move m2 was made possible by move m1. */ private final boolean relatedMoves(Move m1, Move m2) { if ((m1.from == m1.to) || (m2.from == m2.to)) return false; if ((m1.to == m2.from) || (m1.from == m2.to) || ((BitBoard.squaresBetween[m2.from][m2.to] & (1L << m1.from)) != 0)) return true; return false; } /** Return true if move should be skipped in order to make engine play weaker. */ private final boolean weakPlaySkipMove(Position pos, Move m, int ply) { long rndL = pos.zobristHash() ^ Position.psHashKeys[0][m.from] ^ Position.psHashKeys[0][m.to] ^ randomSeed; double rnd = ((rndL & 0x7fffffffffffffffL) % 1000000000) / 1e9; double s = strength * 1e-3; double offs = (17 - 50 * s) / 3; double effPly = ply * Evaluate.interpolate(pos.wMtrl + pos.bMtrl, 0, 30, Evaluate.qV * 4, 100) * 1e-2; double t = effPly + offs; double p = 1/(1+Math.exp(t)); // Probability to "see" move boolean easyMove = ((pos.getPiece(m.to) != Piece.EMPTY) || (ply < 2) || (searchTreeInfo[ply-2].currentMove.to == m.from)); if (easyMove) p = 1-(1-p)*(1-p); if (rnd > p) return true; return false; } private static final boolean passedPawnPush(Position pos, Move m) { int p = pos.getPiece(m.from); if (pos.whiteMove) { if (p != Piece.WPAWN) return false; if ((BitBoard.wPawnBlockerMask[m.to] & pos.pieceTypeBB[Piece.BPAWN]) != 0) return false; return m.to >= 40; } else { if (p != Piece.BPAWN) return false; if ((BitBoard.bPawnBlockerMask[m.to] & pos.pieceTypeBB[Piece.WPAWN]) != 0) return false; return m.to <= 23; } } /** * Quiescence search. Only non-losing captures are searched. */ final private int quiesce(int alpha, int beta, int ply, int depth, final boolean inCheck) { int score; if (inCheck) { score = -(MATE0 - (ply+1)); } else { if ((depth == 0) && (q0Eval != UNKNOWN_SCORE)) { score = q0Eval; } else { score = eval.evalPos(pos); if (depth == 0) q0Eval = score; } } if (score >= beta) { if ((depth == 0) && (score < MATE0 - ply)) { if (MoveGen.canTakeKing(pos)) { // To make stale-mate detection work score = MATE0 - ply; } } return score; } final int evalScore = score; if (score > alpha) alpha = score; int bestScore = score; final boolean tryChecks = (depth > -1); MoveGen.MoveList moves; if (inCheck) { moves = moveGen.checkEvasions(pos); } else if (tryChecks) { moves = moveGen.pseudoLegalCapturesAndChecks(pos); } else { moves = moveGen.pseudoLegalCaptures(pos); } scoreMoveListMvvLva(moves); UndoInfo ui = searchTreeInfo[ply].undoInfo; for (int mi = 0; mi < moves.size; mi++) { if (mi < 8) { // If the first 8 moves didn't fail high, this is probably an ALL-node, // so spending more effort on move ordering is probably wasted time. selectBest(moves, mi); } Move m = moves.m[mi]; if (pos.getPiece(m.to) == (pos.whiteMove ? Piece.BKING : Piece.WKING)) { moveGen.returnMoveList(moves); return MATE0-ply; // King capture } boolean givesCheck = false; boolean givesCheckComputed = false; if (inCheck) { // Allow all moves } else { if ((pos.getPiece(m.to) == Piece.EMPTY) && (m.promoteTo == Piece.EMPTY)) { // Non-capture if (!tryChecks) continue; givesCheck = MoveGen.givesCheck(pos, m); givesCheckComputed = true; if (!givesCheck) continue; if (negSEE(m)) // Needed because m.score is not computed for non-captures continue; } else { if (negSEE(m)) continue; int capt = Evaluate.pieceValue[pos.getPiece(m.to)]; int prom = Evaluate.pieceValue[m.promoteTo]; int optimisticScore = evalScore + capt + prom + 200; if (optimisticScore < alpha) { // Delta pruning if ((pos.wMtrlPawns > 0) && (pos.wMtrl > capt + pos.wMtrlPawns) && (pos.bMtrlPawns > 0) && (pos.bMtrl > capt + pos.bMtrlPawns)) { if (depth -1 > -2) { givesCheck = MoveGen.givesCheck(pos, m); givesCheckComputed = true; } if (!givesCheck) { if (optimisticScore > bestScore) bestScore = optimisticScore; continue; } } } } } if (!givesCheckComputed) { if (depth - 1 > -2) { givesCheck = MoveGen.givesCheck(pos, m); } } final boolean nextInCheck = (depth - 1) > -2 ? givesCheck : false; pos.makeMove(m, ui); qNodes++; totalNodes++; score = -quiesce(-beta, -alpha, ply + 1, depth - 1, nextInCheck); pos.unMakeMove(m, ui); if (score > bestScore) { bestScore = score; if (score > alpha) { if (depth == 0) { SearchTreeInfo sti = searchTreeInfo[ply]; sti.bestMove.setMove(m.from, m.to, m.promoteTo, score); } alpha = score; if (alpha >= beta) { moveGen.returnMoveList(moves); return alpha; } } } } moveGen.returnMoveList(moves); return bestScore; } /** Return >0, 0, <0, depending on the sign of SEE(m). */ final public int signSEE(Move m) { int p0 = Evaluate.pieceValue[pos.getPiece(m.from)]; int p1 = Evaluate.pieceValue[pos.getPiece(m.to)]; if (p0 < p1) return 1; return SEE(m); } /** Return true if SEE(m) < 0. */ final public boolean negSEE(Move m) { int p0 = Evaluate.pieceValue[pos.getPiece(m.from)]; int p1 = Evaluate.pieceValue[pos.getPiece(m.to)]; if (p1 >= p0) return false; return SEE(m) < 0; } private int[] captures = new int[64]; // Value of captured pieces private UndoInfo seeUi = new UndoInfo(); /** * Static exchange evaluation function. * @return SEE score for m. Positive value is good for the side that makes the first move. */ final public int SEE(Move m) { final int kV = Evaluate.kV; final int square = m.to; if (square == pos.getEpSquare()) { captures[0] = Evaluate.pV; } else { captures[0] = Evaluate.pieceValue[pos.getPiece(square)]; if (captures[0] == kV) return kV; } int nCapt = 1; // Number of entries in captures[] pos.makeSEEMove(m, seeUi); boolean white = pos.whiteMove; int valOnSquare = Evaluate.pieceValue[pos.getPiece(square)]; long occupied = pos.whiteBB | pos.blackBB; while (true) { int bestValue = Integer.MAX_VALUE; long atk; if (white) { atk = BitBoard.bPawnAttacks[square] & pos.pieceTypeBB[Piece.WPAWN] & occupied; if (atk != 0) { bestValue = Evaluate.pV; } else { atk = BitBoard.knightAttacks[square] & pos.pieceTypeBB[Piece.WKNIGHT] & occupied; if (atk != 0) { bestValue = Evaluate.nV; } else { long bAtk = BitBoard.bishopAttacks(square, occupied) & occupied; atk = bAtk & pos.pieceTypeBB[Piece.WBISHOP]; if (atk != 0) { bestValue = Evaluate.bV; } else { long rAtk = BitBoard.rookAttacks(square, occupied) & occupied; atk = rAtk & pos.pieceTypeBB[Piece.WROOK]; if (atk != 0) { bestValue = Evaluate.rV; } else { atk = (bAtk | rAtk) & pos.pieceTypeBB[Piece.WQUEEN]; if (atk != 0) { bestValue = Evaluate.qV; } else { atk = BitBoard.kingAttacks[square] & pos.pieceTypeBB[Piece.WKING] & occupied; if (atk != 0) { bestValue = kV; } else { break; } } } } } } } else { atk = BitBoard.wPawnAttacks[square] & pos.pieceTypeBB[Piece.BPAWN] & occupied; if (atk != 0) { bestValue = Evaluate.pV; } else { atk = BitBoard.knightAttacks[square] & pos.pieceTypeBB[Piece.BKNIGHT] & occupied; if (atk != 0) { bestValue = Evaluate.nV; } else { long bAtk = BitBoard.bishopAttacks(square, occupied) & occupied; atk = bAtk & pos.pieceTypeBB[Piece.BBISHOP]; if (atk != 0) { bestValue = Evaluate.bV; } else { long rAtk = BitBoard.rookAttacks(square, occupied) & occupied; atk = rAtk & pos.pieceTypeBB[Piece.BROOK]; if (atk != 0) { bestValue = Evaluate.rV; } else { atk = (bAtk | rAtk) & pos.pieceTypeBB[Piece.BQUEEN]; if (atk != 0) { bestValue = Evaluate.qV; } else { atk = BitBoard.kingAttacks[square] & pos.pieceTypeBB[Piece.BKING] & occupied; if (atk != 0) { bestValue = kV; } else { break; } } } } } } } captures[nCapt++] = valOnSquare; if (valOnSquare == kV) break; valOnSquare = bestValue; occupied &= ~(atk & -atk); white = !white; } pos.unMakeSEEMove(m, seeUi); int score = 0; for (int i = nCapt - 1; i > 0; i--) { score = Math.max(0, captures[i] - score); } return captures[0] - score; } /** * Compute scores for each move in a move list, using SEE, killer and history information. * @param moves List of moves to score. */ final void scoreMoveList(MoveGen.MoveList moves, int ply) { scoreMoveList(moves, ply, 0); } final void scoreMoveList(MoveGen.MoveList moves, int ply, int startIdx) { for (int i = startIdx; i < moves.size; i++) { Move m = moves.m[i]; boolean isCapture = (pos.getPiece(m.to) != Piece.EMPTY) || (m.promoteTo != Piece.EMPTY); int score = 0; if (isCapture) { int seeScore = isCapture ? signSEE(m) : 0; int v = pos.getPiece(m.to); int a = pos.getPiece(m.from); score = Evaluate.pieceValue[v]/10 * 1000 - Evaluate.pieceValue[a]/10; if (seeScore > 0) score += 2000000; else if (seeScore == 0) score += 1000000; else score -= 1000000; score *= 100; } int ks = kt.getKillerScore(ply, m); if (ks > 0) { score += ks + 50; } else { int hs = ht.getHistScore(pos, m); score += hs; } m.score = score; } } private final void scoreMoveListMvvLva(MoveGen.MoveList moves) { for (int i = 0; i < moves.size; i++) { Move m = moves.m[i]; int v = pos.getPiece(m.to); int a = pos.getPiece(m.from); m.score = Evaluate.pieceValue[v] * 10000 - Evaluate.pieceValue[a]; } } /** * Find move with highest score and move it to the front of the list. */ final static void selectBest(MoveGen.MoveList moves, int startIdx) { int bestIdx = startIdx; int bestScore = moves.m[bestIdx].score; for (int i = startIdx + 1; i < moves.size; i++) { int sc = moves.m[i].score; if (sc > bestScore) { bestIdx = i; bestScore = sc; } } if (bestIdx != startIdx) { Move m = moves.m[startIdx]; moves.m[startIdx] = moves.m[bestIdx]; moves.m[bestIdx] = m; } } /** If hashMove exists in the move list, move the hash move to the front of the list. */ final static boolean selectHashMove(MoveGen.MoveList moves, Move hashMove) { if (hashMove == null) { return false; } for (int i = 0; i < moves.size; i++) { Move m = moves.m[i]; if (m.equals(hashMove)) { moves.m[i] = moves.m[0]; moves.m[0] = m; m.score = 10000; return true; } } return false; } public final static boolean canClaimDraw50(Position pos) { return (pos.halfMoveClock >= 100); } public final static boolean canClaimDrawRep(Position pos, long[] posHashList, int posHashListSize, int posHashFirstNew) { int reps = 0; for (int i = posHashListSize - 4; i >= 0; i -= 2) { if (pos.zobristHash() == posHashList[i]) { reps++; if (i >= posHashFirstNew) { reps++; break; } } } return (reps >= 2); } private final void initNodeStats() { nodes = qNodes = 0; nodesPlyVec = new int[20]; nodesDepthVec = new int[20]; for (int i = 0; i < 20; i++) { nodesPlyVec[i] = 0; nodesDepthVec[i] = 0; } } }