diff --git a/DroidFish/jni/stockfish/benchmark.cpp b/DroidFish/jni/stockfish/benchmark.cpp index 8f3e6ae..33e2668 100644 --- a/DroidFish/jni/stockfish/benchmark.cpp +++ b/DroidFish/jni/stockfish/benchmark.cpp @@ -91,8 +91,8 @@ const vector Defaults = { void benchmark(const Position& current, istream& is) { string token; - Search::LimitsType limits; vector fens; + Search::LimitsType limits; // Assign default values to missing arguments string ttSize = (is >> token) ? token : "16"; @@ -103,10 +103,10 @@ void benchmark(const Position& current, istream& is) { Options["Hash"] = ttSize; Options["Threads"] = threads; - Search::reset(); + Search::clear(); if (limitType == "time") - limits.movetime = stoi(limit); // movetime is in ms + limits.movetime = stoi(limit); // movetime is in millisecs else if (limitType == "nodes") limits.nodes = stoi(limit); @@ -151,14 +151,14 @@ void benchmark(const Position& current, istream& is) { cerr << "\nPosition: " << i + 1 << '/' << fens.size() << endl; if (limitType == "perft") - nodes += Search::perft(pos, limits.depth * ONE_PLY); + nodes += Search::perft(pos, limits.depth * ONE_PLY); else { Search::StateStackPtr st; limits.startTime = now(); Threads.start_thinking(pos, limits, st); - Threads.main()->join(); + Threads.main()->wait_for_search_finished(); nodes += Threads.nodes_searched(); } } diff --git a/DroidFish/jni/stockfish/endgame.cpp b/DroidFish/jni/stockfish/endgame.cpp index 97e7e12..b64b3d1 100644 --- a/DroidFish/jni/stockfish/endgame.cpp +++ b/DroidFish/jni/stockfish/endgame.cpp @@ -59,6 +59,9 @@ namespace { const int PushClose[8] = { 0, 0, 100, 80, 60, 40, 20, 10 }; const int PushAway [8] = { 0, 5, 20, 40, 60, 80, 90, 100 }; + // Pawn Rank based scaling factors used in KRPPKRP endgame + const int KRPPKRPScaleFactors[RANK_NB] = { 0, 9, 10, 14, 21, 44, 0, 0 }; + #ifndef NDEBUG bool verify_material(const Position& pos, Color c, Value npm, int pawnsCnt) { return pos.non_pawn_material(c) == npm && pos.count(c) == pawnsCnt; @@ -600,14 +603,8 @@ ScaleFactor Endgame::operator()(const Position& pos) const { && distance(bksq, wpsq2) <= 1 && relative_rank(strongSide, bksq) > r) { - switch (r) { - case RANK_2: return ScaleFactor(9); - case RANK_3: return ScaleFactor(10); - case RANK_4: return ScaleFactor(14); - case RANK_5: return ScaleFactor(21); - case RANK_6: return ScaleFactor(44); - default: assert(false); - } + assert(r > RANK_1 && r < RANK_7); + return ScaleFactor(KRPPKRPScaleFactors[r]); } return SCALE_FACTOR_NONE; } diff --git a/DroidFish/jni/stockfish/evaluate.cpp b/DroidFish/jni/stockfish/evaluate.cpp index 44a0669..925d70a 100644 --- a/DroidFish/jni/stockfish/evaluate.cpp +++ b/DroidFish/jni/stockfish/evaluate.cpp @@ -102,15 +102,16 @@ namespace { int kingAdjacentZoneAttacksCount[COLOR_NB]; Bitboard pinnedPieces[COLOR_NB]; + Material::Entry* me; Pawns::Entry* pi; }; // Evaluation weights, indexed by the corresponding evaluation term - enum { Mobility, PawnStructure, PassedPawns, Space, KingSafety }; + enum { PawnStructure, PassedPawns, Space, KingSafety }; const struct Weight { int mg, eg; } Weights[] = { - {289, 344}, {233, 201}, {221, 273}, {46, 0}, {322, 0} + {214, 203}, {193, 262}, {47, 0}, {330, 0} }; Score operator*(Score s, const Weight& w) { @@ -122,23 +123,22 @@ namespace { #define S(mg, eg) make_score(mg, eg) // MobilityBonus[PieceType][attacked] contains bonuses for middle and end - // game, indexed by piece type and number of attacked squares not occupied by - // friendly pieces. + // game, indexed by piece type and number of attacked squares in the MobilityArea. const Score MobilityBonus[][32] = { {}, {}, - { S(-70,-52), S(-52,-37), S( -7,-17), S( 0, -6), S( 8, 5), S( 16, 9), // Knights - S( 23, 20), S( 31, 21), S( 36, 22) }, - { S(-49,-44), S(-22,-13), S( 16, 0), S( 27, 11), S( 38, 19), S( 52, 34), // Bishops - S( 56, 44), S( 65, 47), S( 67, 51), S( 73, 56), S( 81, 59), S( 83, 69), - S( 95, 72), S(100, 75) }, - { S(-49,-57), S(-22,-14), S(-10, 18), S( -5, 39), S( -4, 50), S( -2, 58), // Rooks - S( 6, 78), S( 11, 86), S( 17, 92), S( 19,103), S( 26,111), S( 27,115), - S( 36,119), S( 41,121), S( 50,122) }, - { S(-41,-24), S(-26, -8), S( 0, 6), S( 2, 14), S( 12, 27), S( 21, 40), // Queens - S( 22, 45), S( 37, 55), S( 40, 57), S( 43, 63), S( 50, 68), S( 52, 74), - S( 56, 80), S( 66, 84), S( 68, 85), S( 69, 88), S( 71, 92), S( 72, 94), - S( 80, 96), S( 89, 98), S( 94,101), S(102,113), S(106,114), S(107,116), - S(112,125), S(113,127), S(117,137), S(122,143) } + { S(-75,-76), S(-56,-54), S(- 9,-26), S( -2,-10), S( 6, 5), S( 15, 11), // Knights + S( 22, 26), S( 30, 28), S( 36, 29) }, + { S(-48,-58), S(-21,-19), S( 16, -2), S( 26, 12), S( 37, 22), S( 51, 42), // Bishops + S( 54, 54), S( 63, 58), S( 65, 63), S( 71, 70), S( 79, 74), S( 81, 86), + S( 92, 90), S( 97, 94) }, + { S(-56,-78), S(-25,-18), S(-11, 26), S( -5, 55), S( -4, 70), S( -1, 81), // Rooks + S( 8,109), S( 14,120), S( 21,128), S( 23,143), S( 31,154), S( 32,160), + S( 43,165), S( 49,168), S( 59,169) }, + { S(-40,-35), S(-25,-12), S( 2, 7), S( 4, 19), S( 14, 37), S( 24, 55), // Queens + S( 25, 62), S( 40, 76), S( 43, 79), S( 47, 87), S( 54, 94), S( 56,102), + S( 60,111), S( 70,116), S( 72,118), S( 73,122), S( 75,128), S( 77,130), + S( 85,133), S( 94,136), S( 99,140), S(108,157), S(112,158), S(113,161), + S(118,174), S(119,177), S(123,191), S(128,199) } }; // Outpost[knight/bishop][supported by pawn] contains bonuses for knights and @@ -148,22 +148,29 @@ namespace { { S(18, 5), S(27, 8) } // Bishops }; - // Threat[defended/weak][minor/rook attacking][attacked PieceType] contains + // ReachableOutpost[knight/bishop][supported by pawn] contains bonuses for + // knights and bishops which can reach an outpost square in one move, bigger + // if outpost square is supported by a pawn. + const Score ReachableOutpost[][2] = { + { S(21, 5), S(31, 8) }, // Knights + { S( 8, 2), S(13, 4) } // Bishops + }; + + // Threat[minor/rook][attacked PieceType] contains // bonuses according to which piece type attacks which one. - const Score Threat[][2][PIECE_TYPE_NB] = { - { { S(0, 0), S( 0, 0), S(19, 37), S(24, 37), S(44, 97), S(35,106) }, // Minor on Defended - { S(0, 0), S( 0, 0), S( 9, 14), S( 9, 14), S( 7, 14), S(24, 48) } }, // Rook on Defended - { { S(0, 0), S( 0,32), S(33, 41), S(31, 50), S(41,100), S(35,104) }, // Minor on Weak - { S(0, 0), S( 0,27), S(26, 57), S(26, 57), S(0 , 43), S(23, 51) } } // Rook on Weak + // Attacks on lesser pieces which are pawn defended are not considered. + const Score Threat[2][PIECE_TYPE_NB] = { + { S(0, 0), S(0, 33), S(45, 43), S(46, 47), S(72, 107), S(48,118) }, // Minor attacks + { S(0, 0), S(0, 25), S(40, 62), S(40, 59), S( 0, 34), S(35, 48) } // Rook attacks }; // ThreatenedByPawn[PieceType] contains a penalty according to which piece - // type is attacked by an enemy pawn. + // type is attacked by a pawn. const Score ThreatenedByPawn[PIECE_TYPE_NB] = { - S(0, 0), S(0, 0), S(107, 138), S(84, 122), S(114, 203), S(121, 217) + S(0, 0), S(0, 0), S(176, 139), S(131, 127), S(217, 218), S(203, 215) }; - // Passed[mg/eg][rank] contains midgame and endgame bonuses for passed pawns. + // Passed[mg/eg][Rank] contains midgame and endgame bonuses for passed pawns. // We don't use a Score because we process the two components independently. const Value Passed[][RANK_NB] = { { V(0), V( 1), V(34), V(90), V(214), V(328) }, @@ -172,15 +179,15 @@ namespace { // PassedFile[File] contains a bonus according to the file of a passed pawn. const Score PassedFile[] = { - S( 12, 10), S( 3, 10), S( 1, -8), S(-27, -12), - S(-27, -12), S( 1, -8), S( 3, 10), S( 12, 10) + S( 12, 10), S( 3, 10), S( 1, -8), S(-27, -12), + S(-27, -12), S( 1, -8), S( 3, 10), S( 12, 10) }; - const Score ThreatenedByHangingPawn = S(40, 60); + const Score ThreatenedByHangingPawn = S(70, 63); // Assorted bonuses and penalties used by evaluation - const Score KingOnOne = S( 2, 58); - const Score KingOnMany = S( 6,125); + const Score KingOnOne = S( 3, 62); + const Score KingOnMany = S( 9,138); const Score RookOnPawn = S( 7, 27); const Score RookOnOpenFile = S(43, 21); const Score RookOnSemiOpenFile = S(19, 10); @@ -188,8 +195,8 @@ namespace { const Score MinorBehindPawn = S(16, 0); const Score TrappedRook = S(92, 0); const Score Unstoppable = S( 0, 20); - const Score Hanging = S(31, 26); - const Score PawnAttackThreat = S(20, 20); + const Score Hanging = S(48, 28); + const Score PawnAttackThreat = S(31, 19); const Score Checked = S(20, 20); // Penalty for a bishop on a1/h1 (a8/h8 for black) which is trapped by @@ -200,15 +207,6 @@ namespace { #undef S #undef V - // SpaceMask[Color] contains the area of the board which is considered - // by the space evaluation. In the middlegame, each side is given a bonus - // based on how many squares inside this area are safe and available for - // friendly minor pieces. - const Bitboard SpaceMask[COLOR_NB] = { - (FileCBB | FileDBB | FileEBB | FileFBB) & (Rank2BB | Rank3BB | Rank4BB), - (FileCBB | FileDBB | FileEBB | FileFBB) & (Rank7BB | Rank6BB | Rank5BB) - }; - // King danger constants and variables. The king danger scores are looked-up // in KingDanger[]. Various little "meta-bonuses" measuring the strength // of the enemy attack are added up into an integer, which is used as an @@ -226,11 +224,11 @@ namespace { const int KnightCheck = 14; - // init_eval_info() initializes king bitboards for given color adding - // pawn attacks. To be done at the beginning of the evaluation. + // eval_init() initializes king and attack bitboards for given color + // adding pawn attacks. To be done at the beginning of the evaluation. template - void init_eval_info(const Position& pos, EvalInfo& ei) { + void eval_init(const Position& pos, EvalInfo& ei) { const Color Them = (Us == WHITE ? BLACK : WHITE); const Square Down = (Us == WHITE ? DELTA_S : DELTA_N); @@ -253,17 +251,20 @@ namespace { } - // evaluate_pieces() assigns bonuses and penalties to the pieces of a given color + // evaluate_pieces() assigns bonuses and penalties to the pieces of a given + // color and type. - template - Score evaluate_pieces(const Position& pos, EvalInfo& ei, Score* mobility, const Bitboard* mobilityArea) { - - Bitboard b; + template + Score evaluate_pieces(const Position& pos, EvalInfo& ei, Score* mobility, + const Bitboard* mobilityArea) { + Bitboard b, bb; Square s; Score score = SCORE_ZERO; const PieceType NextPt = (Us == WHITE ? Pt : PieceType(Pt + 1)); const Color Them = (Us == WHITE ? BLACK : WHITE); + const Bitboard OutpostRanks = (Us == WHITE ? Rank4BB | Rank5BB | Rank6BB + : Rank5BB | Rank4BB | Rank3BB); const Square* pl = pos.squares(Us); ei.attackedBy[Us][Pt] = 0; @@ -284,7 +285,7 @@ namespace { { ei.kingAttackersCount[Us]++; ei.kingAttackersWeight[Us] += KingAttackWeights[Pt]; - Bitboard bb = b & ei.attackedBy[Them][KING]; + bb = b & ei.attackedBy[Them][KING]; if (bb) ei.kingAdjacentZoneAttacksCount[Us] += popcount(bb); } @@ -300,11 +301,16 @@ namespace { if (Pt == BISHOP || Pt == KNIGHT) { - // Bonus for outpost square - if ( relative_rank(Us, s) >= RANK_4 - && relative_rank(Us, s) <= RANK_6 - && !(pos.pieces(Them, PAWN) & pawn_attack_span(Us, s))) + // Bonus for outpost squares + bb = OutpostRanks & ~ei.pi->pawn_attacks_span(Them); + if (bb & s) score += Outpost[Pt == BISHOP][!!(ei.attackedBy[Us][PAWN] & s)]; + else + { + bb &= b & ~pos.pieces(Us); + if (bb) + score += ReachableOutpost[Pt == BISHOP][!!(ei.attackedBy[Us][PAWN] & bb)]; + } // Bonus when behind a pawn if ( relative_rank(Us, s) < RANK_5 @@ -361,13 +367,13 @@ namespace { Trace::add(Pt, Us, score); // Recursively call evaluate_pieces() of next piece type until KING excluded - return score - evaluate_pieces(pos, ei, mobility, mobilityArea); + return score - evaluate_pieces(pos, ei, mobility, mobilityArea); } template<> - Score evaluate_pieces(const Position&, EvalInfo&, Score*, const Bitboard*) { return SCORE_ZERO; } + Score evaluate_pieces(const Position&, EvalInfo&, Score*, const Bitboard*) { return SCORE_ZERO; } template<> - Score evaluate_pieces(const Position&, EvalInfo&, Score*, const Bitboard*) { return SCORE_ZERO; } + Score evaluate_pieces< true, WHITE, KING>(const Position&, EvalInfo&, Score*, const Bitboard*) { return SCORE_ZERO; } // evaluate_king() assigns bonuses and penalties to a king of a given color @@ -388,7 +394,7 @@ namespace { if (ei.kingAttackersCount[Them]) { // Find the attacked squares around the king which have no defenders - // apart from the king itself + // apart from the king itself. undefended = ei.attackedBy[Them][ALL_PIECES] & ei.attackedBy[Us][KING] & ~( ei.attackedBy[Us][PAWN] | ei.attackedBy[Us][KNIGHT] @@ -484,7 +490,6 @@ namespace { const Bitboard TRank2BB = (Us == WHITE ? Rank2BB : Rank7BB); const Bitboard TRank7BB = (Us == WHITE ? Rank7BB : Rank2BB); - enum { Defended, Weak }; enum { Minor, Rook }; Bitboard b, weak, defended, safeThreats; @@ -510,33 +515,21 @@ namespace { // Non-pawn enemies defended by a pawn defended = (pos.pieces(Them) ^ pos.pieces(Them, PAWN)) & ei.attackedBy[Them][PAWN]; - // Add a bonus according to the kind of attacking pieces - if (defended) - { - b = defended & (ei.attackedBy[Us][KNIGHT] | ei.attackedBy[Us][BISHOP]); - while (b) - score += Threat[Defended][Minor][type_of(pos.piece_on(pop_lsb(&b)))]; - - b = defended & ei.attackedBy[Us][ROOK]; - while (b) - score += Threat[Defended][Rook][type_of(pos.piece_on(pop_lsb(&b)))]; - } - // Enemies not defended by a pawn and under our attack weak = pos.pieces(Them) & ~ei.attackedBy[Them][PAWN] & ei.attackedBy[Us][ALL_PIECES]; // Add a bonus according to the kind of attacking pieces - if (weak) + if (defended | weak) { - b = weak & (ei.attackedBy[Us][KNIGHT] | ei.attackedBy[Us][BISHOP]); + b = (defended | weak) & (ei.attackedBy[Us][KNIGHT] | ei.attackedBy[Us][BISHOP]); while (b) - score += Threat[Weak][Minor][type_of(pos.piece_on(pop_lsb(&b)))]; + score += Threat[Minor][type_of(pos.piece_on(pop_lsb(&b)))]; - b = weak & ei.attackedBy[Us][ROOK]; + b = (pos.pieces(Them, QUEEN) | weak) & ei.attackedBy[Us][ROOK]; while (b) - score += Threat[Weak][Rook][type_of(pos.piece_on(pop_lsb(&b)))]; + score += Threat[Rook ][type_of(pos.piece_on(pop_lsb(&b)))]; b = weak & ~ei.attackedBy[Them][ALL_PIECES]; if (b) @@ -662,11 +655,14 @@ namespace { Score evaluate_space(const Position& pos, const EvalInfo& ei) { const Color Them = (Us == WHITE ? BLACK : WHITE); + const Bitboard SpaceMask = + Us == WHITE ? (FileCBB | FileDBB | FileEBB | FileFBB) & (Rank2BB | Rank3BB | Rank4BB) + : (FileCBB | FileDBB | FileEBB | FileFBB) & (Rank7BB | Rank6BB | Rank5BB); // Find the safe squares for our pieces inside the area defined by - // SpaceMask[]. A square is unsafe if it is attacked by an enemy + // SpaceMask. A square is unsafe if it is attacked by an enemy // pawn, or if it is undefended and attacked by an enemy piece. - Bitboard safe = SpaceMask[Us] + Bitboard safe = SpaceMask & ~pos.pieces(Us, PAWN) & ~ei.attackedBy[Them][PAWN] & (ei.attackedBy[Us][ALL_PIECES] | ~ei.attackedBy[Them][ALL_PIECES]); @@ -688,31 +684,66 @@ namespace { } - // evaluate_initiative() computes the initiative correction value for the position, i.e. - // second order bonus/malus based on the known attacking/defending status of the players. - Score evaluate_initiative(const Position& pos, const EvalInfo& ei, const Score positional_score) { + // evaluate_initiative() computes the initiative correction value for the + // position, i.e. second order bonus/malus based on the known attacking/defending + // status of the players. + Score evaluate_initiative(const Position& pos, int asymmetry, Value eg) { - int pawns = pos.count(WHITE) + pos.count(BLACK); - int king_separation = distance(pos.square(WHITE), pos.square(BLACK)); - int asymmetry = ei.pi->pawn_asymmetry(); + int kingDistance = distance(pos.square(WHITE), pos.square(BLACK)); + int pawns = pos.count(WHITE) + pos.count(BLACK); // Compute the initiative bonus for the attacking side - int attacker_bonus = 8 * (pawns + asymmetry + king_separation) - 120; + int initiative = 8 * (pawns + asymmetry + kingDistance - 15); - // Now apply the bonus: note that we find the attacking side by extracting the sign - // of the endgame value of "positional_score", and that we carefully cap the bonus so - // that the endgame score with the correction will never be divided by more than two. - int eg = eg_value(positional_score); - int value = ((eg > 0) - (eg < 0)) * std::max( attacker_bonus , -abs( eg / 2 ) ); + // Now apply the bonus: note that we find the attacking side by extracting + // the sign of the endgame value, and that we carefully cap the bonus so + // that the endgame score will never be divided by more than two. + int value = ((eg > 0) - (eg < 0)) * std::max(initiative, -abs(eg / 2)); - return make_score( 0 , value ) ; + return make_score(0, value); + } + + + // evaluate_scale_factor() computes the scale factor for the winning side + ScaleFactor evaluate_scale_factor(const Position& pos, const EvalInfo& ei, Score score) { + + Color strongSide = eg_value(score) > VALUE_DRAW ? WHITE : BLACK; + ScaleFactor sf = ei.me->scale_factor(pos, strongSide); + + // If we don't already have an unusual scale factor, check for certain + // types of endgames, and use a lower scale for those. + if ( ei.me->game_phase() < PHASE_MIDGAME + && (sf == SCALE_FACTOR_NORMAL || sf == SCALE_FACTOR_ONEPAWN)) + { + if (pos.opposite_bishops()) + { + // Endgame with opposite-colored bishops and no other pieces (ignoring pawns) + // is almost a draw, in case of KBP vs KB is even more a draw. + if ( pos.non_pawn_material(WHITE) == BishopValueMg + && pos.non_pawn_material(BLACK) == BishopValueMg) + sf = more_than_one(pos.pieces(PAWN)) ? ScaleFactor(31) : ScaleFactor(9); + + // Endgame with opposite-colored bishops, but also other pieces. Still + // a bit drawish, but not as drawish as with only the two bishops. + else + sf = ScaleFactor(46 * sf / SCALE_FACTOR_NORMAL); + } + // Endings where weaker side can place his king in front of the opponent's + // pawns are drawish. + else if ( abs(eg_value(score)) <= BishopValueEg + && ei.pi->pawn_span(strongSide) <= 1 + && !pos.pawn_passed(~strongSide, pos.square(~strongSide))) + sf = ei.pi->pawn_span(strongSide) ? ScaleFactor(51) : ScaleFactor(37); + } + + return sf; } } // namespace /// evaluate() is the main evaluation function. It returns a static evaluation -/// of the position always from the point of view of the side to move. +/// of the position from the point of view of the side to move. template Value Eval::evaluate(const Position& pos) { @@ -720,21 +751,21 @@ Value Eval::evaluate(const Position& pos) { assert(!pos.checkers()); EvalInfo ei; - Score score, mobility[2] = { SCORE_ZERO, SCORE_ZERO }; + Score score, mobility[COLOR_NB] = { SCORE_ZERO, SCORE_ZERO }; - // Initialize score by reading the incrementally updated scores included - // in the position object (material + piece square tables). - // Score is computed from the point of view of white. + // Initialize score by reading the incrementally updated scores included in + // the position object (material + piece square tables). Score is computed + // internally from the white point of view. score = pos.psq_score(); // Probe the material hash table - Material::Entry* me = Material::probe(pos); - score += me->imbalance(); + ei.me = Material::probe(pos); + score += ei.me->imbalance(); // If we have a specialized evaluation function for the current material // configuration, call it and return. - if (me->specialized_eval_exists()) - return me->evaluate(pos); + if (ei.me->specialized_eval_exists()) + return ei.me->evaluate(pos); // Probe the pawn hash table ei.pi = Pawns::probe(pos); @@ -742,27 +773,27 @@ Value Eval::evaluate(const Position& pos) { // Initialize attack and king safety bitboards ei.attackedBy[WHITE][ALL_PIECES] = ei.attackedBy[BLACK][ALL_PIECES] = 0; - init_eval_info(pos, ei); - init_eval_info(pos, ei); + eval_init(pos, ei); + eval_init(pos, ei); - // Pawns blocked or on ranks 2 and 3. Will be excluded from the mobility area + // Pawns blocked or on ranks 2 and 3 will be excluded from the mobility area Bitboard blockedPawns[] = { pos.pieces(WHITE, PAWN) & (shift_bb(pos.pieces()) | Rank2BB | Rank3BB), pos.pieces(BLACK, PAWN) & (shift_bb(pos.pieces()) | Rank7BB | Rank6BB) }; - // Do not include in mobility squares protected by enemy pawns, or occupied + // Do not include in mobility area squares protected by enemy pawns, or occupied // by our blocked pawns or king. Bitboard mobilityArea[] = { ~(ei.attackedBy[BLACK][PAWN] | blockedPawns[WHITE] | pos.square(WHITE)), ~(ei.attackedBy[WHITE][PAWN] | blockedPawns[BLACK] | pos.square(BLACK)) }; - // Evaluate pieces and mobility - score += evaluate_pieces(pos, ei, mobility, mobilityArea); - score += (mobility[WHITE] - mobility[BLACK]) * Weights[Mobility]; + // Evaluate all pieces but king and pawns + score += evaluate_pieces(pos, ei, mobility, mobilityArea); + score += mobility[WHITE] - mobility[BLACK]; - // Evaluate kings after all other pieces because we need complete attack + // Evaluate kings after all other pieces because we need full attack // information when computing the king safety evaluation. score += evaluate_king(pos, ei) - evaluate_king(pos, ei); @@ -788,55 +819,28 @@ Value Eval::evaluate(const Position& pos) { // Evaluate space for both sides, only during opening if (pos.non_pawn_material(WHITE) + pos.non_pawn_material(BLACK) >= 12222) - score += (evaluate_space(pos, ei) - evaluate_space(pos, ei)) * Weights[Space]; - - // Evaluate initiative - score += evaluate_initiative(pos, ei, score); + score += ( evaluate_space(pos, ei) + - evaluate_space(pos, ei)) * Weights[Space]; - // Scale winning side if position is more drawish than it appears - Color strongSide = eg_value(score) > VALUE_DRAW ? WHITE : BLACK; - ScaleFactor sf = me->scale_factor(pos, strongSide); + // Evaluate position potential for the winning side + score += evaluate_initiative(pos, ei.pi->pawn_asymmetry(), eg_value(score)); - // If we don't already have an unusual scale factor, check for certain - // types of endgames, and use a lower scale for those. - if ( me->game_phase() < PHASE_MIDGAME - && (sf == SCALE_FACTOR_NORMAL || sf == SCALE_FACTOR_ONEPAWN)) - { - if (pos.opposite_bishops()) - { - // Endgame with opposite-colored bishops and no other pieces (ignoring pawns) - // is almost a draw, in case of KBP vs KB is even more a draw. - if ( pos.non_pawn_material(WHITE) == BishopValueMg - && pos.non_pawn_material(BLACK) == BishopValueMg) - sf = more_than_one(pos.pieces(PAWN)) ? ScaleFactor(31) : ScaleFactor(9); - - // Endgame with opposite-colored bishops, but also other pieces. Still - // a bit drawish, but not as drawish as with only the two bishops. - else - sf = ScaleFactor(46 * sf / SCALE_FACTOR_NORMAL); - } - // Endings where weaker side can place his king in front of the opponent's - // pawns are drawish. - else if ( abs(eg_value(score)) <= BishopValueEg - && ei.pi->pawn_span(strongSide) <= 1 - && !pos.pawn_passed(~strongSide, pos.square(~strongSide))) - sf = ei.pi->pawn_span(strongSide) ? ScaleFactor(51) : ScaleFactor(37); - } + // Evaluate scale factor for the winning side + ScaleFactor sf = evaluate_scale_factor(pos, ei, score); // Interpolate between a middlegame and a (scaled by 'sf') endgame score - Value v = mg_value(score) * int(me->game_phase()) - + eg_value(score) * int(PHASE_MIDGAME - me->game_phase()) * sf / SCALE_FACTOR_NORMAL; + Value v = mg_value(score) * int(ei.me->game_phase()) + + eg_value(score) * int(PHASE_MIDGAME - ei.me->game_phase()) * sf / SCALE_FACTOR_NORMAL; v /= int(PHASE_MIDGAME); - // In case of tracing add all single evaluation terms + // In case of tracing add all remaining individual evaluation terms if (DoTrace) { Trace::add(MATERIAL, pos.psq_score()); - Trace::add(IMBALANCE, me->imbalance()); + Trace::add(IMBALANCE, ei.me->imbalance()); Trace::add(PAWN, ei.pi->pawns_score()); - Trace::add(MOBILITY, mobility[WHITE] * Weights[Mobility] - , mobility[BLACK] * Weights[Mobility]); + Trace::add(MOBILITY, mobility[WHITE], mobility[BLACK]); Trace::add(SPACE, evaluate_space(pos, ei) * Weights[Space] , evaluate_space(pos, ei) * Weights[Space]); Trace::add(TOTAL, score); diff --git a/DroidFish/jni/stockfish/misc.cpp b/DroidFish/jni/stockfish/misc.cpp index 30b0427..54209f6 100644 --- a/DroidFish/jni/stockfish/misc.cpp +++ b/DroidFish/jni/stockfish/misc.cpp @@ -31,7 +31,7 @@ namespace { /// Version number. If Version is left empty, then compile date in the format /// DD-MM-YY and show in engine_info. -const string Version = "231015"; +const string Version = "7Beta1"; /// Our fancy logging facility. The trick here is to replace cin.rdbuf() and /// cout.rdbuf() with two Tie objects that tie cin and cout to a file stream. We @@ -39,7 +39,7 @@ const string Version = "231015"; /// usual I/O functionality, all without changing a single line of code! /// Idea from http://groups.google.com/group/comp.lang.c++/msg/1d941c0f26ea0d81 -struct Tie: public streambuf { // MSVC requires splitted streambuf for cin and cout +struct Tie: public streambuf { // MSVC requires split streambuf for cin and cout Tie(streambuf* b, streambuf* l) : buf(b), logBuf(l) {} diff --git a/DroidFish/jni/stockfish/misc.h b/DroidFish/jni/stockfish/misc.h index 203aec8..4181461 100644 --- a/DroidFish/jni/stockfish/misc.h +++ b/DroidFish/jni/stockfish/misc.h @@ -97,17 +97,17 @@ public: { return T(rand64() & rand64() & rand64()); } }; -inline int stoi(const std::string& s) { - std::stringstream ss(s); - int result = 0; - ss >> result; - return result; -} - -inline std::string to_string(int v) { - char buf[32]; - sprintf(buf, "%d", v); - return buf; -} +inline int stoi(const std::string& s) { + std::stringstream ss(s); + int result = 0; + ss >> result; + return result; +} + +inline std::string to_string(int v) { + std::stringstream ss; + ss << v; + return ss.str(); +} #endif // #ifndef MISC_H_INCLUDED diff --git a/DroidFish/jni/stockfish/movepick.cpp b/DroidFish/jni/stockfish/movepick.cpp index ed7c380..cad9d33 100644 --- a/DroidFish/jni/stockfish/movepick.cpp +++ b/DroidFish/jni/stockfish/movepick.cpp @@ -68,8 +68,8 @@ namespace { /// ordering is at the current node. MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const HistoryStats& h, - const CounterMovesHistoryStats& cmh, Move cm, Search::Stack* s) - : pos(p), history(h), counterMovesHistory(cmh), ss(s), countermove(cm), depth(d) { + const CounterMovesStats& cmh, Move cm, Search::Stack* s) + : pos(p), history(h), counterMovesHistory(&cmh), ss(s), countermove(cm), depth(d) { assert(d > DEPTH_ZERO); @@ -78,9 +78,9 @@ MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const HistoryStats& endMoves += (ttMove != MOVE_NONE); } -MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const HistoryStats& h, - const CounterMovesHistoryStats& cmh, Square s) - : pos(p), history(h), counterMovesHistory(cmh) { +MovePicker::MovePicker(const Position& p, Move ttm, Depth d, + const HistoryStats& h, Square s) + : pos(p), history(h), counterMovesHistory(nullptr) { assert(d <= DEPTH_ZERO); @@ -104,9 +104,8 @@ MovePicker::MovePicker(const Position& p, Move ttm, Depth d, const HistoryStats& endMoves += (ttMove != MOVE_NONE); } -MovePicker::MovePicker(const Position& p, Move ttm, const HistoryStats& h, - const CounterMovesHistoryStats& cmh, Value th) - : pos(p), history(h), counterMovesHistory(cmh), threshold(th) { +MovePicker::MovePicker(const Position& p, Move ttm, const HistoryStats& h, Value th) + : pos(p), history(h), counterMovesHistory(nullptr), threshold(th) { assert(!pos.checkers()); @@ -127,7 +126,7 @@ MovePicker::MovePicker(const Position& p, Move ttm, const HistoryStats& h, template<> void MovePicker::score() { // Winning and equal captures in the main search are ordered by MVV, preferring - // captures near our home rank. Suprisingly, this appears to perform slightly + // captures near our home rank. Surprisingly, this appears to perform slightly // better than SEE based move ordering: exchanging big pieces before capturing // a hanging piece probably helps to reduce the subtree size. // In main search we want to push captures with negative SEE values to the @@ -141,12 +140,9 @@ void MovePicker::score() { template<> void MovePicker::score() { - Square prevSq = to_sq((ss-1)->currentMove); - const HistoryStats& cmh = counterMovesHistory[pos.piece_on(prevSq)][prevSq]; - for (auto& m : *this) m.value = history[pos.moved_piece(m)][to_sq(m)] - + cmh[pos.moved_piece(m)][to_sq(m)]; + + (*counterMovesHistory)[pos.moved_piece(m)][to_sq(m)]; } template<> diff --git a/DroidFish/jni/stockfish/movepick.h b/DroidFish/jni/stockfish/movepick.h index d3bca28..725c40d 100644 --- a/DroidFish/jni/stockfish/movepick.h +++ b/DroidFish/jni/stockfish/movepick.h @@ -36,10 +36,10 @@ /// Countermoves store the move that refute a previous one. Entries are stored /// using only the moving piece and destination square, hence two moves with /// different origin but same destination and piece will be considered identical. -template +template struct Stats { - static const Value Max = Value(1<<28); + static const Value Max = Value(1 << 28); const T* operator[](Piece pc) const { return table[pc]; } T* operator[](Piece pc) { return table[pc]; } @@ -51,29 +51,23 @@ struct Stats { table[pc][to] = m; } - void updateH(Piece pc, Square to, Value v) { + void update(Piece pc, Square to, Value v) { - if (abs(int(v)) >= 324) + if (abs(int(v)) >= 324) return; - table[pc][to] -= table[pc][to] * abs(int(v)) / 324; - table[pc][to] += int(v) * 32; - } - void updateCMH(Piece pc, Square to, Value v) { - - if (abs(int(v)) >= 324) - return; - table[pc][to] -= table[pc][to] * abs(int(v)) / 512; - table[pc][to] += int(v) * 64; + table[pc][to] -= table[pc][to] * abs(int(v)) / (CM ? 512 : 324); + table[pc][to] += int(v) * (CM ? 64 : 32); } private: T table[PIECE_NB][SQUARE_NB]; }; -typedef Stats HistoryStats; typedef Stats MovesStats; -typedef Stats CounterMovesHistoryStats; +typedef Stats HistoryStats; +typedef Stats CounterMovesStats; +typedef Stats CounterMovesHistoryStats; /// MovePicker class is used to pick one pseudo legal move at a time from the @@ -88,9 +82,9 @@ public: MovePicker(const MovePicker&) = delete; MovePicker& operator=(const MovePicker&) = delete; - MovePicker(const Position&, Move, Depth, const HistoryStats&, const CounterMovesHistoryStats&, Square); - MovePicker(const Position&, Move, const HistoryStats&, const CounterMovesHistoryStats&, Value); - MovePicker(const Position&, Move, Depth, const HistoryStats&, const CounterMovesHistoryStats&, Move, Search::Stack*); + MovePicker(const Position&, Move, Depth, const HistoryStats&, Square); + MovePicker(const Position&, Move, const HistoryStats&, Value); + MovePicker(const Position&, Move, Depth, const HistoryStats&, const CounterMovesStats&, Move, Search::Stack*); Move next_move(); @@ -102,7 +96,7 @@ private: const Position& pos; const HistoryStats& history; - const CounterMovesHistoryStats& counterMovesHistory; + const CounterMovesStats* counterMovesHistory; Search::Stack* ss; Move countermove; Depth depth; diff --git a/DroidFish/jni/stockfish/pawns.cpp b/DroidFish/jni/stockfish/pawns.cpp index e2fcfc7..1653e23 100644 --- a/DroidFish/jni/stockfish/pawns.cpp +++ b/DroidFish/jni/stockfish/pawns.cpp @@ -57,12 +57,6 @@ namespace { // Unsupported pawn penalty const Score UnsupportedPawnPenalty = S(20, 10); - // Center bind bonus: Two pawns controlling the same central square - const Bitboard CenterBindMask[COLOR_NB] = { - (FileDBB | FileEBB) & (Rank5BB | Rank6BB | Rank7BB), - (FileDBB | FileEBB) & (Rank4BB | Rank3BB | Rank2BB) - }; - const Score CenterBind = S(16, 0); // Weakness of our pawn shelter in front of the king by [distance from edge][rank] @@ -106,6 +100,10 @@ namespace { const Square Right = (Us == WHITE ? DELTA_NE : DELTA_SW); const Square Left = (Us == WHITE ? DELTA_NW : DELTA_SE); + const Bitboard CenterBindMask = + Us == WHITE ? (FileDBB | FileEBB) & (Rank5BB | Rank6BB | Rank7BB) + : (FileDBB | FileEBB) & (Rank4BB | Rank3BB | Rank2BB); + Bitboard b, neighbours, doubled, supported, phalanx; Square s; bool passed, isolated, opposed, backward, lever, connected; @@ -116,7 +114,7 @@ namespace { Bitboard ourPawns = pos.pieces(Us , PAWN); Bitboard theirPawns = pos.pieces(Them, PAWN); - e->passedPawns[Us] = 0; + e->passedPawns[Us] = e->pawnAttacksSpan[Us] = 0; e->kingSquares[Us] = SQ_NONE; e->semiopenFiles[Us] = 0xFF; e->pawnAttacks[Us] = shift_bb(ourPawns) | shift_bb(ourPawns); @@ -130,8 +128,8 @@ namespace { File f = file_of(s); - // This file cannot be semi-open e->semiopenFiles[Us] &= ~(1 << f); + e->pawnAttacksSpan[Us] |= pawn_attack_span(Us, s); // Flag the pawn neighbours = ourPawns & adjacent_files_bb(f); @@ -198,7 +196,7 @@ namespace { e->pawnSpan[Us] = b ? int(msb(b) - lsb(b)) : 0; // Center binds: Two pawns controlling the same central square - b = shift_bb(ourPawns) & shift_bb(ourPawns) & CenterBindMask[Us]; + b = shift_bb(ourPawns) & shift_bb(ourPawns) & CenterBindMask; score += popcount(b) * CenterBind; return score; @@ -243,7 +241,7 @@ Entry* probe(const Position& pos) { e->key = key; e->score = evaluate(pos, e) - evaluate(pos, e); - e->asymmetry = popcount( e->semiopenFiles[WHITE] ^ e->semiopenFiles[BLACK] ); + e->asymmetry = popcount(e->semiopenFiles[WHITE] ^ e->semiopenFiles[BLACK]); return e; } diff --git a/DroidFish/jni/stockfish/pawns.h b/DroidFish/jni/stockfish/pawns.h index 2b978ba..353cb6a 100644 --- a/DroidFish/jni/stockfish/pawns.h +++ b/DroidFish/jni/stockfish/pawns.h @@ -35,6 +35,7 @@ struct Entry { Score pawns_score() const { return score; } Bitboard pawn_attacks(Color c) const { return pawnAttacks[c]; } Bitboard passed_pawns(Color c) const { return passedPawns[c]; } + Bitboard pawn_attacks_span(Color c) const { return pawnAttacksSpan[c]; } int pawn_span(Color c) const { return pawnSpan[c]; } int pawn_asymmetry() const { return asymmetry; } @@ -66,6 +67,7 @@ struct Entry { Score score; Bitboard passedPawns[COLOR_NB]; Bitboard pawnAttacks[COLOR_NB]; + Bitboard pawnAttacksSpan[COLOR_NB]; Square kingSquares[COLOR_NB]; Score kingSafety[COLOR_NB]; int castlingRights[COLOR_NB]; diff --git a/DroidFish/jni/stockfish/position.cpp b/DroidFish/jni/stockfish/position.cpp index c221c74..0be3301 100644 --- a/DroidFish/jni/stockfish/position.cpp +++ b/DroidFish/jni/stockfish/position.cpp @@ -84,7 +84,7 @@ PieceType min_attacker(const Bitboard*, Square, Bitboard, Bitboard&, Bitbo } // namespace -/// CheckInfo c'tor +/// CheckInfo constructor CheckInfo::CheckInfo(const Position& pos) { @@ -118,7 +118,7 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) { } os << "\nFen: " << pos.fen() << "\nKey: " << std::hex << std::uppercase - << std::setfill('0') << std::setw(16) << pos.st->key << std::dec << "\nCheckers: "; + << std::setfill('0') << std::setw(16) << pos.key() << std::dec << "\nCheckers: "; for (Bitboard b = pos.checkers(); b; ) os << UCI::square(pop_lsb(&b)) << " "; diff --git a/DroidFish/jni/stockfish/position.h b/DroidFish/jni/stockfish/position.h index 0b2a0cc..5d7cb37 100644 --- a/DroidFish/jni/stockfish/position.h +++ b/DroidFish/jni/stockfish/position.h @@ -28,7 +28,7 @@ #include "types.h" class Position; -struct Thread; +class Thread; namespace PSQT { @@ -37,8 +37,8 @@ namespace PSQT { void init(); } -/// CheckInfo struct is initialized at c'tor time and keeps info used to detect -/// if a move gives check. +/// CheckInfo struct is initialized at constructor time and keeps info used to +/// detect if a move gives check. struct CheckInfo { @@ -82,8 +82,6 @@ struct StateInfo { class Position { - friend std::ostream& operator<<(std::ostream&, const Position&); - public: static void init(); @@ -210,6 +208,8 @@ private: bool chess960; }; +extern std::ostream& operator<<(std::ostream& os, const Position& pos); + inline Color Position::side_to_move() const { return sideToMove; } diff --git a/DroidFish/jni/stockfish/search.cpp b/DroidFish/jni/stockfish/search.cpp index 33ab6b5..ed8d9ff 100644 --- a/DroidFish/jni/stockfish/search.cpp +++ b/DroidFish/jni/stockfish/search.cpp @@ -37,7 +37,7 @@ namespace Search { - volatile SignalsType Signals; + SignalsType Signals; LimitsType Limits; StateStackPtr SetupStates; } @@ -64,7 +64,7 @@ namespace { enum NodeType { Root, PV, NonPV }; // Razoring and futility margin based on depth - int razor_margin[4] = {483, 570, 603, 554}; + const int razor_margin[4] = { 483, 570, 603, 554 }; Value futility_margin(Depth d) { return Value(200 * d); } // Futility and reductions lookup tables, initialized at startup @@ -127,7 +127,6 @@ namespace { }; EasyMoveManager EasyMove; - double BestMoveChanges; Value DrawValue[COLOR_NB]; CounterMovesHistoryStats CounterMovesHistory; @@ -141,6 +140,7 @@ namespace { Value value_from_tt(Value v, int ply); void update_pv(Move* pv, Move move, Move* childPv); void update_stats(const Position& pos, Stack* ss, Move move, Depth depth, Move* quiets, int quietsCnt); + void check_time(); } // namespace @@ -174,17 +174,17 @@ void Search::init() { } -/// Search::reset() clears all search memory, to obtain reproducible search results +/// Search::clear() resets to zero search state, to obtain reproducible results -void Search::reset () { +void Search::clear() { TT.clear(); CounterMovesHistory.clear(); for (Thread* th : Threads) { - th->History.clear(); - th->Countermoves.clear(); + th->history.clear(); + th->counterMoves.clear(); } } @@ -216,14 +216,14 @@ uint64_t Search::perft(Position& pos, Depth depth) { return nodes; } -template uint64_t Search::perft(Position& pos, Depth depth); +template uint64_t Search::perft(Position&, Depth); -/// MainThread::think() is called by the main thread when the program receives +/// MainThread::search() is called by the main thread when the program receives /// the UCI 'go' command. It searches from root position and at the end prints /// the "bestmove" to output. -void MainThread::think() { +void MainThread::search() { Color us = rootPos.side_to_move(); Time.init(Limits, us, rootPos.game_ply()); @@ -288,29 +288,16 @@ void MainThread::think() { for (Thread* th : Threads) { th->maxPly = 0; - th->depth = DEPTH_ZERO; - th->searching = true; + th->rootDepth = DEPTH_ZERO; if (th != this) { th->rootPos = Position(rootPos, th); th->rootMoves = rootMoves; - th->notify_one(); // Wake up the thread and start searching + th->start_searching(); } } - Threads.timer->run = true; - Threads.timer->notify_one(); // Start the recurring timer - - search(true); // Let's start searching! - - // Stop the threads and the timer - Signals.stop = true; - Threads.timer->run = false; - - // Wait until all threads have finished - for (Thread* th : Threads) - if (th != this) - th->wait_while(th->searching); + Thread::search(); // Let's start searching! } // When playing in 'nodes as time' mode, subtract the searched nodes from @@ -329,10 +316,34 @@ void MainThread::think() { wait(Signals.stop); } - sync_cout << "bestmove " << UCI::move(rootMoves[0].pv[0], rootPos.is_chess960()); + // Stop the threads if not already stopped + Signals.stop = true; - if (rootMoves[0].pv.size() > 1 || rootMoves[0].extract_ponder_from_tt(rootPos)) - std::cout << " ponder " << UCI::move(rootMoves[0].pv[1], rootPos.is_chess960()); + // Wait until all threads have finished + for (Thread* th : Threads) + if (th != this) + th->wait_for_search_finished(); + + // Check if there are threads with a better score than main thread + Thread* bestThread = this; + if ( !this->easyMovePlayed + && Options["MultiPV"] == 1 + && !Skill(Options["Skill Level"]).enabled()) + { + for (Thread* th : Threads) + if ( th->completedDepth > bestThread->completedDepth + && th->rootMoves[0].score > bestThread->rootMoves[0].score) + bestThread = th; + } + + // Send new PV when needed + if (bestThread != this) + sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth, -VALUE_INFINITE, VALUE_INFINITE) << sync_endl; + + sync_cout << "bestmove " << UCI::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960()); + + if (bestThread->rootMoves[0].pv.size() > 1 || bestThread->rootMoves[0].extract_ponder_from_tt(rootPos)) + std::cout << " ponder " << UCI::move(bestThread->rootMoves[0].pv[1], rootPos.is_chess960()); std::cout << sync_endl; } @@ -342,22 +353,25 @@ void MainThread::think() { // repeatedly with increasing depth until the allocated thinking time has been // consumed, user stops the search, or the maximum search depth is reached. -void Thread::search(bool isMainThread) { +void Thread::search() { - Stack* ss = stack + 2; // To allow referencing (ss-2) and (ss+2) + Stack stack[MAX_PLY+4], *ss = stack+2; // To allow referencing (ss-2) and (ss+2) Value bestValue, alpha, beta, delta; Move easyMove = MOVE_NONE; + MainThread* mainThread = (this == Threads.main() ? Threads.main() : nullptr); std::memset(ss-2, 0, 5 * sizeof(Stack)); bestValue = delta = alpha = -VALUE_INFINITE; beta = VALUE_INFINITE; + completedDepth = DEPTH_ZERO; - if (isMainThread) + if (mainThread) { easyMove = EasyMove.get(rootPos.key()); EasyMove.clear(); - BestMoveChanges = 0; + mainThread->easyMovePlayed = mainThread->failedLow = false; + mainThread->bestMoveChanges = 0; TT.new_search(); } @@ -372,15 +386,35 @@ void Thread::search(bool isMainThread) { multiPV = std::min(multiPV, rootMoves.size()); // Iterative deepening loop until requested to stop or target depth reached - while (++depth < DEPTH_MAX && !Signals.stop && (!Limits.depth || depth <= Limits.depth)) + while (++rootDepth < DEPTH_MAX && !Signals.stop && (!Limits.depth || rootDepth <= Limits.depth)) { - // Set up the new depth for the helper threads - if (!isMainThread) - depth = Threads.main()->depth + Depth(int(3 * log(1 + this->idx))); + // Set up the new depth for the helper threads skipping in average each + // 2nd ply (using a half density map similar to a Hadamard matrix). + if (!mainThread) + { + int d = rootDepth + rootPos.game_ply(); + + if (idx <= 6 || idx > 24) + { + if (((d + idx) >> (msb(idx + 1) - 1)) % 2) + continue; + } + else + { + // Table of values of 6 bits with 3 of them set + static const int HalfDensityMap[] = { + 0x07, 0x0b, 0x0d, 0x0e, 0x13, 0x16, 0x19, 0x1a, 0x1c, + 0x23, 0x25, 0x26, 0x29, 0x2c, 0x31, 0x32, 0x34, 0x38 + }; + + if ((HalfDensityMap[idx - 7] >> (d % 6)) & 1) + continue; + } + } // Age out PV variability metric - if (isMainThread) - BestMoveChanges *= 0.5; + if (mainThread) + mainThread->bestMoveChanges *= 0.505, mainThread->failedLow = false; // Save the last iteration's scores before first PV line is searched and // all the move scores except the (new) PV are set to -VALUE_INFINITE. @@ -391,7 +425,7 @@ void Thread::search(bool isMainThread) { for (PVIdx = 0; PVIdx < multiPV && !Signals.stop; ++PVIdx) { // Reset aspiration window starting size - if (depth >= 5 * ONE_PLY) + if (rootDepth >= 5 * ONE_PLY) { delta = Value(18); alpha = std::max(rootMoves[PVIdx].previousScore - delta,-VALUE_INFINITE); @@ -403,7 +437,7 @@ void Thread::search(bool isMainThread) { // high/low anymore. while (true) { - bestValue = ::search(rootPos, ss, alpha, beta, depth, false); + bestValue = ::search(rootPos, ss, alpha, beta, rootDepth, false); // Bring the best move to the front. It is critical that sorting // is done with a stable algorithm because all the values but the @@ -426,11 +460,11 @@ void Thread::search(bool isMainThread) { // When failing high/low give some update (without cluttering // the UI) before a re-search. - if ( isMainThread + if ( mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta) && Time.elapsed() > 3000) - sync_cout << UCI::pv(rootPos, depth, alpha, beta) << sync_endl; + sync_cout << UCI::pv(rootPos, rootDepth, alpha, beta) << sync_endl; // In case of failing low/high increase aspiration window and // re-search, otherwise exit the loop. @@ -439,9 +473,9 @@ void Thread::search(bool isMainThread) { beta = (alpha + beta) / 2; alpha = std::max(bestValue - delta, -VALUE_INFINITE); - if (isMainThread) + if (mainThread) { - Signals.failedLowAtRoot = true; + mainThread->failedLow = true; Signals.stopOnPonderhit = false; } } @@ -461,7 +495,7 @@ void Thread::search(bool isMainThread) { // Sort the PV lines searched so far and update the GUI std::stable_sort(rootMoves.begin(), rootMoves.begin() + PVIdx + 1); - if (!isMainThread) + if (!mainThread) break; if (Signals.stop) @@ -469,14 +503,17 @@ void Thread::search(bool isMainThread) { << " time " << Time.elapsed() << sync_endl; else if (PVIdx + 1 == multiPV || Time.elapsed() > 3000) - sync_cout << UCI::pv(rootPos, depth, alpha, beta) << sync_endl; + sync_cout << UCI::pv(rootPos, rootDepth, alpha, beta) << sync_endl; } - if (!isMainThread) + if (!Signals.stop) + completedDepth = rootDepth; + + if (!mainThread) continue; // If skill level is enabled and time is up, pick a sub-optimal best move - if (skill.enabled() && skill.time_to_pick(depth)) + if (skill.enabled() && skill.time_to_pick(rootDepth)) skill.pick_best(multiPV); // Have we found a "mate in x"? @@ -491,17 +528,17 @@ void Thread::search(bool isMainThread) { if (!Signals.stop && !Signals.stopOnPonderhit) { // Take some extra time if the best move has changed - if (depth > 4 * ONE_PLY && multiPV == 1) - Time.pv_instability(BestMoveChanges); + if (rootDepth > 4 * ONE_PLY && multiPV == 1) + Time.pv_instability(mainThread->bestMoveChanges); // Stop the search if only one legal move is available or all // of the available time has been used or we matched an easyMove // from the previous search and just did a fast verification. if ( rootMoves.size() == 1 - || Time.elapsed() > Time.available() - || ( rootMoves[0].pv[0] == easyMove - && BestMoveChanges < 0.03 - && Time.elapsed() > Time.available() / 10)) + || Time.elapsed() > Time.available() * (mainThread->failedLow ? 641 : 315) / 640 + || (mainThread->easyMovePlayed = ( rootMoves[0].pv[0] == easyMove + && mainThread->bestMoveChanges < 0.03 + && Time.elapsed() > Time.available() / 8))) { // If we are allowed to ponder do not stop the search now but // keep pondering until the GUI sends "ponderhit" or "stop". @@ -519,15 +556,12 @@ void Thread::search(bool isMainThread) { } } - searching = false; - notify_one(); // Wake up main thread if is sleeping waiting for us - - if (!isMainThread) + if (!mainThread) return; // Clear any candidate easy move that wasn't stable for the last search // iterations; the second condition prevents consecutive fast moves. - if (EasyMove.stableCnt < 6 || Time.elapsed() < Time.available()) + if (EasyMove.stableCnt < 6 || mainThread->easyMovePlayed) EasyMove.clear(); // If skill level is enabled, swap best PV line with the sub-optimal one @@ -539,12 +573,7 @@ void Thread::search(bool isMainThread) { namespace { - // search<>() is the main search function for both PV and non-PV nodes and for - // normal and SplitPoint nodes. When called just after a split point the search - // is simpler because we have already probed the hash table, done a null move - // search, and searched the first move before splitting, so we don't have to - // repeat all this work again. We also don't need to store anything to the hash - // table here: This is taken care of after we return from the split point. + // search<>() is the main search function for both PV and non-PV nodes template Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode) { @@ -554,7 +583,7 @@ namespace { assert(-VALUE_INFINITE <= alpha && alpha < beta && beta <= VALUE_INFINITE); assert(PvNode || (alpha == beta - 1)); - assert(depth > DEPTH_ZERO); + assert(DEPTH_ZERO < depth && depth < DEPTH_MAX); Move pv[MAX_PLY+1], quietsSearched[64]; StateInfo st; @@ -574,6 +603,20 @@ namespace { bestValue = -VALUE_INFINITE; ss->ply = (ss-1)->ply + 1; + // Check for available remaining time + if (thisThread->resetCalls.load(std::memory_order_relaxed)) + { + thisThread->resetCalls = false; + thisThread->callsCnt = 0; + } + if (++thisThread->callsCnt > 4096) + { + for (Thread* th : Threads) + th->resetCalls = true; + + check_time(); + } + // Used to send selDepth info to GUI if (PvNode && thisThread->maxPly < ss->ply) thisThread->maxPly = ss->ply; @@ -581,8 +624,9 @@ namespace { if (!RootNode) { // Step 2. Check for aborted search and immediate draw - if (Signals.stop || pos.is_draw() || ss->ply >= MAX_PLY) - return ss->ply >= MAX_PLY && !inCheck ? evaluate(pos) : DrawValue[pos.side_to_move()]; + if (Signals.stop.load(std::memory_order_relaxed) || pos.is_draw() || ss->ply >= MAX_PLY) + return ss->ply >= MAX_PLY && !inCheck ? evaluate(pos) + : DrawValue[pos.side_to_move()]; // Step 3. Mate distance pruning. Even if we mate at the next move our score // would be at best mate_in(ss->ply+1), but if alpha is already bigger because @@ -598,24 +642,25 @@ namespace { assert(0 <= ss->ply && ss->ply < MAX_PLY); - ss->currentMove = ss->ttMove = (ss+1)->excludedMove = bestMove = MOVE_NONE; - (ss+1)->skipEarlyPruning = false; (ss+1)->reduction = DEPTH_ZERO; + ss->currentMove = (ss+1)->excludedMove = bestMove = MOVE_NONE; + (ss+1)->skipEarlyPruning = false; (ss+2)->killers[0] = (ss+2)->killers[1] = MOVE_NONE; - // Step 4. Transposition table lookup - // We don't want the score of a partial search to overwrite a previous full search - // TT value, so we use a different position key in case of an excluded move. + // Step 4. Transposition table lookup. We don't want the score of a partial + // search to overwrite a previous full search TT value, so we use a different + // position key in case of an excluded move. excludedMove = ss->excludedMove; posKey = excludedMove ? pos.exclusion_key() : pos.key(); tte = TT.probe(posKey, ttHit); - ss->ttMove = ttMove = RootNode ? thisThread->rootMoves[thisThread->PVIdx].pv[0] : ttHit ? tte->move() : MOVE_NONE; ttValue = ttHit ? value_from_tt(tte->value(), ss->ply) : VALUE_NONE; + ttMove = RootNode ? thisThread->rootMoves[thisThread->PVIdx].pv[0] + : ttHit ? tte->move() : MOVE_NONE; - // At non-PV nodes we check for a fail high/low. We don't prune at PV nodes + // At non-PV nodes we check for an early TT cutoff if ( !PvNode && ttHit && tte->depth() >= depth - && ttValue != VALUE_NONE // Only in case of TT access race + && ttValue != VALUE_NONE // Possible in case of TT access race && (ttValue >= beta ? (tte->bound() & BOUND_LOWER) : (tte->bound() & BOUND_UPPER))) { @@ -679,9 +724,11 @@ namespace { else { eval = ss->staticEval = - (ss-1)->currentMove != MOVE_NULL ? evaluate(pos) : -(ss-1)->staticEval + 2 * Eval::Tempo; + (ss-1)->currentMove != MOVE_NULL ? evaluate(pos) + : -(ss-1)->staticEval + 2 * Eval::Tempo; - tte->save(posKey, VALUE_NONE, BOUND_NONE, DEPTH_NONE, MOVE_NONE, ss->staticEval, TT.generation()); + tte->save(posKey, VALUE_NONE, BOUND_NONE, DEPTH_NONE, MOVE_NONE, + ss->staticEval, TT.generation()); } if (ss->skipEarlyPruning) @@ -753,8 +800,8 @@ namespace { // Step 9. ProbCut (skipped when in check) // If we have a very good capture (i.e. SEE > seeValues[captured_piece_type]) - // and a reduced search returns a value much above beta, we can (almost) safely - // prune the previous move. + // and a reduced search returns a value much above beta, we can (almost) + // safely prune the previous move. if ( !PvNode && depth >= 5 * ONE_PLY && abs(beta) < VALUE_MATE_IN_MAX_PLY) @@ -766,7 +813,7 @@ namespace { assert((ss-1)->currentMove != MOVE_NONE); assert((ss-1)->currentMove != MOVE_NULL); - MovePicker mp(pos, ttMove, thisThread->History, CounterMovesHistory, PieceValue[MG][pos.captured_piece_type()]); + MovePicker mp(pos, ttMove, thisThread->history, PieceValue[MG][pos.captured_piece_type()]); CheckInfo ci(pos); while ((move = mp.next_move()) != MOVE_NONE) @@ -797,10 +844,11 @@ namespace { moves_loop: // When in check search starts from here - Square prevMoveSq = to_sq((ss-1)->currentMove); - Move countermove = thisThread->Countermoves[pos.piece_on(prevMoveSq)][prevMoveSq]; + Square prevSq = to_sq((ss-1)->currentMove); + Move cm = thisThread->counterMoves[pos.piece_on(prevSq)][prevSq]; + const CounterMovesStats& cmh = CounterMovesHistory[pos.piece_on(prevSq)][prevSq]; - MovePicker mp(pos, ttMove, depth, thisThread->History, CounterMovesHistory, countermove, ss); + MovePicker mp(pos, ttMove, depth, thisThread->history, cmh, cm, ss); CheckInfo ci(pos); value = bestValue; // Workaround a bogus 'uninitialized' warning under gcc improving = ss->staticEval >= (ss-2)->staticEval @@ -828,20 +876,16 @@ moves_loop: // When in check search starts from here // At root obey the "searchmoves" option and skip moves not listed in Root // Move List. As a consequence any illegal move is also skipped. In MultiPV // mode we also skip PV moves which have been already searched. - if (RootNode && !std::count(thisThread->rootMoves.begin() + thisThread->PVIdx, thisThread->rootMoves.end(), move)) + if (RootNode && !std::count(thisThread->rootMoves.begin() + thisThread->PVIdx, + thisThread->rootMoves.end(), move)) continue; ss->moveCount = ++moveCount; - if (RootNode && thisThread == Threads.main()) - { - Signals.firstRootMove = (moveCount == 1); - - if (Time.elapsed() > 3000) - sync_cout << "info depth " << depth / ONE_PLY - << " currmove " << UCI::move(move, pos.is_chess960()) - << " currmovenumber " << moveCount + thisThread->PVIdx << sync_endl; - } + if (RootNode && thisThread == Threads.main() && Time.elapsed() > 3000) + sync_cout << "info depth " << depth / ONE_PLY + << " currmove " << UCI::move(move, pos.is_chess960()) + << " currmovenumber " << moveCount + thisThread->PVIdx << sync_endl; if (PvNode) (ss+1)->pv = nullptr; @@ -894,6 +938,13 @@ moves_loop: // When in check search starts from here && moveCount >= FutilityMoveCounts[improving][depth]) continue; + // History based pruning + if ( depth <= 4 * ONE_PLY + && move != ss->killers[0] + && thisThread->history[pos.moved_piece(move)][to_sq(move)] < VALUE_ZERO + && cmh[pos.moved_piece(move)][to_sq(move)] < VALUE_ZERO) + continue; + predictedDepth = newDepth - reduction(improving, depth, moveCount); // Futility pruning: parent node @@ -932,36 +983,33 @@ moves_loop: // When in check search starts from here // re-searched at full depth. if ( depth >= 3 * ONE_PLY && moveCount > 1 - && !captureOrPromotion - && move != ss->killers[0] - && move != ss->killers[1]) + && !captureOrPromotion) { - ss->reduction = reduction(improving, depth, moveCount); + Depth r = reduction(improving, depth, moveCount); + // Increase reduction for cut nodes and moves with a bad history if ( (!PvNode && cutNode) - || ( thisThread->History[pos.piece_on(to_sq(move))][to_sq(move)] < VALUE_ZERO - && CounterMovesHistory[pos.piece_on(prevMoveSq)][prevMoveSq] - [pos.piece_on(to_sq(move))][to_sq(move)] <= VALUE_ZERO)) - ss->reduction += ONE_PLY; + || ( thisThread->history[pos.piece_on(to_sq(move))][to_sq(move)] < VALUE_ZERO + && cmh[pos.piece_on(to_sq(move))][to_sq(move)] <= VALUE_ZERO)) + r += ONE_PLY; - if ( thisThread->History[pos.piece_on(to_sq(move))][to_sq(move)] > VALUE_ZERO - && CounterMovesHistory[pos.piece_on(prevMoveSq)][prevMoveSq] - [pos.piece_on(to_sq(move))][to_sq(move)] > VALUE_ZERO) - ss->reduction = std::max(DEPTH_ZERO, ss->reduction - ONE_PLY); + // Decrease reduction for moves with a good history + if ( thisThread->history[pos.piece_on(to_sq(move))][to_sq(move)] > VALUE_ZERO + && cmh[pos.piece_on(to_sq(move))][to_sq(move)] > VALUE_ZERO) + r = std::max(DEPTH_ZERO, r - ONE_PLY); // Decrease reduction for moves that escape a capture - if ( ss->reduction + if ( r && type_of(move) == NORMAL && type_of(pos.piece_on(to_sq(move))) != PAWN && pos.see(make_move(to_sq(move), from_sq(move))) < VALUE_ZERO) - ss->reduction = std::max(DEPTH_ZERO, ss->reduction - ONE_PLY); + r = std::max(DEPTH_ZERO, r - ONE_PLY); - Depth d = std::max(newDepth - ss->reduction, ONE_PLY); + Depth d = std::max(newDepth - r, ONE_PLY); value = -search(pos, ss+1, -(alpha+1), -alpha, d, true); - doFullDepthSearch = (value > alpha && ss->reduction != DEPTH_ZERO); - ss->reduction = DEPTH_ZERO; + doFullDepthSearch = (value > alpha && r != DEPTH_ZERO); } else doFullDepthSearch = !PvNode || moveCount > 1; @@ -996,12 +1044,13 @@ moves_loop: // When in check search starts from here // Finished searching the move. If a stop occurred, the return value of // the search cannot be trusted, and we return immediately without // updating best move, PV and TT. - if (Signals.stop) + if (Signals.stop.load(std::memory_order_relaxed)) return VALUE_ZERO; if (RootNode) { - RootMove& rm = *std::find(thisThread->rootMoves.begin(), thisThread->rootMoves.end(), move); + RootMove& rm = *std::find(thisThread->rootMoves.begin(), + thisThread->rootMoves.end(), move); // PV move or new best move ? if (moveCount == 1 || value > alpha) @@ -1018,7 +1067,7 @@ moves_loop: // When in check search starts from here // iteration. This information is used for time management: When // the best move changes frequently, we allocate some more time. if (moveCount > 1 && thisThread == Threads.main()) - ++BestMoveChanges; + ++static_cast(thisThread)->bestMoveChanges; } else // All other moves but the PV are set to the lowest value: this is @@ -1080,16 +1129,17 @@ moves_loop: // When in check search starts from here update_stats(pos, ss, bestMove, depth, quietsSearched, quietCount); // Bonus for prior countermove that caused the fail low - else if (!bestMove) + else if ( depth >= 3 * ONE_PLY + && !bestMove + && !inCheck + && !pos.captured_piece_type() + && is_ok((ss - 1)->currentMove) + && is_ok((ss - 2)->currentMove)) { - if (is_ok((ss - 2)->currentMove) && is_ok((ss - 1)->currentMove) && !pos.captured_piece_type() && !inCheck && depth>=3*ONE_PLY) - { - Value bonus = Value((depth / ONE_PLY) * (depth / ONE_PLY)); - Square prevSq = to_sq((ss - 1)->currentMove); - Square prevPrevSq = to_sq((ss - 2)->currentMove); - HistoryStats& flMoveCmh = CounterMovesHistory[pos.piece_on(prevPrevSq)][prevPrevSq]; - flMoveCmh.updateCMH(pos.piece_on(prevSq), prevSq, bonus); - } + Value bonus = Value((depth / ONE_PLY) * (depth / ONE_PLY) + depth / ONE_PLY - 1); + Square prevPrevSq = to_sq((ss - 2)->currentMove); + CounterMovesStats& prevCmh = CounterMovesHistory[pos.piece_on(prevPrevSq)][prevPrevSq]; + prevCmh.update(pos.piece_on(prevSq), prevSq, bonus); } tte->save(posKey, value_to_tt(bestValue, ss->ply), @@ -1139,7 +1189,8 @@ moves_loop: // When in check search starts from here // Check for an instant draw or if the maximum ply has been reached if (pos.is_draw() || ss->ply >= MAX_PLY) - return ss->ply >= MAX_PLY && !InCheck ? evaluate(pos) : DrawValue[pos.side_to_move()]; + return ss->ply >= MAX_PLY && !InCheck ? evaluate(pos) + : DrawValue[pos.side_to_move()]; assert(0 <= ss->ply && ss->ply < MAX_PLY); @@ -1187,7 +1238,8 @@ moves_loop: // When in check search starts from here } else ss->staticEval = bestValue = - (ss-1)->currentMove != MOVE_NULL ? evaluate(pos) : -(ss-1)->staticEval + 2 * Eval::Tempo; + (ss-1)->currentMove != MOVE_NULL ? evaluate(pos) + : -(ss-1)->staticEval + 2 * Eval::Tempo; // Stand pat. Return immediately if static value is at least beta if (bestValue >= beta) @@ -1209,7 +1261,7 @@ moves_loop: // When in check search starts from here // to search the moves. Because the depth is <= 0 here, only captures, // queen promotions and checks (only if depth >= DEPTH_QS_CHECKS) will // be generated. - MovePicker mp(pos, ttMove, depth, pos.this_thread()->History, CounterMovesHistory, to_sq((ss-1)->currentMove)); + MovePicker mp(pos, ttMove, depth, pos.this_thread()->history, to_sq((ss-1)->currentMove)); CheckInfo ci(pos); // Loop through the moves until no moves remain or a beta cutoff occurs @@ -1282,7 +1334,7 @@ moves_loop: // When in check search starts from here if (PvNode) // Update pv even in fail-high case update_pv(ss->pv, move, (ss+1)->pv); - if (PvNode && value < beta) // Update alpha here! Always alpha < beta + if (PvNode && value < beta) // Update alpha here! { alpha = value; bestMove = move; @@ -1348,8 +1400,8 @@ moves_loop: // When in check search starts from here } - // update_stats() updates killers, history, countermove history and - // countermoves stats for a quiet best move. + // update_stats() updates killers, history, countermove and countermove + // history when a new quiet best move is found. void update_stats(const Position& pos, Stack* ss, Move move, Depth depth, Move* quiets, int quietsCnt) { @@ -1360,35 +1412,37 @@ moves_loop: // When in check search starts from here ss->killers[0] = move; } - Value bonus = Value((depth / ONE_PLY) * (depth / ONE_PLY)); + Value bonus = Value((depth / ONE_PLY) * (depth / ONE_PLY) + depth / ONE_PLY - 1); Square prevSq = to_sq((ss-1)->currentMove); - HistoryStats& cmh = CounterMovesHistory[pos.piece_on(prevSq)][prevSq]; + CounterMovesStats& cmh = CounterMovesHistory[pos.piece_on(prevSq)][prevSq]; Thread* thisThread = pos.this_thread(); - thisThread->History.updateH(pos.moved_piece(move), to_sq(move), bonus); + thisThread->history.update(pos.moved_piece(move), to_sq(move), bonus); if (is_ok((ss-1)->currentMove)) { - thisThread->Countermoves.update(pos.piece_on(prevSq), prevSq, move); - cmh.updateCMH(pos.moved_piece(move), to_sq(move), bonus); + thisThread->counterMoves.update(pos.piece_on(prevSq), prevSq, move); + cmh.update(pos.moved_piece(move), to_sq(move), bonus); } // Decrease all the other played quiet moves for (int i = 0; i < quietsCnt; ++i) { - thisThread->History.updateH(pos.moved_piece(quiets[i]), to_sq(quiets[i]), -bonus); + thisThread->history.update(pos.moved_piece(quiets[i]), to_sq(quiets[i]), -bonus); if (is_ok((ss-1)->currentMove)) - cmh.updateCMH(pos.moved_piece(quiets[i]), to_sq(quiets[i]), -bonus); + cmh.update(pos.moved_piece(quiets[i]), to_sq(quiets[i]), -bonus); } - // Extra penalty for PV move in previous ply when it gets refuted - if (is_ok((ss-2)->currentMove) && (ss-1)->moveCount == 1 && !pos.captured_piece_type()) + // Extra penalty for a quiet TT move in previous ply when it gets refuted + if ( (ss-1)->moveCount == 1 + && !pos.captured_piece_type() + && is_ok((ss-2)->currentMove)) { Square prevPrevSq = to_sq((ss-2)->currentMove); - HistoryStats& ttMoveCmh = CounterMovesHistory[pos.piece_on(prevPrevSq)][prevPrevSq]; - ttMoveCmh.updateCMH(pos.piece_on(prevSq), prevSq, -bonus - 2 * depth / ONE_PLY - 1); + CounterMovesStats& prevCmh = CounterMovesHistory[pos.piece_on(prevPrevSq)][prevPrevSq]; + prevCmh.update(pos.piece_on(prevSq), prevSq, -bonus - 2 * (depth + 1) / ONE_PLY); } } @@ -1398,23 +1452,23 @@ moves_loop: // When in check search starts from here Move Skill::pick_best(size_t multiPV) { - // PRNG sequence should be non-deterministic, so we seed it with the time at init const Search::RootMoveVector& rootMoves = Threads.main()->rootMoves; - static PRNG rng(now()); + static PRNG rng(now()); // PRNG sequence should be non-deterministic // RootMoves are already sorted by score in descending order - int variance = std::min(rootMoves[0].score - rootMoves[multiPV - 1].score, PawnValueMg); + Value topScore = rootMoves[0].score; + int delta = std::min(topScore - rootMoves[multiPV - 1].score, PawnValueMg); int weakness = 120 - 2 * level; int maxScore = -VALUE_INFINITE; - // Choose best move. For each move score we add two terms both dependent on + // Choose best move. For each move score we add two terms, both dependent on // weakness. One deterministic and bigger for weaker levels, and one random, // then we choose the move with the resulting highest score. for (size_t i = 0; i < multiPV; ++i) { // This is our magic formula - int push = ( weakness * int(rootMoves[0].score - rootMoves[i].score) - + variance * (rng.rand() % weakness)) / 128; + int push = ( weakness * int(topScore - rootMoves[i].score) + + delta * (rng.rand() % weakness)) / 128; if (rootMoves[i].score + push > maxScore) { @@ -1422,9 +1476,37 @@ moves_loop: // When in check search starts from here best = rootMoves[i].pv[0]; } } + return best; } + + // check_time() is used to print debug info and, more importantly, to detect + // when we are out of available time and thus stop the search. + + void check_time() { + + static TimePoint lastInfoTime = now(); + + int elapsed = Time.elapsed(); + TimePoint tick = Limits.startTime + elapsed; + + if (tick - lastInfoTime >= 1000) + { + lastInfoTime = tick; + dbg_print(); + } + + // An engine may not stop pondering until told so by the GUI + if (Limits.ponder) + return; + + if ( (Limits.use_time_management() && elapsed > Time.maximum() - 10) + || (Limits.movetime && elapsed >= Limits.movetime) + || (Limits.nodes && Threads.nodes_searched() >= Limits.nodes)) + Signals.stop = true; + } + } // namespace @@ -1499,7 +1581,8 @@ void RootMove::insert_pv_in_tt(Position& pos) { TTEntry* tte = TT.probe(pos.key(), ttHit); if (!ttHit || tte->move() != m) // Don't overwrite correct entries - tte->save(pos.key(), VALUE_NONE, BOUND_NONE, DEPTH_NONE, m, VALUE_NONE, TT.generation()); + tte->save(pos.key(), VALUE_NONE, BOUND_NONE, DEPTH_NONE, + m, VALUE_NONE, TT.generation()); pos.do_move(m, *st++, pos.gives_check(m, CheckInfo(pos))); } @@ -1509,10 +1592,10 @@ void RootMove::insert_pv_in_tt(Position& pos) { } -/// RootMove::extract_ponder_from_tt() is called in case we have no ponder move before -/// exiting the search, for instance in case we stop the search during a fail high at -/// root. We try hard to have a ponder move to return to the GUI, otherwise in case of -/// 'ponder on' we have nothing to think on. +/// RootMove::extract_ponder_from_tt() is called in case we have no ponder move +/// before exiting the search, for instance in case we stop the search during a +/// fail high at root. We try hard to have a ponder move to return to the GUI, +/// otherwise in case of 'ponder on' we have nothing to think on. bool RootMove::extract_ponder_from_tt(Position& pos) { @@ -1534,40 +1617,3 @@ bool RootMove::extract_ponder_from_tt(Position& pos) return false; } - - -/// check_time() is called by the timer thread when the timer triggers. It is -/// used to print debug info and, more importantly, to detect when we are out of -/// available time and thus stop the search. - -void check_time() { - - static TimePoint lastInfoTime = now(); - int elapsed = Time.elapsed(); - - if (now() - lastInfoTime >= 1000) - { - lastInfoTime = now(); - dbg_print(); - } - - // An engine may not stop pondering until told so by the GUI - if (Limits.ponder) - return; - - if (Limits.use_time_management()) - { - bool stillAtFirstMove = Signals.firstRootMove - && !Signals.failedLowAtRoot - && elapsed > Time.available() * 75 / 100; - - if ( stillAtFirstMove - || elapsed > Time.maximum() - 2 * TimerThread::Resolution) - Signals.stop = true; - } - else if (Limits.movetime && elapsed >= Limits.movetime) - Signals.stop = true; - - else if (Limits.nodes && Threads.nodes_searched() >= Limits.nodes) - Signals.stop = true; -} diff --git a/DroidFish/jni/stockfish/search.h b/DroidFish/jni/stockfish/search.h index c7abb9d..b1575a5 100644 --- a/DroidFish/jni/stockfish/search.h +++ b/DroidFish/jni/stockfish/search.h @@ -20,7 +20,8 @@ #ifndef SEARCH_H_INCLUDED #define SEARCH_H_INCLUDED -#include // For std::auto_ptr +#include +#include // For std::unique_ptr #include #include @@ -28,8 +29,6 @@ #include "position.h" #include "types.h" -struct SplitPoint; - namespace Search { /// Stack struct keeps track of the information we need to remember from nodes @@ -37,14 +36,11 @@ namespace Search { /// its own array of Stack objects, indexed by the current ply. struct Stack { - SplitPoint* splitPoint; Move* pv; int ply; Move currentMove; - Move ttMove; Move excludedMove; Move killers[2]; - Depth reduction; Value staticEval; bool skipEarlyPruning; int moveCount; @@ -58,7 +54,7 @@ struct RootMove { explicit RootMove(Move m) : pv(1, m) {} - bool operator<(const RootMove& m) const { return score > m.score; } // Ascending sort + bool operator<(const RootMove& m) const { return m.score < score; } // Descending sort bool operator==(const Move& m) const { return pv[0] == m; } void insert_pv_in_tt(Position& pos); bool extract_ponder_from_tt(Position& pos); @@ -91,22 +87,22 @@ struct LimitsType { TimePoint startTime; }; -/// The SignalsType struct stores volatile flags updated during the search +/// The SignalsType struct stores atomic flags updated during the search /// typically in an async fashion e.g. to stop the search by the GUI. struct SignalsType { - bool stop, stopOnPonderhit, firstRootMove, failedLowAtRoot; + std::atomic_bool stop, stopOnPonderhit; }; typedef std::unique_ptr> StateStackPtr; -extern volatile SignalsType Signals; +extern SignalsType Signals; extern LimitsType Limits; extern StateStackPtr SetupStates; void init(); -void reset(); -template uint64_t perft(Position& pos, Depth depth); +void clear(); +template uint64_t perft(Position& pos, Depth depth); } // namespace Search diff --git a/DroidFish/jni/stockfish/syzygy/tbcore.cpp b/DroidFish/jni/stockfish/syzygy/tbcore.cpp index 7b0dae3..3492bef 100644 --- a/DroidFish/jni/stockfish/syzygy/tbcore.cpp +++ b/DroidFish/jni/stockfish/syzygy/tbcore.cpp @@ -343,31 +343,30 @@ void Tablebases::init(const std::string& path) init_tb(str); } - // 6-piece tables are only supported for 64-bit, because tables are mmap()ed into memory if (sizeof(char*) >= 8) { - for (i = 1; i < 6; i++) - for (j = i; j < 6; j++) - for (k = i; k < 6; k++) - for (l = (i == k) ? j : k; l < 6; l++) { - sprintf(str, "K%c%cvK%c%c", pchr[i], pchr[j], pchr[k], pchr[l]); - init_tb(str); - } + for (i = 1; i < 6; i++) + for (j = i; j < 6; j++) + for (k = i; k < 6; k++) + for (l = (i == k) ? j : k; l < 6; l++) { + sprintf(str, "K%c%cvK%c%c", pchr[i], pchr[j], pchr[k], pchr[l]); + init_tb(str); + } - for (i = 1; i < 6; i++) - for (j = i; j < 6; j++) - for (k = j; k < 6; k++) - for (l = 1; l < 6; l++) { - sprintf(str, "K%c%c%cvK%c", pchr[i], pchr[j], pchr[k], pchr[l]); - init_tb(str); - } + for (i = 1; i < 6; i++) + for (j = i; j < 6; j++) + for (k = j; k < 6; k++) + for (l = 1; l < 6; l++) { + sprintf(str, "K%c%c%cvK%c", pchr[i], pchr[j], pchr[k], pchr[l]); + init_tb(str); + } - for (i = 1; i < 6; i++) - for (j = i; j < 6; j++) - for (k = j; k < 6; k++) - for (l = k; l < 6; l++) { - sprintf(str, "K%c%c%c%cvK", pchr[i], pchr[j], pchr[k], pchr[l]); - init_tb(str); - } + for (i = 1; i < 6; i++) + for (j = i; j < 6; j++) + for (k = j; k < 6; k++) + for (l = k; l < 6; l++) { + sprintf(str, "K%c%c%c%cvK", pchr[i], pchr[j], pchr[k], pchr[l]); + init_tb(str); + } } printf("info string Found %d tablebases.\n", TBnum_piece + TBnum_pawn); diff --git a/DroidFish/jni/stockfish/thread.cpp b/DroidFish/jni/stockfish/thread.cpp index 88b4592..3b6074a 100644 --- a/DroidFish/jni/stockfish/thread.cpp +++ b/DroidFish/jni/stockfish/thread.cpp @@ -29,93 +29,68 @@ using namespace Search; ThreadPool Threads; // Global object -extern void check_time(); +/// Thread constructor launch the thread and then wait until it goes to sleep +/// in idle_loop(). -namespace { +Thread::Thread() { - // Helpers to launch a thread after creation and joining before delete. Must be - // outside Thread c'tor and d'tor because the object must be fully initialized - // when start_routine (and hence virtual idle_loop) is called and when joining. - - template T* new_thread() { - std::thread* th = new T; - *th = std::thread(&T::idle_loop, (T*)th); // Will go to sleep - return (T*)th; - } - - void delete_thread(ThreadBase* th) { - - th->mutex.lock(); - th->exit = true; // Search must be already finished - th->mutex.unlock(); - - th->notify_one(); - th->join(); // Wait for thread termination - delete th; - } + resetCalls = exit = false; + maxPly = callsCnt = 0; + history.clear(); + counterMoves.clear(); + idx = Threads.size(); // Start from 0 + std::unique_lock lk(mutex); + searching = true; + nativeThread = std::thread(&Thread::idle_loop, this); + sleepCondition.wait(lk, [&]{ return !searching; }); } -// ThreadBase::notify_one() wakes up the thread when there is some work to do +/// Thread destructor wait for thread termination before returning -void ThreadBase::notify_one() { +Thread::~Thread() { + + mutex.lock(); + exit = true; + sleepCondition.notify_one(); + mutex.unlock(); + nativeThread.join(); +} + + +/// Thread::wait_for_search_finished() wait on sleep condition until not searching + +void Thread::wait_for_search_finished() { std::unique_lock lk(mutex); + sleepCondition.wait(lk, [&]{ return !searching; }); +} + + +/// Thread::wait() wait on sleep condition until condition is true + +void Thread::wait(std::atomic_bool& condition) { + + std::unique_lock lk(mutex); + sleepCondition.wait(lk, [&]{ return bool(condition); }); +} + + +/// Thread::start_searching() wake up the thread that will start the search + +void Thread::start_searching(bool resume) { + + std::unique_lock lk(mutex); + + if (!resume) + searching = true; + sleepCondition.notify_one(); } -// ThreadBase::wait() set the thread to sleep until 'condition' turns true - -void ThreadBase::wait(volatile const bool& condition) { - - std::unique_lock lk(mutex); - sleepCondition.wait(lk, [&]{ return condition; }); -} - - -// ThreadBase::wait_while() set the thread to sleep until 'condition' turns false - -void ThreadBase::wait_while(volatile const bool& condition) { - - std::unique_lock lk(mutex); - sleepCondition.wait(lk, [&]{ return !condition; }); -} - - -// Thread c'tor makes some init but does not launch any execution thread that -// will be started only when c'tor returns. - -Thread::Thread() /* : splitPoints() */ { // Initialization of non POD broken in MSVC - - searching = false; - maxPly = 0; - idx = Threads.size(); // Starts from 0 -} - - -// TimerThread::idle_loop() is where the timer thread waits Resolution milliseconds -// and then calls check_time(). When not searching, thread sleeps until it's woken up. - -void TimerThread::idle_loop() { - - while (!exit) - { - std::unique_lock lk(mutex); - - if (!exit) - sleepCondition.wait_for(lk, std::chrono::milliseconds(run ? Resolution : INT_MAX)); - - lk.unlock(); - - if (!exit && run) - check_time(); - } -} - - -// Thread::idle_loop() is where the thread is parked when it has no work to do +/// Thread::idle_loop() is where the thread is parked when it has no work to do void Thread::idle_loop() { @@ -123,122 +98,83 @@ void Thread::idle_loop() { { std::unique_lock lk(mutex); + searching = false; + while (!searching && !exit) - sleepCondition.wait(lk); - - lk.unlock(); - - if (!exit && searching) - search(); - } -} - - -// MainThread::idle_loop() is where the main thread is parked waiting to be started -// when there is a new search. The main thread will launch all the slave threads. - -void MainThread::idle_loop() { - - while (!exit) - { - std::unique_lock lk(mutex); - - thinking = false; - - while (!thinking && !exit) { - sleepCondition.notify_one(); // Wake up the UI thread if needed + sleepCondition.notify_one(); // Wake up any waiting thread sleepCondition.wait(lk); } lk.unlock(); if (!exit) - think(); + search(); } } -// MainThread::join() waits for main thread to finish thinking - -void MainThread::join() { - - std::unique_lock lk(mutex); - sleepCondition.wait(lk, [&]{ return !thinking; }); -} - - -// ThreadPool::init() is called at startup to create and launch requested threads, -// that will go immediately to sleep. We cannot use a c'tor because Threads is a -// static object and we need a fully initialized engine at this point due to -// allocation of Endgames in Thread c'tor. +/// ThreadPool::init() create and launch requested threads, that will go +/// immediately to sleep. We cannot use a constructor because Threads is a +/// static object and we need a fully initialized engine at this point due to +/// allocation of Endgames in the Thread constructor. void ThreadPool::init() { - timer = new_thread(); - push_back(new_thread()); + push_back(new MainThread); read_uci_options(); } -// ThreadPool::exit() terminates the threads before the program exits. Cannot be -// done in d'tor because threads must be terminated before freeing us. +/// ThreadPool::exit() terminate threads before the program exits. Cannot be +/// done in destructor because threads must be terminated before deleting any +/// static objects, so while still in main(). void ThreadPool::exit() { - delete_thread(timer); // As first because check_time() accesses threads data - timer = nullptr; - - for (Thread* th : *this) - delete_thread(th); - - clear(); // Get rid of stale pointers + while (size()) + delete back(), pop_back(); } -// ThreadPool::read_uci_options() updates internal threads parameters from the -// corresponding UCI options and creates/destroys threads to match the requested -// number. Thread objects are dynamically allocated to avoid creating all possible -// threads in advance (which include pawns and material tables), even if only a -// few are to be used. +/// ThreadPool::read_uci_options() updates internal threads parameters from the +/// corresponding UCI options and creates/destroys threads to match requested +/// number. Thread objects are dynamically allocated. void ThreadPool::read_uci_options() { - size_t requested = Options["Threads"]; + size_t requested = Options["Threads"]; assert(requested > 0); while (size() < requested) - push_back(new_thread()); + push_back(new Thread); while (size() > requested) - { - delete_thread(back()); - pop_back(); - } + delete back(), pop_back(); } -// ThreadPool::nodes_searched() returns the number of nodes searched +/// ThreadPool::nodes_searched() return the number of nodes searched int64_t ThreadPool::nodes_searched() { int64_t nodes = 0; - for (Thread *th : *this) + for (Thread* th : *this) nodes += th->rootPos.nodes_searched(); return nodes; } -// ThreadPool::start_thinking() wakes up the main thread sleeping in -// MainThread::idle_loop() and starts a new search, then returns immediately. +/// ThreadPool::start_thinking() wake up the main thread sleeping in idle_loop() +/// and start a new search, then return immediately. void ThreadPool::start_thinking(const Position& pos, const LimitsType& limits, StateStackPtr& states) { - main()->join(); - Signals.stopOnPonderhit = Signals.firstRootMove = false; - Signals.stop = Signals.failedLowAtRoot = false; + main()->wait_for_search_finished(); + + Signals.stopOnPonderhit = Signals.stop = false; main()->rootMoves.clear(); main()->rootPos = pos; @@ -254,6 +190,5 @@ void ThreadPool::start_thinking(const Position& pos, const LimitsType& limits, || std::count(limits.searchmoves.begin(), limits.searchmoves.end(), m)) main()->rootMoves.push_back(RootMove(m)); - main()->thinking = true; - main()->notify_one(); // Wake up main thread: 'thinking' must be already set + main()->start_searching(); } diff --git a/DroidFish/jni/stockfish/thread.h b/DroidFish/jni/stockfish/thread.h index 97ed219..c38bd2b 100644 --- a/DroidFish/jni/stockfish/thread.h +++ b/DroidFish/jni/stockfish/thread.h @@ -34,89 +34,67 @@ #include "search.h" #include "thread_win32.h" -struct Thread; -const size_t MAX_THREADS = 128; +/// Thread struct keeps together all the thread related stuff. We also use +/// per-thread pawn and material hash tables so that once we get a pointer to an +/// entry its life time is unlimited and we don't have to care about someone +/// changing the entry under our feet. +class Thread { -/// ThreadBase struct is the base of the hierarchy from where we derive all the -/// specialized thread classes. - -struct ThreadBase : public std::thread { - - virtual ~ThreadBase() = default; - virtual void idle_loop() = 0; - void notify_one(); - void wait(volatile const bool& b); - void wait_while(volatile const bool& b); - + std::thread nativeThread; Mutex mutex; ConditionVariable sleepCondition; - volatile bool exit = false; -}; - - -/// Thread struct keeps together all the thread related stuff like locks, state -/// and especially split points. We also use per-thread pawn and material hash -/// tables so that once we get a pointer to an entry its life time is unlimited -/// and we don't have to care about someone changing the entry under our feet. - -struct Thread : public ThreadBase { + bool exit, searching; +public: Thread(); - virtual void idle_loop(); - void search(bool isMainThread = false); + virtual ~Thread(); + virtual void search(); + void idle_loop(); + void start_searching(bool resume = false); + void wait_for_search_finished(); + void wait(std::atomic_bool& b); Pawns::Table pawnsTable; Material::Table materialTable; Endgames endgames; size_t idx, PVIdx; - int maxPly; - volatile bool searching; + int maxPly, callsCnt; Position rootPos; Search::RootMoveVector rootMoves; - Search::Stack stack[MAX_PLY+4]; - HistoryStats History; - MovesStats Countermoves; - Depth depth; + Depth rootDepth; + HistoryStats history; + MovesStats counterMoves; + Depth completedDepth; + std::atomic_bool resetCalls; }; -/// MainThread and TimerThread are derived classes used to characterize the two -/// special threads: the main one and the recurring timer. +/// MainThread is a derived class with a specific overload for the main thread struct MainThread : public Thread { - virtual void idle_loop(); - void join(); - void think(); - volatile bool thinking = true; // Avoid a race with start_thinking() -}; + virtual void search(); -struct TimerThread : public ThreadBase { - - static const int Resolution = 5; // Millisec between two check_time() calls - - virtual void idle_loop(); - - bool run = false; + bool easyMovePlayed, failedLow; + double bestMoveChanges; }; /// ThreadPool struct handles all the threads related stuff like init, starting, -/// parking and, most importantly, launching a slave thread at a split point. -/// All the access to shared thread data is done through this class. +/// parking and, most importantly, launching a thread. All the access to threads +/// data is done through this class. struct ThreadPool : public std::vector { - void init(); // No c'tor and d'tor, threads rely on globals that should be - void exit(); // initialized and are valid during the whole thread lifetime. + void init(); // No constructor and destructor, threads rely on globals that should + void exit(); // be initialized and valid during the whole thread lifetime. MainThread* main() { return static_cast(at(0)); } - void read_uci_options(); void start_thinking(const Position&, const Search::LimitsType&, Search::StateStackPtr&); + void read_uci_options(); int64_t nodes_searched(); - TimerThread* timer; }; extern ThreadPool Threads; diff --git a/DroidFish/jni/stockfish/timeman.cpp b/DroidFish/jni/stockfish/timeman.cpp index 3a4e157..47f57ab 100644 --- a/DroidFish/jni/stockfish/timeman.cpp +++ b/DroidFish/jni/stockfish/timeman.cpp @@ -32,8 +32,8 @@ namespace { enum TimeType { OptimumTime, MaxTime }; const int MoveHorizon = 50; // Plan time management at most this many moves ahead - const double MaxRatio = 7.0; // When in trouble, we can step over reserved time with this ratio - const double StealRatio = 0.33; // However we must not steal time from remaining moves over this ratio + const double MaxRatio = 6.93; // When in trouble, we can step over reserved time with this ratio + const double StealRatio = 0.36; // However we must not steal time from remaining moves over this ratio // move_importance() is a skew-logistic function based on naive statistical @@ -43,9 +43,9 @@ namespace { double move_importance(int ply) { - const double XScale = 9.3; - const double XShift = 59.8; - const double Skew = 0.172; + const double XScale = 8.27; + const double XShift = 59.; + const double Skew = 0.179; return pow((1 + exp((ply - XShift) / XScale)), -Skew) + DBL_MIN; // Ensure non-zero } @@ -129,6 +129,4 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) if (Options["Ponder"]) optimumTime += optimumTime / 4; - - optimumTime = std::min(optimumTime, maximumTime); } diff --git a/DroidFish/jni/stockfish/timeman.h b/DroidFish/jni/stockfish/timeman.h index b6eb348..3faba00 100644 --- a/DroidFish/jni/stockfish/timeman.h +++ b/DroidFish/jni/stockfish/timeman.h @@ -31,7 +31,7 @@ class TimeManagement { public: void init(Search::LimitsType& limits, Color us, int ply); void pv_instability(double bestMoveChanges) { unstablePvFactor = 1 + bestMoveChanges; } - int available() const { return int(optimumTime * unstablePvFactor * 0.76); } + int available() const { return int(optimumTime * unstablePvFactor * 1.016); } int maximum() const { return maximumTime; } int elapsed() const { return int(Search::Limits.npmsec ? Threads.nodes_searched() : now() - startTime); } diff --git a/DroidFish/jni/stockfish/tt.h b/DroidFish/jni/stockfish/tt.h index a983805..84a4b9f 100644 --- a/DroidFish/jni/stockfish/tt.h +++ b/DroidFish/jni/stockfish/tt.h @@ -76,8 +76,9 @@ private: /// A TranspositionTable consists of a power of 2 number of clusters and each /// cluster consists of ClusterSize number of TTEntry. Each non-empty entry /// contains information of exactly one position. The size of a cluster should -/// not be bigger than a cache line size. In case it is less, it should be padded -/// to guarantee always aligned accesses. +/// divide the size of a cache line size, to ensure that clusters never cross +/// cache lines. This ensures best cache performance, as the cacheline is +/// prefetched, as soon as possible. class TranspositionTable { @@ -86,10 +87,10 @@ class TranspositionTable { struct Cluster { TTEntry entry[ClusterSize]; - char padding[2]; // Align to the cache line size + char padding[2]; // Align to a divisor of the cache line size }; - static_assert(sizeof(Cluster) == CacheLineSize / 2, "Cluster size incorrect"); + static_assert(CacheLineSize % sizeof(Cluster) == 0, "Cluster size incorrect"); public: ~TranspositionTable() { free(mem); } diff --git a/DroidFish/jni/stockfish/uci.cpp b/DroidFish/jni/stockfish/uci.cpp index c5dbafa..83a8b93 100644 --- a/DroidFish/jni/stockfish/uci.cpp +++ b/DroidFish/jni/stockfish/uci.cpp @@ -170,7 +170,7 @@ void UCI::loop(int argc, char* argv[]) { || (token == "ponderhit" && Search::Signals.stopOnPonderhit)) { Search::Signals.stop = true; - Threads.main()->notify_one(); // Could be sleeping + Threads.main()->start_searching(true); // Could be sleeping } else if (token == "ponderhit") Search::Limits.ponder = 0; // Switch to normal search @@ -182,7 +182,7 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "ucinewgame") { - Search::reset(); + Search::clear(); Time.availableNodes = 0; } else if (token == "isready") sync_cout << "readyok" << sync_endl; @@ -211,7 +211,7 @@ void UCI::loop(int argc, char* argv[]) { } while (token != "quit" && argc == 1); // Passed args have one-shot behaviour - Threads.main()->join(); // Cannot quit whilst the search is running + Threads.main()->wait_for_search_finished(); } diff --git a/DroidFish/jni/stockfish/ucioption.cpp b/DroidFish/jni/stockfish/ucioption.cpp index f35cea4..2c53713 100644 --- a/DroidFish/jni/stockfish/ucioption.cpp +++ b/DroidFish/jni/stockfish/ucioption.cpp @@ -35,7 +35,7 @@ UCI::OptionsMap Options; // Global object namespace UCI { /// 'On change' actions, triggered by an option's value change -void on_clear_hash(const Option&) { Search::reset(); } +void on_clear_hash(const Option&) { Search::clear(); } void on_hash_size(const Option& o) { TT.resize(o); } void on_logger(const Option& o) { start_logger(o); } void on_threads(const Option&) { Threads.read_uci_options(); } @@ -58,16 +58,15 @@ void init(OptionsMap& o) { o["Write Debug Log"] << Option(false, on_logger); o["Contempt"] << Option(0, -100, 100); - o["Min Split Depth"] << Option(5, 0, 12, on_threads); - o["Threads"] << Option(1, 1, MAX_THREADS, on_threads); + o["Threads"] << Option(1, 1, 128, on_threads); o["Hash"] << Option(16, 1, MaxHashMB, on_hash_size); o["Clear Hash"] << Option(on_clear_hash); - o["Ponder"] << Option(true); + o["Ponder"] << Option(false); o["MultiPV"] << Option(1, 1, 500); o["Skill Level"] << Option(20, 0, 20); o["Move Overhead"] << Option(30, 0, 5000); o["Minimum Thinking Time"] << Option(20, 0, 5000); - o["Slow Mover"] << Option(80, 10, 1000); + o["Slow Mover"] << Option(84, 10, 1000); o["nodestime"] << Option(0, 0, 10000); o["UCI_Chess960"] << Option(false); o["SyzygyPath"] << Option("", on_tb_path);