
検査の順序が必要な理由
AIが座標を正しく読んでも、その手が合法である保証はありません。ルールの検査は、モデルの確率から切り離された決定的なコードでなければなりません。同じ盤と同じ座標を入れたら常に同じ結果が出てこそ、テストや障害の再現ができます。
ステップ1: 範囲と空点を確認します
まず行と列が0〜18の範囲にあるかを検査します。次にその交点が空いているかを見ます。ここで失敗すれば盤をコピーしたり石を取り除いたりする必要がないので、最も安い検査を先に置きます。
ステップ2: 一時的な盤で石の取りを計算します
現在の盤をコピーして候補の座標に石を置き、上下左右の相手の石の塊を探します。呼吸点が0の塊は一時的な盤から取り除きます。複数の塊が同時に取られることもあるので、最初の取りで検査を終えてはいけません。
- 候補の石を一時的な盤に置きます。
- 隣接する相手の石の塊と呼吸点を探します。
- 呼吸点のない相手の塊をすべて取り除きます。
- 候補の石が属する自分の塊の呼吸点を数え直します。
ステップ3: 自殺手とコウを区別します
相手の石を取り除いた後も自分の塊の呼吸点が0なら、自殺手として拒否します。取りによって新しい呼吸点ができたなら合法手です。最後に、一時的な盤全体が直前の盤と同じかを比べ、すぐに取り返すコウを防ぎます。座標一つを永久に禁止する方法は、盤が変わった後の合法的な着手まで防いでしまうので、盤の状態を比べる必要があります。
ステップ4: 拒否の理由をモデルへの要求に返します
反則の座標を除外リストに入れるだけでなく、「すでに石がある」「自殺手」「コウの繰り返し」のように理由も一緒に記録します。再要求には拒否した座標を明示して、同じ答えの繰り返しを減らします。最大再試行回数を超えたら、サーバーは勝手な手を打つ代わりにユーザーに失敗を知らせ、盤と応答を障害ログに残します。
必ず作るテスト
- 隅の石一つが取られる盤
- 二つの塊を同時に取る盤
- 相手を取るので自殺手にならない手
- 本当に自殺手になる手
- すぐに取り返すことだけが禁止されるコウ
この五つの盤は、モデルなしでルール関数だけを呼んで確認できなければなりません。
この段階の実際のコード
Qt クライアントのルールエンジンです。範囲と空点を先に見て、コピーした盤で相手の石をすべて取った後に自殺手を検査し、最後に過去に現れた盤全体と比べてコウを防ぎます。拒否の理由は MoveRejectReason で返します。
src/goboard.cpp
bool GoBoard::isLegalMove(int row, int col, Stone who, MoveRejectReason *outReason) const
{
auto setReason = [outReason](MoveRejectReason r) {
if (outReason)
*outReason = r;
};
setReason(MoveRejectReason::None);
if (m_status != GameStatus::Playing || m_phase != GamePhase::Playing) {
setReason(MoveRejectReason::GameNotPlaying);
return false;
}
const int N = m_size;
if (!inBounds(N, row, col)) {
setReason(MoveRejectReason::OutOfBounds);
return false;
}
if (who != Stone::Black && who != Stone::White) {
setReason(MoveRejectReason::OutOfBounds);
return false;
}
if (m_cells[row * N + col] != Stone::Empty) {
setReason(MoveRejectReason::Occupied);
return false;
}
GoBoard sim = *this; // 복사 후 시뮬레이션
const Stone opp = opponent(who);
sim.m_cells[row * N + col] = who;
int captured = 0;
QVector<int> visited(N * N, 0);
for (int d = 0; d < 4; ++d) {
const int nr = row + kDR[d];
const int nc = col + kDC[d];
if (!inBounds(N, nr, nc))
continue;
const int nidx = nr * N + nc;
if (sim.m_cells[nidx] != opp || visited[nidx])
continue;
QVector<int> grp;
QVector<int> libs;
sim.collectGroup(nr, nc, &grp, &libs);
for (int gi : grp)
visited[gi] = 1;
if (libs.isEmpty()) {
for (int gi : grp) {
sim.m_cells[gi] = Stone::Empty;
++captured;
}
}
}
if (sim.liberties(row, col) == 0) {
setReason(MoveRejectReason::Suicide);
return false;
}
if (sim.wouldRecreatePastPosition(sim.packed())) {
setReason(MoveRejectReason::KoViolation);
return false;
}
Q_UNUSED(captured);
return true;
}使用したモデル: Qwen3-VL-8B-Instruct + LoRA アダプター(lora_mix_qwen_r5)。全ソースとモデルの説明は「8. 全ソースコードと使用したモデル」の記事で見られます。
COMMENTS · CHAPTER 04
AIが選んだ手をルールエンジンで検査する コメント 0
まだコメントはありません。気になった点や補足があれば残してください。
コメントを登録するには、Googleで始めてニックネームとメールアドレスを登録してください。
Googleで始める