Skip to content

Trainer Mode Playback

The Trainer mode in the Dice Chess Trainer provides step-by-step game review with automatic playback of opponent moves. This page documents the playback algorithm and when the system pauses to allow the user to guess the next move.

When a player (training subject or opponent) receives a dice roll with NO allowed die values:

  1. Show dice (pause point) — the dice area shows all dice as blocked (red background) for 800ms.
  2. Display notice — the dice are replaced by a pulsed status message (e.g., “White has no legal moves”) for 1800ms.
  3. Auto-advance — the trainer automatically proceeds to the next turn boundary.

This ensures the user understands why the turn was skipped instead of the game just jumping forward unexpectedly.

The ActiveGameStore (in src/lib/activeGameStore.svelte.ts) computes turn boundaries — indices where the UI should pause for user interaction.

All heavy computed state in ActiveGameStore uses Svelte 5 $derived or $derived.by() fields to cache values reactively. This means historyBlocks, doublingStatusMessage, playerIds, boardOrientation, player names, and turn boundaries are only re-evaluated when their reactive dependencies change — preventing redundant computation on every render cycle.

Pre-Move Boundary (Subject’s Turn Start)

Section titled “Pre-Move Boundary (Subject’s Turn Start)”

A state is marked as a pre-move boundary when:

  • The active color matches the training subject’s color
  • A new dice roll has occurred (dice array differs from the previous state)
  • No move was recorded in the current state

A state is marked as a post-move boundary when:

  • The next state’s active color is the opponent’s color
  • The next state is a dice roll (new dice, no move recorded)

To support the “No legal moves” feedback sequence, extra boundaries are added:

  • Any subject roll with no legal moves.
  • Any opponent roll with no legal moves that follows a subject post-move boundary.

A die is considered “allowed” when:

  • dice.allowed === true (set by the API, indicating it can be used for a legal move)
  • dice.used === false (not yet consumed by a micro-move)

The logic: state.dices.some(d => d.allowed && !d.used)

If no die passes this check, the player cannot make a move and the post-move pause is skipped.

Trainer mode includes an opt-in frontend debug log stream for validating user move prediction against the recorded game state.

The logs are emitted by the browser frontend with console.info(...), so open your browser developer tools and inspect the Console tab.

  • Chrome / Edge / Firefox: F12 -> Console
  • Safari: Develop -> Show Web Inspector -> Console

All trainer debug messages are prefixed with:

[trainer-debug]

Debug logging is available in frontend development mode only. Production builds ignore these logs.

You can enable it in one of two ways.

Option 1: Enable it temporarily in the browser

Section titled “Option 1: Enable it temporarily in the browser”

Run this in the browser console:

window.__TRAINER_DEBUG__ = true

To disable it again:

window.__TRAINER_DEBUG__ = false

This is the fastest option when you want to inspect a single trainer session without restarting the dev server.

Option 2: Enable it through frontend environment config

Section titled “Option 2: Enable it through frontend environment config”

Add the following to frontend-pwa/.env.local:

Terminal window
VITE_TRAINER_DEBUG_LOG=1

Then restart the frontend dev server.

When a dice-roll state becomes visible in trainer mode, the frontend logs the piece-placement part of the current FEN only:

[trainer-debug] trainer.dice.visible {
moveIndex: 12,
fenPart: "rnbqkbnr/pppp1ppp/8/4p3/8/5N2/PPPPPPPP/RNBQKB1R"
}

This is useful for confirming which exact board position the user is being asked to evaluate before pressing Next turn.

When the user drags a piece on the board during a trainer pause, the frontend logs the board state after the manual move preview:

[trainer-debug] trainer.user-move.preview {
from: "e2",
to: "e4",
predictedFenPart: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR"
}

Predicted vs actual result after pressing Next

Section titled “Predicted vs actual result after pressing Next”

When the user presses Next turn, the frontend logs:

  • predictedFenPart — the board position created by the user’s manual move preview
  • actualFenPart — the board position reached after replaying the real move(s) from the game record
  • isMatch — whether the two piece-placement FEN values are identical
[trainer-debug] trainer.next.compare-fen {
predictedFenPart: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR",
actualFenPart: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR",
isMatch: true
}

If the user correctly predicts the move sequence, predictedFenPart and actualFenPart are identical.

  • Only the first field of the FEN is logged. Side to move, castling rights, en passant, and move counters are intentionally omitted.
  • The debug log is intended for validating trainer interaction logic, not for persistent analytics.
  • If you do not see any log output, first confirm that the frontend is running in dev mode and that debug is enabled.

The existing 🎯 Training mode badge in the trainer HUD also acts as a lightweight result indicator after the user presses Next turn.

  • Blue — neutral trainer state, no validated guess yet
  • Green — the user predicted the move sequence correctly
  • Red — the user prediction does not match the recorded move sequence

The comparison uses only the piece-placement part of the FEN.

The badge is updated immediately when the user presses Next turn, before any move animation begins:

The frontend peeks at the destination turn state to calculate the comparison:

  • the board position created by the user’s manual preview move(s) (already available)
  • the actual board position reached after the real move(s) from the game record (peeked, not played yet)

Then:

  1. The badge is resolved immediately (green/red/blue)
  2. Playback follows one of the branches below

Playback branches after pressing Next turn

Section titled “Playback branches after pressing Next turn”
  • User moved pieces and guessed correctly (isMatch = true)
    • Badge turns green (Correct guess)
    • The trainer skips move animation and jumps directly to the next boundary index
    • This avoids replaying moves that are already reflected on the board
  • User moved pieces and guessed incorrectly (isMatch = false)
    • Badge turns red (Incorrect guess)
    • Before replay starts, the board is reset to the current store state (the pre-turn baseline)
    • Then the trainer animates the real move sequence
  • User did not move pieces (board still equals pre-turn baseline)
    • Badge stays blue (Training mode)
    • The trainer animates playback exactly as in normal view mode

This allows the user to see validation feedback at the same instant they press the button, rather than waiting for the opponent animation to complete.

If the opponent has no legal dice moves and trainer playback jumps directly to the next subject pre-move boundary, the previous green/red feedback remains visible for that validated step and is not cleared immediately by the boundary jump.

Validation only occurs when all of the following are true:

  • The current trainer position is a subject pre-move pause
  • The subject has at least one legal move available from the rolled dice
  • The user changed the board position with at least one manual preview move
  • The user advances the trainer with Next turn

If the two FEN board parts are identical, the badge turns green. Otherwise it turns red.

Save Puzzle Button (Trainer Feedback Phase)

Section titled “Save Puzzle Button (Trainer Feedback Phase)”

In Trainer mode, the old bookmark action is repurposed as a Save Puzzle action.

  • The button keeps the same visual style/icon in src/views/TrainerGameView.svelte.
  • It is visible only during evaluation feedback (green/red badge states), not during neutral playback.
  • The button is hidden when feedback is idle, so regular browsing flow is not affected.
  • Tooltip text is contextual: Save evaluated puzzle, Saving puzzle..., or Puzzle already saved.
  • Once the current evaluated scenario is saved, the button is disabled to prevent redundant clicks.

When clicked, the frontend sends POST /api/training with puzzle data captured at the moment feedback is computed:

  • normalized_initial_fen — trainer position before applying the real move sequence
  • dice — rolled dice values for that decision point (space-separated)
  • solution_moves — historical move sequence from the source game (UCI, space-separated)
  • normalized_final_fen — resulting FEN after the actual recorded turn
  • game_id and ply — source game reference and move index

src/lib/trainingPuzzlesStore.svelte.ts owns Trainer puzzle persistence logic:

  • wraps GET/POST/DELETE /api/training via ApiClient
  • tracks loading states for list/saving/deleting
  • blocks duplicate submissions while a matching save request is already in flight
  • caches already-saved scenario keys in-session to avoid immediate re-submission spam
  • emits user feedback through toastStore for success, duplicate, and failure outcomes

If the rolled dice do not allow the player to move any piece, the trainer skips guess validation entirely.

In that scenario:

  • the board position is expected to remain unchanged
  • there is no meaningful user guess to validate
  • the badge stays in its neutral trainer state instead of showing red or green

Trainer mode now submits guess analytics in the background to POST /api/trainer/logs.

  • start/end timing for the current presented position (time_spent_ms)
  • board state before decision and after expected outcome (fen_before, fen_after_actual)
  • optional board state after the user preview (fen_after_guess)
  • guessed move sequence and actual move sequence
  • move counters for analytics transport (actual_moves_count, guessed_moves_count)
  • perfect-position flag (is_perfect)

is_perfect is determined by the following priority chain:

If both the user’s predicted board and the actual game board lack the opponent’s king, the guess is accepted as correct regardless of any other positional differences:

isOpponentKingAbsent(guessedFenPart) && isOpponentKingAbsent(actualFenPart)
→ is_perfect = true

This handles the scenario where the king can be reached via multiple routes. For example, when two queens are rolled, one route might capture a blocking piece first before taking the king, while another route goes directly to the king — both routes are equally valid.

API data note: the trailing “clear highlights” state that follows a king-capture move (a state with gameMoveHistoryMove: null and the same FEN) is the boundary state used for validation. Its FEN already reflects the captured king, so the absence of the king symbol in the FEN is a reliable signal.

Opponent king symbol by training subject color:

  • Subject is white (trainingSubjectColor = 'w') → opponent king symbol is 'k'
  • Subject is black (trainingSubjectColor = 'b') → opponent king symbol is 'K'

2. FEN board-part equality (standard rule)

Section titled “2. FEN board-part equality (standard rule)”

If the king-capture bypass does not apply, perfection is decided by strict piece-placement equality:

getFenBoardPart(fen_after_guess) === getFenBoardPart(fen_after_actual)
→ is_perfect = true

Move strings are not used to decide perfection.

Example of standard equality:

  • user: e1d1,d1e1
  • actual: e1f1,f1e1
  • final board placement is identical → is_perfect = true

guessed_moves_count no longer represents partial move-matching quality. It is kept only as a transport field for backend compatibility.

If the original player has no legal moves for the shown dice (actual_moves_count == 0), the frontend does not send a trainer log entry.

Log submission uses a fire-and-forget async request. UI feedback (badge state and animation behavior) is never delayed while the network request is in flight.

  • playback proceeds with the standard animation path

Duplicate-evaluation guard when navigating backward

Section titled “Duplicate-evaluation guard when navigating backward”

Trainer mode does not record duplicate results for already evaluated positions.

When the user moves backward and then tries to evaluate the same decision point again, the frontend compares the current move index with the latest recorded index for this session.

  • If moveIndex <= lastEvaluatedMoveIndex, the attempt is treated as a replay.
  • Replayed attempts are not sent to POST /api/trainer/logs.
  • Session counters are not updated for replayed attempts.

This keeps analytics clean and prevents inflated success/failure counts caused by repeated retries on the same step.

Trainer mode tracks per-session statistics in ActiveGameStore.trainingStats.

  • correctGuesses — number of validated guesses marked as correct.
  • incorrectGuesses — number of validated guesses marked as incorrect.
  • lastEvaluatedMoveIndex — the latest move index accepted for analytics in the current session.

totalMoves is derived in the UI from:

correctGuesses + incorrectGuesses

Training stats are reset when a new game is opened in trainer mode via loadGame(...).

At the last game state, trainer mode shows a completion modal with:

  • total evaluated moves,
  • correct guesses,
  • incorrect guesses,
  • success rate in percent.

The modal also provides two actions:

  • Restart Training — jumps to the first move and resets session counters.
  • Close Game — exits the active game session.
  • Keeps per-session feedback transparent for the user.
  • Avoids duplicate backend log entries after backtracking.
  • Preserves fair success metrics for future analytics.

Trainer mode playback is covered by comprehensive Vitest tests in src/lib/activeGameStore.test.ts:

  • uses subject pre-move and post-move stops for a white training subject
  • switches subject color based on board orientation and tracks black cycle stops
  • includes post-move pause when opponent has no legal moves (to show notice)
  • includes post-move pause when opponent CAN make legal moves
  • plays subject moves from pre-move to post-move pause
  • plays through the full opponent turn from post-move to next subject pre-move
  • stops at opponent blocked roll (to allow trainer notice)
  • validates guesses only on subject dice-roll states with legal moves
  • skips guess validation when the subject has no legal moves for the rolled dice
  • returns player name message when there are no legal moves (noLegalMovesMessage)
  • boardOrientation / trainingSubjectColor derived chain
  • player name formatting with rating (whitePlayerName, blackPlayerName)
  • time control formatting (timeControl)
  • playerIds and whitePlayerId / blackPlayerId fallback behaviour
  • helper coverage for trainer badge states (idle, correct, incorrect)

Run tests with:

Terminal window
npm run test -- activeGameStore.test.ts --run
  • Store class: src/lib/activeGameStore.svelte.ts
  • Turn boundary computation: computeTurnBoundaries() method
  • Post-move detection: isSubjectPostMoveBoundary() method
  • Move availability check: canPlayerMakeAnyMove() method
  • Playback: playTurnForward() and playTurnBackward() methods
  • Trainer debug gate: src/lib/trainerDebug.ts
  • Board preview capture: src/components/ChessgroundBoard.svelte
  • Trainer debug logging: src/views/TrainerGameView.svelte
  • Trainer statistics modal: src/views/TrainerGameView.svelte
ConstantValuePurpose
OPPONENT_MOVE_DELAY_MS500Delay between opponent’s micro-moves during auto-play

Practice mode reuses the frontend trainer infrastructure but validates user answers by final board state.

  1. PracticeView starts a session and calls GET /api/v1/training-puzzles/next via practiceStore.
  2. While practiceStore.sessionState === 'playing', the UI renders ActivePuzzle.
  3. ActivePuzzle mounts:
    • ChessgroundBoard initialized with currentPuzzle.normalized_initial_fen
    • DiceBox with puzzle dice values
    • a live timer (MM:SS)
  4. On Check Answer:
    • read current board FEN from chessground API
    • extract the piece placement part with getFenBoardPart()
    • compare with the piece placement part of currentPuzzle.normalized_final_fen
  5. Submit attempt via practiceStore.submitAttempt(isCorrect, timeSpentMs).
  6. User advances with Next Puzzle, which calls practiceStore.fetchNextPuzzle().

Practice validation uses strict equality on the piece-placement FEN field (the first field) only.

Active color, castling rights, en-passant targets, and move counters are ignored.

This matches Dice Chess puzzle semantics where multiple micro-move sequences may still lead to the same correct resulting position.

  • src/views/PracticeView.svelte
  • src/components/ActivePuzzle.svelte
  • src/lib/practiceStore.svelte.ts
  • src/utils/fenUtils.ts