Skip to content

KataGo Review

A KataGo review turns every move in a finished game into a winrate / score delta, a classification (good / inaccuracy / mistake / blunder), and a list of suggested continuations. The unified flow uses one KataGo query per game instead of one per turn, which collapsed the typical N+2 round-trips into a single replay.

This page describes the flow end-to-end: where it is triggered, what runFullReview does, where the results land, and how the UI consumes them.

When a bot game ends:

  1. BotGameService.endGame marks the game ended in botGames.
  2. A BullMQ job is enqueued for BotToAnalysisProcessor (apps/weiqi-server/src/modules/bot/infrastructure/jobs/bot-to-analysis.processor.ts:37).
  3. The processor resolves the game’s boardNodes (the canonical board tree) and calls AnalysisService.runFullReview.

The legacy single-turn BotGameReviewProcessor still exists for back-compat but is no longer wired into the hot path.

AnalysisService.runFullReview (modules/analysis/application/analysis.service.ts:745):

  1. Walks the main line of boardNodes to collect every position.
  2. Builds one analyzeOptions with analyzeTurns: [0, 1, 2, …, N].
  3. Calls KatagoService.getGameReview(analyzeOptions).
  4. Persists results as one BoardAiReview row in board_ai_reviews plus one BoardAiSnapshot per (record, node, engine, hash) in board_ai_snapshots.
  5. Emits an AnalysisGraphPoint per turn with { turnNumber, winrate, scoreLead, bestMove, classification }.

KataGo accepts analyzeTurns: number[] to replay multiple positions in one process invocation. Internally it walks each turn, computes its root position, and returns one response per turn in a single KatagoAnalysisResponse.

Before this consolidation, each turn was a separate query (plus a pre-game probe and a post-game probe). For a 200-move game that was 202 round-trips through katago-service; now it is 1.

The downside is that very long games keep KataGo busy for longer per call — the queue’s concurrency cap (KATAGO_MAX_CONCURRENT) limits how many games can be in flight at once.

KatagoAnalysisResponse (modules/game/infrastructure/katago/types.ts:71):

{
jobId: string;
status: "ok" | "error";
error?: string;
turns?: Record<number, KatagoTurnResponse>; // 0-based ply keys
// legacy single-turn fields — mirror turns[0] when set
moveInfos?: KatagoMoveInfo[];
rootInfo?: KatagoRootInfo;
}
type KatagoTurnResponse = {
turnNumber: number;
moveInfos: KatagoMoveInfo[];
rootInfo: {
winrate: number; // 0..1, side-to-move perspective
scoreMean: number; // lead in points, side-to-move perspective
visits: number;
ownership?: number[]; // row-major, length boardSize², -1..1
};
};
type KatagoMoveInfo = {
move: string; // GTP coord, e.g. "Q16" or "pass"
visits: number;
winrate: number;
scoreMean: number;
scoreStdev: number;
pv: string[]; // principal variation
};

MoveClassification is derived from rootInfo.winrate and the played move’s winrate:

  • good — played move is within 1 % winrate of the engine’s top choice.
  • inaccuracy — 1–5 % gap.
  • mistake — 5–15 % gap.
  • blunder — > 15 % gap.

These thresholds are defined alongside the analysis service and surfaced to the client via MoveClass in apps/game-web/src/entities/bot-game/types/bot-game.ts.

Two tables, both keyed off the underlying board record:

  • board_ai_reviews — one row per review run. Stores engine, engineConfigHash, createdAt, and a JSON blob of the full response (so we can re-project without re-querying KataGo).
  • board_ai_snapshots — one row per (record, node, engine, hash). Used for “compare two snapshots” flows and for diffing engine versions.

The (recordId, nodeId, engine, engineConfigHash) unique key prevents duplicate snapshots when the same review is re-run.

  • GET /api/game/bot/games/:id/review (bot.controller.ts:299) returns the latest board_ai_reviews row, projected through projectAiReviewToBotReview for the bot-review UI. Falls back to legacy bot_game_reviews if no new review is present yet.
  • GET /api/analysis/games/:id/graph returns the time-series subset (turnNumber, winrate, scoreLead) for the chart component.

apps/game-web/src/pages/play/bots/review/bot-review.page.tsx renders:

  • A WinrateGraph for the per-turn winrate series.
  • A MoveList widget (apps/game-web/src/widgets/MoveList/ui/move-list.tsx:1) with classification badges per ply.
  • A GameOverModal widget (apps/game-web/src/widgets/GameOverModal/game-over-modal.tsx:1) on entry, with onNewGame, onReview, onRematch actions.

AnalysisGateway emits a Socket.IO event per turn as runFullReview progresses, so the chart can draw point-by-point instead of waiting for the whole review. This is wired only on the analysis page; the bot-review page waits for the full result before rendering (less noise, simpler state).