// 基本思考ルーチン::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; } }
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; }