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]); }
void TranspositionTable::store(const Key posKey, const Score score, const Bound bound, Depth depth, Move move, const Score evalScore) { #ifdef OUTPUT_TRANSPOSITION_EXPIRATION_RATE ++numberOfSaves; #endif TTEntry* tte = firstEntry(posKey); TTEntry* replace = tte; const u32 posKeyHigh32 = posKey >> 32; if (depth < Depth0) { depth = Depth0; } for (int i = 0; i < ClusterSize; ++i, ++tte) { // 置換表が空か、keyが同じな古い情報が入っているとき if (!tte->key() || tte->key() == posKeyHigh32) { // move が無いなら、とりあえず古い情報でも良いので、他の指し手を保存する。 if (move.isNone()) { move = tte->move(); } tte->save(depth, score, move, posKeyHigh32, bound, this->generation(), evalScore); return; } int c = (replace->generation() == this->generation() ? 2 : 0); c += (tte->generation() == this->generation() || tte->type() == BoundExact ? -2 : 0); c += (tte->depth() < replace->depth() ? 1 : 0); if (0 < c) { replace = tte; } } #ifdef OUTPUT_TRANSPOSITION_EXPIRATION_RATE if (replace->key() != 0 && replace->key() != posKeyHigh32) { ++numberOfCacheExpirations; } #endif replace->save(depth, score, move, posKeyHigh32, bound, this->generation(), evalScore); }
// 基本思考ルーチン::depthMax手読み int AI::think(LightField &self, LightField &enemy, int depth, int timeLimit) { if (calls_count++ > 1000) { checkTime(); calls_count = 0; } // rootからの手数 int ply = depth_max_ - depth; #ifdef HASH TTEntry *tte; // 同一局面が発生するのは2手目以降しかありえない。 if (ply > 0) { bool tt_hit = TT.probe<true>(&self, nullptr, tte); // 局面が登録されていた if (tt_hit) { assert(tte->depth() >= 0); // 局面表に登録されている局面が、現在の局面の残り深さ以上 if (tte->depth() >= depth) { best_[depth] = tte->move(); // 局面表のスコアは信用できるのでそのまま返す return tte->score(); } } } #endif if (depth <= DEPTH_ZERO) return evalate(self, enemy, depth, timeLimit); else { int score; int max = -SCORE_INFINITE - 1; int movenum; int con_prev[3]; self.saveConnect(con_prev); Move move[22];// おける場所は最大で22種類なので // 置く場所なし == 負け if ((movenum = self.generateMoves(move)) == 0) return -SCORE_INFINITE; self.nextPlus(); #if defined(DEBUG) LightField f = self;// debug #endif for (int i = 0; i < movenum; i++) { // generateMovesでもとめた場所においてみる self.doMove<true>(move[i]); assert(self.key() == self.keyInit()); // 設置にかかる時間を計算 // 落下にかかる時間は、13 - 設置位置のyの小さいほう ちぎりなら、余計に時間がかかる int takeTime = (13 - std::min(toY(move[i].csq()), toY(move[i].psq()))) * FALLTIME + (move[i].isTigiri() ? TAKETIME_INCLUDETIGIRI : TAKETIME); if (self.flag(VANISH))// 消えるなら { score = evalVanish(self, enemy, depth, timeLimit) - takeTime; self.clearFlag(VANISH); } else// 消えないとき { if (timeLimit < takeTime) { // 致死量振るときに発火できなければ負け。 if (enemy.scoreMax() >= 30 * RATE || enemy.scoreMax() >= self.deadLine() * RATE) score = -SCORE_INFINITE; else score = think(self, enemy, depth - ONE_PLY, timeLimit - takeTime) - takeTime; } else score = think(self, enemy, depth - ONE_PLY, timeLimit - takeTime) - takeTime; } self.undoMove(move[i], con_prev); if (stop) return SCORE_ZERO; if (max < score) { max = score; best_[depth] = move[i];// もっとも評価の高い手を選ぶ } #if defined DEBUG assert(self == f); // debug::きちんともとの局面に戻せているか #endif } self.nextMinus(); #ifdef HASH if (ply > 0) { // この探索では後ろ二つの引数は使わない tte->save(self.key(), best_[depth].get(), max, depth, TT.generation(), BOUND_EXACT, 0, 0, 0); } #endif return max; } }
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; }
Score AI::search(Score alpha, Score beta, LightField& self, LightField& enemy, int depth, int my_remain_time) { if (calls_count++ > 100) { checkTime(); calls_count = 0; } node_searched++; const bool rootnode = NT == ROOT; assert(alpha >= -SCORE_INFINITE && alpha < beta && beta <= SCORE_INFINITE); // ルートからの手数 int ply = depth_max_ - depth; // 局面表を見る Move tt_move, best_move; Score tt_score; TTEntry* tte; const Key key = self.key() ^ enemy.key(); const bool tt_hit = TT.probe<false>(&self, &enemy, tte); if (stop) return SCORE_ZERO; // 局面表の指し手 tt_move = tt_hit ? tte->move() : rootnode ? root_moves[0].pv[0] : Move::moveNone(); // 置換表上のスコア tt_score = tt_hit ? tte->score() : SCORE_NONE; // 置換表のスコアが信用に足るならそのまま返す。 // 条件) // root nodeではない // 置換表にエントリーがあった // 置換表のエントリーのdepthが今回の残り探索depth以上である // 置換表のエントリーのスコアがSCORE_NONEではない。(置換表のアクセス競合のときにのみこの値になりうる) // 置換表のスコアのboundがBOUND_EXACTなら信用に足るので置換表の指し手をそのまま返す // non PV nodeであれば置換表の値がbetaを超えているならBOUND_LOWER(真の値はこれより上なのでこのときbeta cutできる)であるか、 // betaを超えていないのであれば、BOUND_UPPER(真の値はこれより下なのでこのときbeta cutが起きないことが確定)である。 if (!rootnode && tt_hit && tte->depth() >= depth && (depth == 0 || (tte->bound() == BOUND_EXACT || (tte->bound() & BOUND_LOWER && tt_score >= beta)))) { best_[depth] = tte->move(); assert(tte->move().isNone() || tte->move().isLegal(self)); // 局面表のスコアは信用できるのでそのまま返す return tt_score; } // 最大探索深さまでいったら、その局面の点数を返す if (depth <= DEPTH_ZERO) { Score s = evaluateEX(self, enemy, depth, my_remain_time); // せっかく評価関数を呼び出したので局面表に登録しておく。 tte->save(key, 0, s, 0, BOUND_NONE, TT.generation(), self.player(), my_remain_time, self.ojama() - enemy.ojama()); return s; } Move move[23]; int move_max = 0; Move *pmove = move; if (!tt_move.isNone()) { if (tt_move.isLegalAbout(self)) { // 置換表にある手を手のバッファの先頭に入れる。 *pmove++ = tt_move; move_max++; } } if ((move_max += self.generateMoves(pmove)) == 0) { // 置く場所なし == 負け return SCORE_MATED; } Score score, max = -SCORE_INFINITE; // お邪魔ぷよが降る場合はenemyのojamaを減らしているので、 // このmoveを調べ終わった後元に戻さなければならない int enemy_ojama_prev = enemy.ojama(); Flag enemy_flag = enemy.flag(); self.nextPlus(); // TODO:ムーブオーダリングしたい // 手がある間、すべての手を試す for (int i = 0; i < move_max; i++) { // 局面のコピーを用意 LightField self2(self); // 着手。連鎖が起きる場合は起きた後の形になるまで進めてしまう。 int put_time = self2.doMoveEX(move[i], my_remain_time, enemy); // 自分の残り時間が0以下なら、相手の手盤になると考える if (my_remain_time - put_time <= 0) { continue_self_num_ = 0; // 相手の探索をする。残り時間の超過分は、相手の残り時間になる。 score = -search<PV>(-beta, -alpha, enemy, self2, depth - 1, -(my_remain_time - put_time)); } else { continue_self_num_++; // 連続3回以上自分の探索をした場合はこれ以上深く探索しないために残り深さを減らす。(重すぎるので) if (continue_self_num_ >= 3) { score = search<PV>(alpha, beta, self2, enemy, 0, my_remain_time - put_time); } else { // 残り時間が残っている間は、連続して自分の探索をする score = search<PV>(alpha, beta, self2, enemy, depth - 1, my_remain_time - put_time); } continue_self_num_--; } enemy.setOjama(enemy_ojama_prev); enemy.resetFlag(enemy_flag); if (stop) return SCORE_ZERO; if (rootnode) { // Root nodeでの指し手のなかから、いま探索したばかりの指し手に関してそれがどこにあるかをfind()で探し、 // この指し手に付随するPV(最善応手列)を置換表から取り出して更新しておく。 // このfind()が失敗することは想定していない。 Search::RootMove& rm = *std::find(root_moves.begin(), root_moves.end(), move[i]); if (score > alpha) { rm.score = score; // 局面表をprobeするときにツモ番号を元に戻す必要があるので。 /*self.nextMinus(); rm.extractPvFromTT(self, enemy, my_remain_time); self.nextPlus();*/ } else { // tt_moveがセットされているなら一番最初に調べられたはず。 // ここにくるということはtt_moveが2回調べられているということ。 if (move[i] != tt_move) { rm.score = -SCORE_INFINITE; } } } if (score > max) { max = score; best_[depth] = move[i]; if (max > alpha) { alpha = max; if (alpha >= beta) { // βカットされたalphaという値は正確な値ではないが、そのノードの評価値は最低でもalpha以上だということがわかる。 // なので、alphaはそのノードの下限値である。だからBOUND_LOWER break; } } } } self.nextMinus(); // 最大でも評価値はmaxまでしか行かない tte->save(key, best_[depth].get(), max, depth, TT.generation(), max < beta ? BOUND_EXACT : BOUND_LOWER, self.player(), my_remain_time, self.ojama() - enemy.ojama()); return max; }