界面的作用

网页界面的作用不只是画出棋盘。它要减少用户的误输入,在等待服务器响应期间防止重复落子,还要能重新加载已保存的对局。尤其是在小屏手机上,不用一次点击就确定落子会更安全。

第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. 完整源代码与使用的模型”一文中查看。