Trainer Mode Playback
Overview
Section titled “Overview”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.
Playback Flow
Section titled “Playback Flow”Blocked Turn (No Legal Moves)
Section titled “Blocked Turn (No Legal Moves)”When a player (training subject or opponent) receives a dice roll with NO allowed die values:
- Show dice (pause point) — the dice area shows all dice as blocked (red background) for 800ms.
- Display notice — the dice are replaced by a pulsed status message (e.g., “White has no legal moves”) for 1800ms.
- 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.
Implementation Details
Section titled “Implementation Details”Turn Boundary Detection
Section titled “Turn Boundary Detection”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
Post-Move Boundary (Subject’s Turn End)
Section titled “Post-Move Boundary (Subject’s Turn End)”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)
Blocked Turn Boundary
Section titled “Blocked Turn Boundary”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.
Allowed Dice Detection
Section titled “Allowed Dice Detection”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.
Debug Logging
Section titled “Debug Logging”Trainer mode includes an opt-in frontend debug log stream for validating user move prediction against the recorded game state.
Where to view the logs
Section titled “Where to view the logs”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]How to enable debug logging
Section titled “How to enable debug logging”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__ = trueTo disable it again:
window.__TRAINER_DEBUG__ = falseThis 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:
VITE_TRAINER_DEBUG_LOG=1Then restart the frontend dev server.
What gets logged
Section titled “What gets logged”Dice visibility
Section titled “Dice visibility”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.
User preview moves
Section titled “User preview moves”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 previewactualFenPart— the board position reached after replaying the real move(s) from the game recordisMatch— 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.
Notes and limitations
Section titled “Notes and limitations”- 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.
Guess Result Indicator
Section titled “Guess Result Indicator”The existing 🎯 Training mode badge in the trainer HUD also acts as a lightweight result indicator after the
user presses Next turn.
Badge states
Section titled “Badge states”- 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.
When the indicator is evaluated
Section titled “When the indicator is evaluated”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:
- The badge is resolved immediately (green/red/blue)
- 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
- Badge turns green (
- 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
- Badge turns red (
- 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
- Badge stays blue (
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..., orPuzzle already saved. - Once the current evaluated scenario is saved, the button is disabled to prevent redundant clicks.
Saved payload
Section titled “Saved payload”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 sequencedice— 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 turngame_idandply— source game reference and move index
Store responsibilities
Section titled “Store responsibilities”src/lib/trainingPuzzlesStore.svelte.ts owns Trainer puzzle persistence logic:
- wraps
GET/POST/DELETE /api/trainingviaApiClient - 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
toastStorefor success, duplicate, and failure outcomes
When validation is skipped
Section titled “When validation is skipped”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
Background Training Logs
Section titled “Background Training Logs”Trainer mode now submits guess analytics in the background to POST /api/trainer/logs.
What is tracked per decision
Section titled “What is tracked per decision”- 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)
Scoring rule
Section titled “Scoring rule”is_perfect is determined by the following priority chain:
1. King-capture bypass (takes priority)
Section titled “1. King-capture bypass (takes priority)”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 = trueThis 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 = trueMove 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.
Guard: no legal moves
Section titled “Guard: no legal moves”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.
Non-blocking delivery
Section titled “Non-blocking delivery”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.
Session Statistics (End of Game)
Section titled “Session Statistics (End of Game)”Trainer mode tracks per-session statistics in ActiveGameStore.trainingStats.
Tracked fields
Section titled “Tracked fields”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 + incorrectGuessesInitialization
Section titled “Initialization”Training stats are reset when a new game is opened in trainer mode via loadGame(...).
End-of-game modal
Section titled “End-of-game modal”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.
Why this matters
Section titled “Why this matters”- Keeps per-session feedback transparent for the user.
- Avoids duplicate backend log entries after backtracking.
- Preserves fair success metrics for future analytics.
Testing
Section titled “Testing”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 subjectswitches subject color based on board orientation and tracks black cycle stopsincludes post-move pause when opponent has no legal moves (to show notice)includes post-move pause when opponent CAN make legal movesplays subject moves from pre-move to post-move pauseplays through the full opponent turn from post-move to next subject pre-movestops at opponent blocked roll (to allow trainer notice)validates guesses only on subject dice-roll states with legal movesskips guess validation when the subject has no legal moves for the rolled dicereturns 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) playerIdsandwhitePlayerId / blackPlayerIdfallback behaviour- helper coverage for trainer badge states (
idle,correct,incorrect)
Run tests with:
npm run test -- activeGameStore.test.ts --runCode Reference
Section titled “Code Reference”- Store class:
src/lib/activeGameStore.svelte.ts - Turn boundary computation:
computeTurnBoundaries()method - Post-move detection:
isSubjectPostMoveBoundary()method - Move availability check:
canPlayerMakeAnyMove()method - Playback:
playTurnForward()andplayTurnBackward()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
Key Constants
Section titled “Key Constants”| Constant | Value | Purpose |
|---|---|---|
OPPONENT_MOVE_DELAY_MS | 500 | Delay between opponent’s micro-moves during auto-play |
Practice Mode (Puzzle Solving)
Section titled “Practice Mode (Puzzle Solving)”Practice mode reuses the frontend trainer infrastructure but validates user answers by final board state.
Frontend flow
Section titled “Frontend flow”PracticeViewstarts a session and callsGET /api/v1/training-puzzles/nextviapracticeStore.- While
practiceStore.sessionState === 'playing', the UI rendersActivePuzzle. ActivePuzzlemounts:ChessgroundBoardinitialized withcurrentPuzzle.normalized_initial_fenDiceBoxwith puzzle dice values- a live timer (
MM:SS)
- 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
- Submit attempt via
practiceStore.submitAttempt(isCorrect, timeSpentMs). - User advances with Next Puzzle, which calls
practiceStore.fetchNextPuzzle().
Validation rule
Section titled “Validation rule”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.
Core files
Section titled “Core files”src/views/PracticeView.sveltesrc/components/ActivePuzzle.sveltesrc/lib/practiceStore.svelte.tssrc/utils/fenUtils.ts