O papel da tela

A tela da web não se limita a desenhar o tabuleiro. Ela precisa reduzir os toques errados do usuário, impedir jogadas duplicadas enquanto espera a resposta do servidor e carregar de novo as partidas salvas. Principalmente em celulares pequenos, é mais seguro não confirmar uma pedra com um único toque.

Etapa 1: Criar a jogada em duas etapas

O primeiro toque em uma interseção vazia mostra apenas uma pedra de pré-visualização dourada. A jogada é enviada ao servidor quando o mesmo ponto é tocado de novo ou quando o botão "Jogar" é pressionado. Tocar em outro ponto só muda a posição da pré-visualização. Até o servidor responder, o botão e a entrada no tabuleiro ficam bloqueados por um momento para que a mesma jogada não seja salva duas vezes.

Etapa 2: Tornar o servidor a fonte oficial do estado da partida

Ao receber uma jogada, o servidor confere o código da partida e a chave de acesso e verifica se é mesmo a vez daquele jogador. Só as jogadas que passam pelo motor de regras são salvas em uma transação, e depois a vez da IA é executada. Não confie no tabuleiro que está no navegador; aplique a nova jogada ao último estado do servidor, para evitar adulterações e conflitos com telas desatualizadas.

  1. Validar e salvar a jogada do humano.
  2. Pedir à IA a próxima jogada usando o último tabuleiro salvo.
  3. Verificar a jogada da IA com o motor de regras.
  4. Salvar a jogada da IA e o resultado das capturas.
  5. Devolver à tela o estado mais recente já completo.

Etapa 3: Separar a revisão da continuação

Apertar as setas de voltar e avançar durante a revisão não pode mudar o último estado da partida real. Só quando "Continuar daqui" é escolhido enquanto se vê uma jogada passada é que criamos uma nova variação copiada até aquele ponto. Cortar imediatamente a sequência existente poderia fazer o usuário perder o registro original por engano.

Etapa 4: Mostrar com honestidade o fim e a contagem

Quando os dois lados passam seguidos ou o usuário pede a contagem, dá para calcular o território vazio, as pedras capturadas e o komi das brancas. Mas, se não for possível julgar completamente a vida e morte das pedras mortas no tabuleiro, o resultado deve ser indicado como "contagem de referência". Explicar o que foi contado é mais útil para o usuário do que esconder os limites do cálculo automático.

Lista de verificação antes de publicar

  • A partida em andamento é restaurada depois de atualizar a página?
  • Uma jogada nunca é salva duas vezes em uma rede lenta?
  • É impossível abrir a partida de outra pessoa com uma chave de acesso diferente?
  • A partida original continua igual enquanto se veem jogadas passadas?
  • O tabuleiro e a sequência de jogadas continuam consistentes quando ocorre um erro?

Ligando tudo até aqui, temos a primeira versão completa. A partir daí, as falhas encontradas nos logs de partidas reais, como escadas, boca de tigre e passes precoces, podem ser avaliadas e corrigidas um tipo de cada vez.

O código real desta etapa

A tela de revisão monta o tabuleiro reproduzindo a sequência salva desde o início até a jogada escolhida. A sequência original nunca é alterada, então avançar e voltar não muda o último estado da partida real.

mariago-web/lib/board.tstypescript · 44 linhas
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 };
}

Modelo usado: Qwen3-VL-8B-Instruct + adaptador LoRA (lora_mix_qwen_r5). O código-fonte completo e a descrição do modelo estão no post "8. O código-fonte completo e os modelos usados".