void learning_tools_unit_test_kpp() { // KPPの三角配列化にバグがないかテストする // k-p0-p1のすべての組み合わせがきちんとKPPの扱う対象になっていかと、そのときの次元下げが // 正しいかを判定する。 KK g_kk; g_kk.set(SQ_NB, Eval::fe_end, 0); KKP g_kkp; g_kkp.set(SQ_NB, Eval::fe_end, g_kk.max_index()); KPP g_kpp; g_kpp.set(SQ_NB, Eval::fe_end, g_kkp.max_index()); std::vector<bool> f; f.resize(g_kpp.max_index() - g_kpp.min_index()); for(auto k = SQ_ZERO ; k < SQ_NB ; ++k) for(auto p0 = BonaPiece::BONA_PIECE_ZERO; p0 < fe_end ; ++p0) for (auto p1 = BonaPiece::BONA_PIECE_ZERO; p1 < fe_end; ++p1) { KPP kpp_org = g_kpp.fromKPP(k,p0,p1); KPP kpp0; KPP kpp1 = g_kpp.fromKPP(Mir(k), mir_piece(p0), mir_piece(p1)); KPP kpp_array[2]; auto index = kpp_org.toIndex(); ASSERT_LV3(g_kpp.is_ok(index)); kpp0 = g_kpp.fromIndex(index); //if (kpp0 != kpp_org) // std::cout << "index = " << index << "," << kpp_org << "," << kpp0 << std::endl; kpp0.toLowerDimensions(kpp_array); ASSERT_LV3(kpp_array[0] == kpp0); ASSERT_LV3(kpp0 == kpp_org); ASSERT_LV3(kpp_array[1] == kpp1); auto index2 = kpp1.toIndex(); f[index - g_kpp.min_index()] = f[index2-g_kpp.min_index()] = true; } // 抜けてるindexがなかったかの確認。 for(size_t index = 0 ; index < f.size(); index++) if (!f[index]) { std::cout << index << g_kpp.fromIndex(index + g_kpp.min_index()) << std::endl; } }
void learning_tools_unit_test_kkpp() { KKPP g_kkpp; g_kkpp.set(SQ_NB, 10000 , 0); u64 n = 0; for (int k = 0; k<SQ_NB; ++k) for (int i = 0; i<10000; ++i) // 試しに、かなり大きなfe_endを想定して10000で回してみる。 for (int j = 0; j < i; ++j) { auto kkpp = g_kkpp.fromKKPP(k, (BonaPiece)i, (BonaPiece)j); auto r = kkpp.toRawIndex(); ASSERT_LV3(n++ == r); auto kkpp2 = g_kkpp.fromIndex(r + g_kkpp.min_index()); ASSERT_LV3(kkpp2.king() == k && kkpp2.piece0() == i && kkpp2.piece1() == j); } }
// 通常探索から呼び出されるとき用。 MovePicker::MovePicker(const Position& p, Move ttm, Depth d, Search::Stack*s) : pos(p), ss(s), depth(d) { // 通常探索から呼び出されているので残り深さはゼロより大きい。 ASSERT_LV3(d > DEPTH_ZERO); Square prevSq = to_sq((ss - 1)->currentMove); Piece prevPc = pos.moved_piece_after((ss - 1)->currentMove); countermove = is_ok((ss - 1)->currentMove) ? pos.this_thread()->counterMoves[prevSq][prevPc] : MOVE_NONE ; // 次の指し手生成の段階 // 王手がかかっているなら回避手、かかっていないなら通常探索用の指し手生成 stage = pos.in_check() ? EVASION : MAIN_SEARCH; // 置換表の指し手があるならそれを最初に返す。ただしpseudo_legalでなければならない。 ttMove = ttm && pos.pseudo_legal_s<false>(ttm) ? ttm : MOVE_NONE; // 置換表の指し手がないなら、次のstageから開始する。 stage += (ttMove == MOVE_NONE); }
// 通常探索から呼び出されるとき用。 MovePicker::MovePicker(const Position& p, Move ttm, Depth d, Search::Stack*s) : pos(p), ss(s), depth(d) { // 通常探索から呼び出されているので残り深さはゼロより大きい。 ASSERT_LV3(d > DEPTH_ZERO); #ifdef MUST_CAPTURE_SHOGI_ENGINE checkMustCapture(); #endif // (ss - 1)->currentMove == MOVE_NONEである可能性はある。 // そのときは、prevPc == NO_PIECEになり、それでもうまく動作する。 Square prevSq = to_sq((ss - 1)->currentMove); Piece prevPc = pos.moved_piece_after((ss - 1)->currentMove); countermove = pos.this_thread()->counterMoves[prevSq][prevPc]; killers[0] = ss->killers[0]; killers[1] = ss->killers[1]; // 次の指し手生成の段階 // 王手がかかっているなら回避手、かかっていないなら通常探索用の指し手生成 stage = pos.in_check() ? EVASION : MAIN_SEARCH; // 置換表の指し手があるならそれを最初に返す。ただしpseudo_legalでなければならない。 ttMove = ttm && pos.pseudo_legal_s<false>(ttm) ? ttm : MOVE_NONE; // 置換表の指し手がないなら、次のstageから開始する。 stage += (ttMove == MOVE_NONE); }
// 静止探索から呼び出される時用。 MovePicker::MovePicker(const Position& p, Move ttm, Depth d, Square recapSq) : pos(p) { #ifdef MUST_CAPTURE_SHOGI_ENGINE checkMustCapture(); #endif // 静止探索から呼び出されているので残り深さはゼロ以下。 ASSERT_LV3(d <= DEPTH_ZERO); if (pos.in_check()) stage = EVASION; else if (d > DEPTH_QS_NO_CHECKS) stage = QSEARCH_WITH_CHECKS; else if (d > DEPTH_QS_RECAPTURES) stage = QSEARCH_NO_CHECKS; else { stage = QSEARCH_RECAPTURES; recaptureSquare = recapSq; return; } // 歩の不成、香の2段目への不成、大駒の不成を除外 ttMove = ttm && pos.pseudo_legal_s<false>(ttm) ? ttm : MOVE_NONE; // 置換表の指し手がないなら、次のstageから開始する。 stage += (ttMove == MOVE_NONE); }
void RootMove::insert_pv_in_tt(Position& pos) { StateInfo state[MAX_PLY], *st = state; bool ttHit; // 細かいことだがpvのtailから前方に向かって置換表に書き込んでいくほうが、 // pvの前のほうがエントリーの価値が高いので上書きされてしまう場合にわずかに得ではある。 // ただ、現実的にはほとんど起こりえないので気にしないことにする。 for (Move m : pv) { // 銀の不成の指し手をcounter moveとして登録して、この位置に角が来ると // 角の不成の指し手を生成することになるからLEGALではなくLEGAL_ALLで判定しないといけない。 ASSERT_LV3(MoveList<LEGAL_ALL>(pos).contains(m)); TTEntry* tte = TT.probe(pos.state()->key(), ttHit); // 正しいエントリーは書き換えない。 if (!ttHit || tte->move() != m) tte->save(pos.state()->key(), VALUE_NONE, BOUND_NONE, DEPTH_NONE, m, #ifndef NO_EVAL_IN_TT VALUE_NONE, #endif TT.generation()); pos.do_move(m, *st++); } for (size_t i = pv.size(); i > 0; ) pos.undo_move(pv[--i]); }
TTEntry* TranspositionTable::probe(const Key key, bool& found) const { ASSERT_LV3(clusterCount != 0); // 最初のTT_ENTRYのアドレス(このアドレスからTT_ENTRYがClusterSize分だけ連なっている) // keyの下位bitをいくつか使って、このアドレスを求めるので、自ずと下位bitはいくらかは一致していることになる。 TTEntry* const tte = &table[(size_t)(key) & (clusterCount - 1)].entry[0]; // 上位16bitが合致するTT_ENTRYを探す const uint16_t key16 = key >> 48; // クラスターのなかから、keyが合致するTT_ENTRYを探す for (int i = 0; i < ClusterSize; ++i) { // returnする条件 // 1. keyが合致しているentryを見つけた。(found==trueにしてそのTT_ENTRYのアドレスを返す) // 2. 空のエントリーを見つけた(そこまではkeyが合致していないので、found==falseにして新規TT_ENTRYのアドレスとして返す) if (!tte[i].key16) return found = false, &tte[i]; if (tte[i].key16 == key16) { tte[i].set_generation(generation8); // Refresh return found = true, &tte[i]; } } // 空きエントリーも、探していたkeyが格納されているentryが見当たらなかった。 // クラスター内のどれか一つを潰す必要がある。 TTEntry* replace = tte; for (int i = 1; i < ClusterSize; ++i) // ・深い探索の結果であるものほど価値があるので残しておきたい。depth8 × 重み1.0 // ・generationがいまの探索generationに近いものほど価値があるので残しておきたい。geration(4ずつ増える)×重み 2.0 // 以上に基いてスコアリングする。 // 以上の合計が一番小さいTTEntryを使う。 if (replace->depth8 - ((259 + generation8 - replace->genBound8) & 0xFC) * 2 * ONE_PLY > tte[i].depth8 - ((259 + generation8 - tte[i].genBound8 ) & 0xFC) * 2 * ONE_PLY) replace = &tte[i]; // generationは256になるとオーバーフローして0になるのでそれをうまく処理できなければならない。 // a,bが8bitであるとき ( 256 + a - b ) & 0xff のようにすれば、オーバーフローを考慮した引き算が出来る。 // このテクニックを用いる。 // いま、 // a := generationは下位2bitは用いていないので0。 // b := genBound8は下位2bitにはBoundが入っているのでこれはゴミと考える。 // ( 256 + a - b + c) & 0xfc として c = 3としても結果に影響は及ぼさない、かつ、このゴミを無視した計算が出来る。 return found = false, replace; }
// 通常探索時にProbCutの処理から呼び出されるの専用 // th = 枝刈りのしきい値 MovePicker::MovePicker(const Position& p, Move ttm, Value th) : pos(p), threshold(th) { ASSERT_LV3(!pos.in_check()); stage = PROBCUT; // ProbCutにおいて、SEEが与えられたthresholdの値より大きな指し手のみ生成する。 // (置換表の指しても、この条件を満たさなければならない) ttMove = ttm && pos.pseudo_legal_s<false>(ttm) && pos.capture(ttm) && pos.see_ge(ttm, threshold + 1) ? ttm : MOVE_NONE; // 置換表の指し手がないなら、次のstageから開始する。 stage += (ttMove == MOVE_NONE); }
// 通常探索時にProbCutの処理から呼び出されるの専用 // th = 枝刈りのしきい値 MovePicker::MovePicker(const Position& p, Move ttm, Value th) : pos(p), threshold(th) { ASSERT_LV3(!pos.in_check()); #ifdef MUST_CAPTURE_SHOGI_ENGINE checkMustCapture(); #endif stage = PROBCUT; // ProbCutにおいて、SEEが与えられたthresholdの値以上の指し手のみ生成する。 // (置換表の指しても、この条件を満たさなければならない) ttMove = ttm && pos.pseudo_legal_s<false>(ttm) && pos.capture(ttm) && pos.see_ge(ttm, threshold) ? ttm : MOVE_NONE; // 置換表の指し手がないなら、次のstageから開始する。 stage += (ttMove == MOVE_NONE); }
// スコアを歩の価値を100として正規化して出力する。 std::string score_to_usi(Value v) { ASSERT_LV3(-VALUE_INFINITE < v && v < VALUE_INFINITE); std::stringstream s; // 置換表上、値が確定していないことがある。 if (v == VALUE_NONE) s << "none"; else if (abs(v) < VALUE_MATE_IN_MAX_PLY) s << "cp " << v * 100 / int(Eval::PawnValue); else if (v == -VALUE_MATE) // USIプロトコルでは、手数がわからないときには "mate -"と出力するらしい。 // 手数がわからないというか詰んでいるのだが…。これを出力する方法がUSIプロトコルで定められていない。 // ここでは"-0"を出力しておく。 // 将棋所では検討モードは、go infiniteで呼び出されて、このときbestmoveを返さないから // 結局、このときのスコアは画面に表示されない。 // ShogiGUIだと、これできちんと"+詰"と出力されるようである。 s << "mate -0"; else s << "mate " << (v > 0 ? VALUE_MATE - v : -VALUE_MATE - v); return s.str(); }
// 利きのある場所への取れない近接王手からの3手詰め Move Position::weak_mate_n_ply(int ply) const { // 1手詰めであるならこれを返す Move m = mate1ply(); if (m) return m; // 詰まない if (ply <= 1) return MOVE_NONE; Color us = side_to_move(); Color them = ~us; Bitboard around8 = kingEffect(king_square(them)); // const剥がし Position* This = ((Position*)this); StateInfo si; StateInfo si2; // 近接王手で味方の利きがあり、敵の利きのない場所を探す。 for (auto m : MoveList<CHECKS>(*this)) { // 近接王手で、この指し手による駒の移動先に敵の駒がない。 Square to = to_sq(m); if ((around8 & to) #ifndef LONG_EFFECT_LIBRARY // toに利きがあるかどうか。mが移動の指し手の場合、mの元の利きを取り除く必要がある。 && (is_drop(m) ? effected_to(us, to) : (attackers_to(us, to, pieces() ^ from_sq(m)) ^ from_sq(m))) // 敵玉の利きは必ずtoにあるのでそれを除いた利きがあるかどうか。 && (attackers_to(them,to,pieces()) ^ king_square(them)) #else && (is_drop(m) ? effected_to(us, to) : board_effect[us].effect(to) >= 2 || (long_effect.directions_of(us, from_sq(m)) & Effect8::directions_of(from_sq(m), to)) != 0) // 敵玉の利きがあるので2つ以上なければそれで良い。 && (board_effect[them].effect(to) <= 1) #endif ) { if (!legal(m)) continue; ASSERT_LV3(gives_check(m)); This->do_move(m,si,true); ASSERT_LV3(in_check()); // この局面ですべてのevasionを試す for (auto m2 : MoveList<EVASIONS>(*this)) { if (!legal(m2)) continue; // この指し手で逆王手になるなら、不詰めとして扱う if (gives_check(m2)) goto NEXT_CHECK; This->do_move(m2, si2, false); ASSERT_LV3(!in_check()); if (!weak_mate_n_ply(ply-2)) { // 詰んでないので、m2で詰みを逃れている。 This->undo_move(m2); goto NEXT_CHECK; } This->undo_move(m2); } // すべて詰んだ This->undo_move(m); // mによって3手で詰む。 return m; NEXT_CHECK:; This->undo_move(m); } } return MOVE_NONE; }
Move MovePicker::next_move2() { #endif Move move; // 以下、caseのfall throughを駆使して書いてある。 switch (stage) { // 置換表の指し手を返すフェーズ case MAIN_SEARCH: case EVASION: case QSEARCH_WITH_CHECKS: case QSEARCH_NO_CHECKS: case PROBCUT: ++stage; return ttMove; case CAPTURES_INIT: endBadCaptures = cur = moves; endMoves = generateMoves<CAPTURES_PRO_PLUS>(pos, cur); score<CAPTURES>(); // CAPTUREの指し手の並べ替え。 ++stage; /* fallthrough */ // 置換表の指し手を返したあとのフェーズ // (killer moveの前のフェーズなのでkiller除去は不要) // SSEの符号がマイナスのものはbad captureのほうに回す。 case GOOD_CAPTURES: while (cur < endMoves) { move = pick_best(cur++, endMoves); if (move != ttMove) { // ここでSSEの符号がマイナスならbad captureのほうに回す。 // ToDo: moveは駒打ちではないからsee()の内部での駒打ちは判定不要なのだが。 if (pos.see_ge(move, VALUE_ZERO)) return move; // 損をするCAPTUREの指し手は、後回しにする。 *endBadCaptures++ = move; } } ++stage; // 1つ目のkiller move // ※ killer[]は32bit化されている(上位に移動後の駒が格納されている)と仮定している。 move = killers[0]; if ( move != MOVE_NONE && move != ttMove && pos.pseudo_legal_s<false>(move) && !pos.capture_or_pawn_promotion(move)) return move; /* fallthrough */ // killer moveを返すフェーズ // (直前に置換表の指し手を返しているし、CAPTURES_PRO_PLUSでの指し手も返しているのでそれらの指し手は除外されるべき) case KILLERS: ++stage; move = killers[1]; // 2つ目のkiller move if ( move != MOVE_NONE // ss->killer[0],[1]からコピーしただけなのでMOVE_NONEの可能性がある && move != ttMove // 置換表の指し手を重複除去しないといけない && pos.pseudo_legal_s<false>(move) // pseudo_legalでない指し手以外に歩や大駒の不成なども除外 && !pos.capture_or_pawn_promotion(move)) // 直前にCAPTURES_PRO_PLUSで生成している指し手を除外 return move; /* fallthrough */ // counter moveを返すフェーズ case COUNTERMOVE: ++stage; move = countermove; if ( move != MOVE_NONE && move != ttMove && move != killers[0] && move != killers[1] && pos.pseudo_legal_s<false>(move) && !pos.capture_or_pawn_promotion(move)) return move; /* fallthrough */ case QUIET_INIT: cur = endBadCaptures; endMoves = generateMoves<NON_CAPTURES_PRO_MINUS>(pos, cur); score<QUIETS>(); // 指し手を部分的にソートする。depthに線形に依存する閾値で。 partial_insertion_sort(cur, endMoves, -4000 * depth / ONE_PLY); ++stage; /* fallthrough */ // 捕獲しない指し手を返す。 // (置換表の指し手とkillerの指し手は返したあとなのでこれらの指し手は除外する必要がある) // ※ これ、指し手の数が多い場合、AVXを使って一気に削除しておいたほうが良いのでは.. case QUIET: while (cur < endMoves && (!skipQuiets || cur->value >= VALUE_ZERO)) { move = *cur++; if (move != ttMove && move != killers[0] && move != killers[1] && move != countermove) return move; } ++stage; // bad capturesの先頭を指すようにする。これは指し手生成バッファの先頭付近を再利用している。 cur = moves; /* fallthrough */ // see()が負の指し手を返す。 case BAD_CAPTURES: if (cur < endBadCaptures) return *cur++; break; // ここでcaseのfall throughは終わり。 // 回避手の生成 case EVASIONS_INIT: cur = moves; endMoves = generateMoves<EVASIONS>(pos, cur); score<EVASIONS>(); ++stage; /* fallthrough */ // 王手回避の指し手を返す case ALL_EVASIONS: while (cur < endMoves) { move = pick_best(cur++, endMoves); if (move != ttMove) return move; } break; case PROBCUT_INIT: cur = moves; endMoves = generateMoves<CAPTURES_PRO_PLUS>(pos, cur); score<CAPTURES>(); ++stage; /* fallthrough */ // 通常探索のProbCutの処理から呼び出されるとき用。 // 直前に捕獲された駒の価値以上のcaptureの指し手のみを生成する。 case PROBCUT_CAPTURES: while (cur < endMoves) { move = pick_best(cur++, endMoves); if (move != ttMove && pos.see_ge(move, threshold)) return move; } break; // 捕獲する指し手のみを生成 case QCAPTURES_1_INIT: case QCAPTURES_2_INIT: cur = moves; endMoves = generateMoves<CAPTURES_PRO_PLUS>(pos, cur); score<CAPTURES>(); ++stage; /* fallthrough */ // 残りの指し手を生成するフェーズ(共通処理) case QCAPTURES_1: case QCAPTURES_2: while (cur < endMoves) { move = pick_best(cur++, endMoves); if (move != ttMove) return move; } if (stage == QCAPTURES_2) break; cur = moves; // CAPTURES_PRO_PLUSで生成していたので、歩の成る指し手は除外された成る指し手+王手の指し手生成が必要。 // QUIET_CHECKS_PRO_MINUSがあれば良いのだが、実装が難しいので、このあと除外する。 endMoves = generateMoves<QUIET_CHECKS>(pos, cur); ++stage; /* fallthrough */ // 王手になる指し手を一手ずつ返すフェーズ // (置換表の指し手は返したあとなのでこの指し手は除外する必要がある) case QCHECKS: while (cur < endMoves) { move = cur++->move; if (move != ttMove && !pos.pawn_promotion(move) ) return move; } break; case QSEARCH_RECAPTURES: cur = moves; endMoves = generateMoves<RECAPTURES>(pos, moves, recaptureSquare); score<CAPTURES>(); // CAPTUREの指し手の並べ替え ++stage; /* fallthrough */ // 取り返す指し手。これはすでにrecaptureの指し手だけが生成されているのでそのまま返す。 case QRECAPTURES: while (cur < endMoves) { // recaptureの指し手が2つ以上あることは稀なのでここでオーダリングしてもあまり意味をなさないが、 // 生成される指し手自体が少ないなら、pick_best()のコストはほぼ無視できるのでこれはやらないよりはマシ。 move = pick_best(cur++, endMoves); //if (to_sq(move) == recaptureSquare) // return move; // → recaptureの指し手のみを生成しているのでこの判定は不要。 ASSERT_LV3(to_sq(move) == recaptureSquare); return move; } break; default: UNREACHABLE; return MOVE_NONE; } return MOVE_NONE; }
TTEntry* TranspositionTable::probe(const Key key, bool& found #if defined(USE_GLOBAL_OPTIONS) , size_t thread_id #endif ) const { ASSERT_LV3(clusterCount != 0); #if defined(USE_GLOBAL_OPTIONS) if (!GlobalOptions.use_hash_probe) { // 置換表にhitさせないモードであるなら、見つからなかったことにして // つねに確保しているメモリの先頭要素を返せば良い。(ここに書き込まれたところで問題ない) return found = false, first_entry(0); } #endif // 最初のTT_ENTRYのアドレス(このアドレスからTT_ENTRYがClusterSize分だけ連なっている) // keyの下位bitをいくつか使って、このアドレスを求めるので、自ずと下位bitはいくらかは一致していることになる。 TTEntry* tte; u8 gen8; #if !defined(USE_GLOBAL_OPTIONS) tte = first_entry(key); gen8 = generation(); #else if (GlobalOptions.use_per_thread_tt) { // スレッドごとに置換表の異なるエリアを渡す必要がある。 // 置換表にはclusterCount個のクラスターがあるのでこれをスレッドの個数で均等に割って、 // そのthread_id番目のblockを使わせてあげる、的な考え。 // // ただしkeyのbit0は手番bitであり、これはそのままindexのbit0に反映されている必要がある。 // // また、blockは2の倍数になるように下丸めしておく。 // ・上丸めするとblock*max_thread > clusterCountになりかねない) // ・2の倍数にしておかないと、(key % block)にkeyのbit0を反映させたときにこの値がblockと同じ値になる。 // (各スレッドが使えるのは、( 0~(block-1) ) + (thread_id * block)のTTEntryなので、これはまずい。 size_t block = (clusterCount / max_thread) & ~1; size_t index = (((size_t)key % block) & ~1 ) | ((size_t)key & 1); tte = &table[index + thread_id * block].entry[0]; } else { tte = first_entry(key); } gen8 = generation(thread_id); #endif // 上位16bitが合致するTT_ENTRYを探す const uint16_t key16 = key >> 48; // クラスターのなかから、keyが合致するTT_ENTRYを探す for (int i = 0; i < ClusterSize; ++i) { // returnする条件 // 1. 空のエントリーを見つけた(そこまではkeyが合致していないので、found==falseにして新規TT_ENTRYのアドレスとして返す) // 2. keyが合致しているentryを見つけた。(found==trueにしてそのTT_ENTRYのアドレスを返す) // Stockfishのコードだと、1.が成立したタイミングでもgenerationのrefreshをしているが、 // save()のときにgenerationを書き出すため、このケースにおいてrefreshは必要ない。 // 1. if (!tte[i].key16) return found = false, &tte[i]; // 2. if (tte[i].key16 == key16) { #if defined(USE_GLOBAL_OPTIONS) // 置換表とTTEntryの世代が異なるなら、信用できないと仮定するフラグ。 if (GlobalOptions.use_strict_generational_tt) if (tte[i].generation() != gen8) return found = false, &tte[i]; #endif tte[i].set_generation(gen8); // Refresh return found = true, &tte[i]; } } // 空きエントリーも、探していたkeyが格納されているentryが見当たらなかった。 // クラスター内のどれか一つを潰す必要がある。 TTEntry* replace = tte; for (int i = 1; i < ClusterSize; ++i) // ・深い探索の結果であるものほど価値があるので残しておきたい。depth8 × 重み1.0 // ・generationがいまの探索generationに近いものほど価値があるので残しておきたい。geration(4ずつ増える)×重み 2.0 // 以上に基いてスコアリングする。 // 以上の合計が一番小さいTTEntryを使う。 if (replace->depth8 - ((259 + gen8 - replace->genBound8) & 0xFC) * 2 > tte[i].depth8 - ((259 + gen8 - tte[i].genBound8) & 0xFC) * 2) replace = &tte[i]; // generationは256になるとオーバーフローして0になるのでそれをうまく処理できなければならない。 // a,bが8bitであるとき ( 256 + a - b ) & 0xff のようにすれば、オーバーフローを考慮した引き算が出来る。 // このテクニックを用いる。 // いま、 // a := generationは下位2bitは用いていないので0。 // b := genBound8は下位2bitにはBoundが入っているのでこれはゴミと考える。 // ( 256 + a - b + c) & 0xfc として c = 3としても結果に影響は及ぼさない、かつ、このゴミを無視した計算が出来る。 return found = false, replace; }
Value calc_diff_kpp(const Position& pos) { // 過去に遡って差分を計算していく。 auto st = pos.state(); // すでに計算されている。rootか? EvalSum sum; if (st->sum.p[2][0] != INT_MAX) { sum = st->sum; goto CALC_DIFF_END; } // 遡るのは一つだけ // ひとつずつ遡りながらsumKPPがVALUE_NONEでないところまで探してそこからの差分を計算することは出来るが // レアケースだし、StateInfoにEvalListを持たせる必要が出てきて、あまり得しない。 auto now = st; auto prev = st->previous; if (prev->sum.p[2][0] == INT_MAX) { // 全計算 return compute_eval(pos); } // この差分を求める { sum = prev->sum; int k0, k1, k2, k3; auto sq_bk0 = pos.king_square(BLACK); auto sq_wk0 = pos.king_square(WHITE); auto sq_wk1 = Inv(pos.king_square(WHITE)); auto now_list_fb = pos.eval_list()->piece_list_fb(); auto now_list_fw = pos.eval_list()->piece_list_fw(); int i, j; auto& dp = now->dirtyPiece; // 移動させた駒は最大2つある。その数 int k = dp.dirty_num; auto dirty = dp.pieceNo[0]; if (dirty >= PIECE_NO_KING) // 王と王でないかで場合分け { if (dirty == PIECE_NO_BKING) { // ---------------------------- // 先手玉が移動したときの計算 // ---------------------------- // 現在の玉の位置に移動させて計算する。 // 先手玉に関するKKP,KPPは全計算なので一つ前の値は関係ない。 // BKPP sum.p[0][0] = 0; sum.p[0][1] = 0; // このときKKPは差分で済まない。 sum.p[2] = Eval::kk[sq_bk0][sq_wk0]; // 片側まるごと計算 for (i = 0; i < PIECE_NO_KING; i++) { k0 = now_list_fb[i]; sum.p[2] += Eval::kkp[sq_bk0][sq_wk0][k0]; for (j = 0; j < i; j++) sum.p[0] += Eval::kpp[sq_bk0][k0][now_list_fb[j]]; } // もうひとつの駒がないならこれで計算終わりなのだが。 if (k == 2) { // この駒についての差分計算をしないといけない。 k1 = dp.piecePrevious[1].fw; k3 = dp.pieceNow[1].fw; dirty = dp.pieceNo[1]; // BKPPはすでに計算済みなのでWKPPのみ。 // WKは移動していないのでこれは前のままでいい。 for (i = 0; i < dirty; ++i) { sum.p[1] -= Eval::kpp[sq_wk1][k1][now_list_fw[i]]; sum.p[1] += Eval::kpp[sq_wk1][k3][now_list_fw[i]]; } for (++i; i < PIECE_NO_KING; ++i) { sum.p[1] -= Eval::kpp[sq_wk1][k1][now_list_fw[i]]; sum.p[1] += Eval::kpp[sq_wk1][k3][now_list_fw[i]]; } } } else { // ---------------------------- // 後手玉が移動したときの計算 // ---------------------------- ASSERT_LV3(dirty == PIECE_NO_WKING); // WKPP sum.p[1][0] = 0; sum.p[1][1] = 0; sum.p[2] = Eval::kk[sq_bk0][sq_wk0]; for (i = 0; i < PIECE_NO_KING; i++) { k0 = now_list_fb[i]; // これ、KKPテーブルにk1側も入れておいて欲しい気はするが.. k1 = now_list_fw[i]; sum.p[2] += Eval::kkp[sq_bk0][sq_wk0][k0]; for (j = 0; j < i; j++) sum.p[1] += Eval::kpp[sq_wk1][k1][now_list_fw[j]]; } if (k == 2) { k0 = dp.piecePrevious[1].fb; k2 = dp.pieceNow[1].fb; dirty = dp.pieceNo[1]; for (i = 0; i < dirty; ++i) { sum.p[0] -= Eval::kpp[sq_bk0][k0][now_list_fb[i]]; sum.p[0] += Eval::kpp[sq_bk0][k2][now_list_fb[i]]; } for (++i; i < PIECE_NO_KING; ++i) { sum.p[0] -= Eval::kpp[sq_bk0][k0][now_list_fb[i]]; sum.p[0] += Eval::kpp[sq_bk0][k2][now_list_fb[i]]; } } } } else { // ---------------------------- // 玉以外が移動したときの計算 // ---------------------------- #define ADD_BWKPP(W0,W1,W2,W3) { \ sum.p[0] -= Eval::kpp[sq_bk0][W0][now_list_fb[i]]; \ sum.p[1] -= Eval::kpp[sq_wk1][W1][now_list_fw[i]]; \ sum.p[0] += Eval::kpp[sq_bk0][W2][now_list_fb[i]]; \ sum.p[1] += Eval::kpp[sq_wk1][W3][now_list_fw[i]]; \ } if (k == 1) { // 移動した駒が一つ。 k0 = dp.piecePrevious[0].fb; k1 = dp.piecePrevious[0].fw; k2 = dp.pieceNow[0].fb; k3 = dp.pieceNow[0].fw; // KKP差分 sum.p[2] -= Eval::kkp[sq_bk0][sq_wk0][k0]; sum.p[2] += Eval::kkp[sq_bk0][sq_wk0][k2]; // KP値、要らんのでi==dirtyを除く for (i = 0; i < dirty; ++i) ADD_BWKPP(k0, k1, k2, k3); for (++i; i < PIECE_NO_KING; ++i) ADD_BWKPP(k0, k1, k2, k3); } else if (k == 2) { // 移動する駒が王以外の2つ。 PieceNo dirty2 = dp.pieceNo[1]; if (dirty > dirty2) swap(dirty, dirty2); // PIECE_NO_ZERO <= dirty < dirty2 < PIECE_NO_KING // にしておく。 k0 = dp.piecePrevious[0].fb; k1 = dp.piecePrevious[0].fw; k2 = dp.pieceNow[0].fb; k3 = dp.pieceNow[0].fw; int m0, m1, m2, m3; m0 = dp.piecePrevious[1].fb; m1 = dp.piecePrevious[1].fw; m2 = dp.pieceNow[1].fb; m3 = dp.pieceNow[1].fw; // KKP差分 sum.p[2] -= Eval::kkp[sq_bk0][sq_wk0][k0]; sum.p[2] += Eval::kkp[sq_bk0][sq_wk0][k2]; sum.p[2] -= Eval::kkp[sq_bk0][sq_wk0][m0]; sum.p[2] += Eval::kkp[sq_bk0][sq_wk0][m2]; // KPP差分 for (i = 0; i < dirty; ++i) { ADD_BWKPP(k0, k1, k2, k3); ADD_BWKPP(m0, m1, m2, m3); } for (++i; i < dirty2; ++i) { ADD_BWKPP(k0, k1, k2, k3); ADD_BWKPP(m0, m1, m2, m3); } for (++i; i < PIECE_NO_KING; ++i) { ADD_BWKPP(k0, k1, k2, k3); ADD_BWKPP(m0, m1, m2, m3); } sum.p[0] -= Eval::kpp[sq_bk0][k0][m0]; sum.p[1] -= Eval::kpp[sq_wk1][k1][m1]; sum.p[0] += Eval::kpp[sq_bk0][k2][m2]; sum.p[1] += Eval::kpp[sq_wk1][k3][m3]; } } } now->sum = sum; // 差分計算終わり CALC_DIFF_END:; sum.p[2][0] += pos.state()->materialValue * FV_SCALE; return Value(sum.sum(pos.side_to_move()) / FV_SCALE); }
void init_min_index_flag() { // mir_piece、inv_pieceの初期化が終わっていなければならない。 ASSERT_LV1(mir_piece(Eval::f_hand_pawn) == Eval::f_hand_pawn); // 次元下げ用フラグ配列の初期化 // KPPPに関しては関与しない。 KK g_kk; g_kk.set(SQ_NB, Eval::fe_end, 0); KKP g_kkp; g_kkp.set(SQ_NB, Eval::fe_end, g_kk.max_index()); KPP g_kpp; g_kpp.set(SQ_NB, Eval::fe_end, g_kkp.max_index()); u64 size = g_kpp.max_index(); min_index_flag.resize(size); #pragma omp parallel { #if defined(_OPENMP) // Windows環境下でCPUが2つあるときに、論理64コアまでしか使用されないのを防ぐために // ここで明示的にCPUに割り当てる int thread_index = omp_get_thread_num(); // 自分のthread numberを取得 WinProcGroup::bindThisThread(thread_index); #endif #pragma omp for schedule(dynamic,20000) for (s64 index_ = 0; index_ < (s64)size; ++index_) { // OpenMPの制約からループ変数は符号型でないといけないらしいのだが、 // さすがに使いにくい。 u64 index = (u64)index_; if (g_kk.is_ok(index)) { // indexからの変換と逆変換によって元のindexに戻ることを確認しておく。 // 起動時に1回しか実行しない処理なのでASSERT_LV1で書いておく。 ASSERT_LV1(g_kk.fromIndex(index).toIndex() == index); KK a[KK_LOWER_COUNT]; g_kk.fromIndex(index).toLowerDimensions(a); // 次元下げの1つ目の要素が元のindexと同一であることを確認しておく。 ASSERT_LV1(a[0].toIndex() == index); u64 min_index = UINT64_MAX; for (auto& e : a) min_index = std::min(min_index, e.toIndex()); min_index_flag[index] = (min_index == index); } else if (g_kkp.is_ok(index)) { ASSERT_LV1(g_kkp.fromIndex(index).toIndex() == index); KKP x = g_kkp.fromIndex(index); KKP a[KKP_LOWER_COUNT]; x.toLowerDimensions(a); ASSERT_LV1(a[0].toIndex() == index); u64 min_index = UINT64_MAX; for (auto& e : a) min_index = std::min(min_index, e.toIndex()); min_index_flag[index] = (min_index == index); } else if (g_kpp.is_ok(index)) { ASSERT_LV1(g_kpp.fromIndex(index).toIndex() == index); KPP x = g_kpp.fromIndex(index); KPP a[KPP_LOWER_COUNT]; x.toLowerDimensions(a); ASSERT_LV1(a[0].toIndex() == index); u64 min_index = UINT64_MAX; for (auto& e : a) min_index = std::min(min_index, e.toIndex()); min_index_flag[index] = (min_index == index); } else { ASSERT_LV3(false); } } } }
Value calc_diff_kpp(const Position& pos) { // 過去に遡って差分を計算していく。 auto st = pos.state(); // すでに計算されている。rootか? int sumKKP, sumBKPP, sumWKPP; if (st->sumKKP != INT_MAX) { sumKKP = st->sumKKP; sumBKPP = st->sumBKPP; sumWKPP = st->sumWKPP; goto CALC_DIFF_END; } #ifdef USE_EHASH HASH_KEY key = st->key(); { auto e = ehash[key & (EHASH_SIZE - 1)]; if (e.key == key) { // hitしたのでこのまま返す st->sumKKP = sumKKP = e.sumKKP; st->sumBKPP = sumBKPP = e.sumBKPP; st->sumWKPP = sumWKPP = e.sumWKPP; goto CALC_DIFF_END; } } #endif // 遡るのは一つだけ // ひとつずつ遡りながらsumKPPがVALUE_NONEでないところまで探してそこからの差分を計算することは出来るが // レアケースだし、StateInfoにEvalListを持たせる必要が出てきて、あまり得しない。 auto now = st; auto prev = st->previous; if (prev->sumKKP == INT_MAX) { #ifdef USE_EHASH HASH_KEY key2 = prev->key(); auto e = ehash[key2 & (EHASH_SIZE - 1)]; if (e.key == key2) { // hitしたのでここからの差分計算を行なう。 prev->sumKKP = e.sumKKP; prev->sumBKPP = e.sumBKPP; prev->sumWKPP = e.sumWKPP; ASSERT_LV3(e.sumKKP != INT_MAX); } else #endif { // 全計算 compute_eval(pos); sumKKP = now->sumKKP; sumBKPP = now->sumBKPP; sumWKPP = now->sumWKPP; goto CALC_DIFF_END; } } // この差分を求める { sumKKP = prev->sumKKP; sumBKPP = prev->sumBKPP; sumWKPP = prev->sumWKPP; int k0, k1, k2, k3; auto sq_bk0 = pos.king_square(BLACK); auto sq_wk1 = Inv(pos.king_square(WHITE)); auto now_list_fb = pos.eval_list()->piece_list_fb(); auto now_list_fw = pos.eval_list()->piece_list_fw(); int i, j; auto& dp = now->dirtyPiece; // 移動させた駒は最大2つある。その数 int k = dp.dirty_num; auto dirty = dp.pieceNo[0]; if (dirty >= PIECE_NO_KING) // 王と王でないかで場合分け { if (dirty == PIECE_NO_BKING) { // ---------------------------- // 先手玉が移動したときの計算 // ---------------------------- // 現在の玉の位置に移動させて計算する。 // 先手玉に関するKKP,KPPは全計算なので一つ前の値は関係ない。 sumBKPP = 0; // このときKKPは差分で済まない。 sumKKP = Eval::kkp[sq_bk0][sq_wk1][fe_end]; // 片側まるごと計算 for (i = 0; i < PIECE_NO_KING; i++) { k0 = now_list_fb[i]; sumKKP += Eval::kkp[sq_bk0][sq_wk1][k0]; for (j = 0; j < i; j++) sumBKPP += Eval::kpp[sq_bk0][k0][now_list_fb[j]]; } // もうひとつの駒がないならこれで計算終わりなのだが。 if (k == 2) { // この駒についての差分計算をしないといけない。 k1 = dp.piecePrevious[1].fw; k3 = dp.pieceNow[1].fw; dirty = dp.pieceNo[1]; // BKPPはすでに計算済みなのでWKPPのみ。 // WKは移動していないのでこれは前のままでいい。 for (i = 0; i < dirty; ++i) { sumWKPP += Eval::kpp[sq_wk1][k1][now_list_fw[i]]; sumWKPP -= Eval::kpp[sq_wk1][k3][now_list_fw[i]]; } for (++i; i < PIECE_NO_KING; ++i) { sumWKPP += Eval::kpp[sq_wk1][k1][now_list_fw[i]]; sumWKPP -= Eval::kpp[sq_wk1][k3][now_list_fw[i]]; } } } else { // ---------------------------- // 後手玉が移動したときの計算 // ---------------------------- ASSERT_LV3(dirty == PIECE_NO_WKING); sumWKPP = 0; sumKKP = Eval::kkp[sq_bk0][sq_wk1][fe_end]; for (i = 0; i < PIECE_NO_KING; i++) { k0 = now_list_fb[i]; // これ、KKPテーブルにk1側も入れておいて欲しい気はするが.. k1 = now_list_fw[i]; sumKKP += Eval::kkp[sq_bk0][sq_wk1][k0]; for (j = 0; j < i; j++) sumWKPP -= Eval::kpp[sq_wk1][k1][now_list_fw[j]]; } if (k == 2) { k0 = dp.piecePrevious[1].fb; k2 = dp.pieceNow[1].fb; dirty = dp.pieceNo[1]; for (i = 0; i < dirty; ++i) { sumBKPP -= Eval::kpp[sq_bk0][k0][now_list_fb[i]]; sumBKPP += Eval::kpp[sq_bk0][k2][now_list_fb[i]]; } for (++i; i < PIECE_NO_KING; ++i) { sumBKPP -= Eval::kpp[sq_bk0][k0][now_list_fb[i]]; sumBKPP += Eval::kpp[sq_bk0][k2][now_list_fb[i]]; } } } } else { // ---------------------------- // 玉以外が移動したときの計算 // ---------------------------- #define ADD_BWKPP(W0,W1,W2,W3) { \ sumBKPP -= Eval::kpp[sq_bk0][W0][now_list_fb[i]]; \ sumWKPP += Eval::kpp[sq_wk1][W1][now_list_fw[i]]; \ sumBKPP += Eval::kpp[sq_bk0][W2][now_list_fb[i]]; \ sumWKPP -= Eval::kpp[sq_wk1][W3][now_list_fw[i]]; \ } if (k == 1) { // 移動した駒が一つ。 k0 = dp.piecePrevious[0].fb; k1 = dp.piecePrevious[0].fw; k2 = dp.pieceNow[0].fb; k3 = dp.pieceNow[0].fw; // KKP差分 sumKKP -= Eval::kkp[sq_bk0][sq_wk1][k0]; sumKKP += Eval::kkp[sq_bk0][sq_wk1][k2]; // KP値、要らんのでi==dirtyを除く for (i = 0; i < dirty; ++i) ADD_BWKPP(k0, k1, k2, k3); for (++i; i < PIECE_NO_KING; ++i) ADD_BWKPP(k0, k1, k2, k3); } else if (k == 2) { // 移動する駒が王以外の2つ。 PieceNo dirty2 = dp.pieceNo[1]; if (dirty > dirty2) swap(dirty, dirty2); // PIECE_NO_ZERO <= dirty < dirty2 < PIECE_NO_KING // にしておく。 k0 = dp.piecePrevious[0].fb; k1 = dp.piecePrevious[0].fw; k2 = dp.pieceNow[0].fb; k3 = dp.pieceNow[0].fw; int m0, m1, m2, m3; m0 = dp.piecePrevious[1].fb; m1 = dp.piecePrevious[1].fw; m2 = dp.pieceNow[1].fb; m3 = dp.pieceNow[1].fw; // KKP差分 sumKKP -= Eval::kkp[sq_bk0][sq_wk1][k0]; sumKKP += Eval::kkp[sq_bk0][sq_wk1][k2]; sumKKP -= Eval::kkp[sq_bk0][sq_wk1][m0]; sumKKP += Eval::kkp[sq_bk0][sq_wk1][m2]; // KPP差分 for (i = 0; i < dirty; ++i) { ADD_BWKPP(k0, k1, k2, k3); ADD_BWKPP(m0, m1, m2, m3); } for (++i; i < dirty2; ++i) { ADD_BWKPP(k0, k1, k2, k3); ADD_BWKPP(m0, m1, m2, m3); } for (++i; i < PIECE_NO_KING; ++i) { ADD_BWKPP(k0, k1, k2, k3); ADD_BWKPP(m0, m1, m2, m3); } sumBKPP -= Eval::kpp[sq_bk0][k0][m0]; sumWKPP += Eval::kpp[sq_wk1][k1][m1]; sumBKPP += Eval::kpp[sq_bk0][k2][m2]; sumWKPP -= Eval::kpp[sq_wk1][k3][m3]; } } } now->sumKKP = sumKKP; now->sumBKPP = sumBKPP; now->sumWKPP = sumWKPP; #ifdef USE_EHASH // せっかく計算したのでehashに保存しておく。 { EvalHash e; e.sumKKP = Value(sumKKP); e.sumBKPP = Value(sumBKPP); e.sumWKPP = Value(sumWKPP); e.key = key; ehash[key & (EHASH_SIZE - 1)] = e; } #endif // 差分計算終わり CALC_DIFF_END:; return (Value)((sumBKPP + sumWKPP + sumKKP / FV_SCALE_KKP) / FV_SCALE); }
Value search(Position& pos, Value alpha, Value beta, Depth depth) { ASSERT_LV3(alpha < beta); // ----------------------- // nodeの種類 // ----------------------- // root nodeであるか const bool RootNode = NT == Root; // PV nodeであるか(root nodeはPV nodeに含まれる) const bool PvNode = NT == PV || NT == Root; // ----------------------- // 変数宣言 // ----------------------- // 現在のnodeのrootからの手数。これカウンターが必要。 // nanoだとこのカウンター持ってないので適当にごまかす。 const int ply_from_root = (pos.this_thread()->rootDepth - depth / ONE_PLY) + 1; // ----------------------- // 置換表のprobe // ----------------------- auto key = pos.state()->key(); bool ttHit; // 置換表がhitしたか TTEntry* tte = TT.probe(key, ttHit); // 置換表上のスコア // 置換表にhitしなければVALUE_NONE Value ttValue = ttHit ? value_from_tt(tte->value(), ply_from_root) : VALUE_NONE; auto thisThread = pos.this_thread(); // 置換表の指し手 // 置換表にhitしなければMOVE_NONE // RootNodeであるなら、指し手は現在注目している1手だけであるから、それが置換表にあったものとして指し手を進める。 Move ttMove = RootNode ? thisThread->rootMoves[thisThread->PVIdx].pv[0] : ttHit ? tte->move() : MOVE_NONE; // 置換表の値による枝刈り if (!PvNode // PV nodeでは置換表の指し手では枝刈りしない(PV nodeはごくわずかしかないので..) && ttHit // 置換表の指し手がhitして && tte->depth() >= depth // 置換表に登録されている探索深さのほうが深くて && ttValue != VALUE_NONE // (VALUE_NONEだとすると他スレッドからTTEntryが読みだす直前に破壊された可能性がある) && (ttValue >= beta ? (tte->bound() & BOUND_LOWER) : (tte->bound() & BOUND_UPPER)) // ttValueが下界(真の評価値はこれより大きい)もしくはジャストな値で、かつttValue >= beta超えならbeta cutされる // ttValueが上界(真の評価値はこれより小さい)だが、tte->depth()のほうがdepthより深いということは、 // 今回の探索よりたくさん探索した結果のはずなので、今回よりは枝刈りが甘いはずだから、その値を信頼して // このままこの値でreturnして良い。 ) { return ttValue; } // ----------------------- // 1手ずつ指し手を試す // ----------------------- pos.check_info_update(); MovePicker mp(pos,ttMove); Value value; Move move; StateInfo si; // この局面でdo_move()された合法手の数 int moveCount = 0; Move bestMove = MOVE_NONE; while (move = mp.nextMove()) { // root nodeでは、rootMoves()の集合に含まれていない指し手は探索をスキップする。 if (RootNode && !std::count(thisThread->rootMoves.begin() + thisThread->PVIdx, thisThread->rootMoves.end(), move)) continue; // legal()のチェック。root nodeだとlegal()だとわかっているのでこのチェックは不要。 if (!RootNode && !pos.legal(move)) continue; // ----------------------- // 1手進める // ----------------------- pos.do_move(move, si, pos.gives_check(move)); // do_moveした指し手の数のインクリメント ++moveCount; // ----------------------- // 再帰的にsearchを呼び出す // ----------------------- // PV nodeの1つ目の指し手で進めたnodeは、PV node。さもなくば、non PV nodeとして扱い、 // alphaの値を1でも超えるかどうかだけが問題なので簡単なチェックで済ませる。 // また、残り探索深さがなければ静止探索を呼び出して評価値を返す。 // (searchを再帰的に呼び出して、その先頭でチェックする呼び出しのオーバーヘッドが嫌なのでここで行なう) bool fullDepthSearch = (PV && moveCount == 1); if (!fullDepthSearch) { // nonPVならざっくり2手ぐらい深さを削っていいのでは..(本当はもっとちゃんとやるべき) Depth R = ONE_PLY * 2; value = depth - R < ONE_PLY ? -qsearch<NonPV>(pos, -beta, -alpha, depth - R) : -YaneuraOuNano::search<NonPV>(pos, -(alpha + 1), -alpha, depth - R); // 上の探索によりalphaを更新しそうだが、いい加減な探索なので信頼できない。まともな探索で検証しなおす fullDepthSearch = value > alpha; } if ( fullDepthSearch) value = depth - ONE_PLY < ONE_PLY ? -qsearch<PV>(pos, -beta, -alpha, depth - ONE_PLY) : -YaneuraOuNano::search<PV>(pos, -beta, -alpha, depth - ONE_PLY); // ----------------------- // 1手戻す // ----------------------- pos.undo_move(move); // 停止シグナルが来たら置換表を汚さずに終了。 if (Signals.stop) return VALUE_ZERO; // ----------------------- // root node用の特別な処理 // ----------------------- if (RootNode) { auto& rm = *std::find(thisThread->rootMoves.begin(), thisThread->rootMoves.end(), move); if (moveCount == 1 || value > alpha) { // root nodeにおいてPVの指し手または、α値を更新した場合、スコアをセットしておく。 // (iterationの終わりでsortするのでそのときに指し手が入れ替わる。) rm.score = value; rm.pv.resize(1); // PVは変化するはずなのでいったんリセット // ここにPVを代入するコードを書く。(か、置換表からPVをかき集めてくるか) } else { // root nodeにおいてα値を更新しなかったのであれば、この指し手のスコアを-VALUE_INFINITEにしておく。 // こうしておかなければ、stable_sort()しているにもかかわらず、前回の反復深化のときの値との // 大小比較してしまい指し手の順番が入れ替わってしまうことによるオーダリング性能の低下がありうる。 rm.score = -VALUE_INFINITE; } } // ----------------------- // alpha値の更新処理 // ----------------------- if (value > alpha) { alpha = value; bestMove = move; // αがβを上回ったらbeta cut if (alpha >= beta) break; } } // end of while // ----------------------- // 生成された指し手がない? // ----------------------- // 合法手がない == 詰まされている ので、rootの局面からの手数で詰まされたという評価値を返す。 if (moveCount == 0) alpha = mated_in(ply_from_root); // ----------------------- // 置換表に保存する // ----------------------- tte->save(key, value_to_tt(alpha, ply_from_root), alpha >= beta ? BOUND_LOWER : PvNode && bestMove ? BOUND_EXACT : BOUND_UPPER, // betaを超えているということはbeta cutされるわけで残りの指し手を調べていないから真の値はまだ大きいと考えられる。 // すなわち、このとき値は下界と考えられるから、BOUND_LOWER。 // さもなくば、(PvNodeなら)枝刈りはしていないので、これが正確な値であるはずだから、BOUND_EXACTを返す。 // また、PvNodeでないなら、枝刈りをしているので、これは正確な値ではないから、BOUND_UPPERという扱いにする。 // ただし、指し手がない場合は、詰まされているスコアなので、これより短い/長い手順の詰みがあるかも知れないから、 // すなわち、スコアは変動するかも知れないので、BOUND_UPPERという扱いをする。 depth, bestMove, VALUE_NONE,TT.generation()); return alpha; }