画面の役割

Web画面は盤を描くだけでは終わりません。ユーザーの誤入力を減らし、サーバーの応答を待つ間は二重の着手を防ぎ、保存された対局を再び読み込む必要があります。特に小さなスマートフォンでは、一回のタッチで石を確定しないほうが安全です。

ステップ1: 二段階の着手を作ります

空いた交点を最初に押すと、金色の仮の石だけを表示します。同じ点をもう一度押すか「着手」ボタンを押したときにサーバーへ送信します。別の点を押すと仮の位置だけが変わります。サーバーの応答が来るまではボタンと盤の入力を一時的にロックし、同じ手が二回保存されないようにします。

ステップ2: サーバーを対局状態の原本にします

サーバーは着手を受け取ると、対局コードとアクセスキーを確認し、その手番かどうかを検査します。ルールエンジンを通過した手だけをトランザクションで保存した後、AIの手番を実行します。ブラウザが持つ盤をそのまま信じず、サーバーの最新の状態に新しい手を適用することで、改ざんや古い画面との衝突を防げます。

  1. 人の着手を検証して保存します。
  2. 保存された最新の盤でAIに次の手を要求します。
  3. AIの手をルールエンジンで検査します。
  4. AIの手と取りの結果を保存します。
  5. 完成した最新の状態を画面に返します。

ステップ3: 棋譜の閲覧と続きの対局を分けます

振り返り中に前後の矢印を押しても、実際の対局の最新の状態は変わってはいけません。過去の手を見ている状態で「ここから続ける」を選んだときだけ、その地点までコピーした新しい変化図を作ります。既存の手順をすぐに切り捨てると、ユーザーが誤って元の棋譜を失う恐れがあります。

ステップ4: 終局と整地を正直に表示します

双方が続けてパスするか、ユーザーが整地を選ぶと、空いた地、取った石、白のコミを計算できます。しかし盤上の死に石の死活を完全に判断できないなら、結果を「参考の整地」と表示すべきです。自動計算の限界を隠すより、何を数えたのかを説明するほうがユーザーにとって有用です。

公開前の確認リスト

  • 再読み込みの後に進行中の盤が復元されるか
  • 遅いネットワークで一手が二重に保存されないか
  • 別のアクセスキーで他人の対局を開けないか
  • 過去の手を見ている間に元の対局が変わらないか
  • エラーが起きたときに盤と手順がずれないか

ここまでつなげれば最初の完成形になります。その後は、実際の対局ログで見つかったシチョウ、虎の口、早すぎるパスのような失敗を一種類ずつ評価して直していけます。

この段階の実際のコード

振り返り画面は、保存された手順を最初から指定した手まで打ち直して盤を作ります。元の手順には触れないので、前後にめくって見ても実際の対局の最新の状態は変わりません。

mariago-web/lib/board.tstypescript · 44行
export type ReplayState = {
  board: Stone[];
  ko: Point | null;
  lastMove: Point | null;
  capB: number;
  capW: number;
  passStreak: number;
  toPlay: Stone;
};

/** 기보 0..upto 수까지 재현 (upto=0 이면 빈 판). */
export function replayMoves(
  moves: Array<{ color: 1 | 2; pass?: boolean; r?: number; c?: number }>,
  upto: number,
): ReplayState {
  let board = emptyBoard();
  let ko: Point | null = null;
  let lastMove: Point | null = null;
  let capB = 0;
  let capW = 0;
  let passStreak = 0;
  let toPlay: Stone = BLACK;
  const n = Math.max(0, Math.min(upto, moves.length));
  for (let i = 0; i < n; i++) {
    const m = moves[i];
    const who = m.color as Stone;
    if (m.pass) {
      passStreak += 1;
      lastMove = null;
    } else if (m.r != null && m.c != null) {
      const res = tryPlay(board, m.r, m.c, who, ko);
      if (res.ok) {
        board = res.board;
        ko = res.koPoint;
        lastMove = { r: m.r, c: m.c };
        if (who === BLACK) capB += res.captured;
        else capW += res.captured;
        passStreak = 0;
      }
    }
    toPlay = who === BLACK ? WHITE : BLACK;
  }
  return { board, ko, lastMove, capB, capW, passStreak, toPlay };
}

使用したモデル: Qwen3-VL-8B-Instruct + LoRA アダプター(lora_mix_qwen_r5)。全ソースとモデルの説明は「8. 全ソースコードと使用したモデル」の記事で見られます。