Trainer Algorithm
Purpose
Section titled “Purpose”This page defines the playback algorithm used by trainer mode in frontend-pwa/src/lib/activeGameStore.svelte.ts.
Trainer mode is designed to focus on one training subject: the stronger player shown at the bottom of the board. Navigation pauses only at subject decision points, while the opponent turn is played as one continuous event.
All derived state in ActiveGameStore — including turnBoundaries, currentState, boardOrientation, trainingSubjectColor, historyBlocks, doublingStatusMessage, and player identifiers — is implemented as Svelte 5 $derived or $derived.by() fields. This ensures values are cached and only recomputed when their reactive dependencies actually change.
- State index: A key in
gameMoveHistoryStateMap(e.g.0,1,2, …). - Training subject: Bottom player color, derived from
boardOrientation(white -> w,black -> b). - Turn boundary: A state index where trainer navigation (
Next/Prev) stops. - Pre-move boundary: Subject roll state (
turn === subjectColor, non-emptydices,gameMoveHistoryMove === null, and dice changed from previous state). - Post-move boundary: State immediately before opponent dice roll (detected by checking that the next state is an opponent dice-roll state).
- Blocked turn boundary: A state where a player (subject or opponent) rolls dice but has zero legal moves available (all dice are blocked).
- Micro-move: A single move in
gameMoveHistoryMoveinside one turn.
Algorithm Contracts
Section titled “Algorithm Contracts”- A turn in Dice Chess is
1 roll + up to 3 micro-moves. - During micro-moves of one turn, side-to-move in FEN does not need to alternate per micro-move.
- Trainer boundaries are subject-focused and alternate between Pre-move and Post-move points.
- Opponent dice roll and opponent micro-moves are auto-played inside one forward step from Post-move to next Pre-move.
- If no cycle boundary is detected,
maxMoveIndexis used as a fallback boundary.
Boundary Detection
Section titled “Boundary Detection”ActiveGameStore.computeTurnBoundaries() scans states and marks index i as a boundary when any condition is true:
- Subject Pre-move (
iitself is a subject dice-roll state). - Subject Post-move (
i + 1exists and is an opponent dice-roll state). - Blocked Turn (
iis a roll state with no moves, oriis an opponent roll state with no moves immediately following a subject post-move boundary).
This ensures the trainer pauses to show dice even when no moves are possible, allowing for feedback messages.
flowchart TD
A[Get trainingSubjectColor from boardOrientation] --> B[For each index i from 0..max]
B --> C{Subject pre-move boundary at i?}
C -- yes --> D[Add i]
C -- no --> E{Subject post-move boundary at i?}
E -- yes --> D
E -- no --> G{Opponent blocked roll at i+1?}
G -- yes --> D
G -- no --> B
D --> B
B --> H{Any boundaries found?}
H -- no --> I[Add maxMoveIndex fallback]
H -- yes --> J[Return sorted boundaries]
I --> J
Playback Behavior
Section titled “Playback Behavior”Next turn
Section titled “Next turn”playTurnForward() finds the smallest boundary greater than currentMoveIndex and animates index-by-index.
- From Pre-move, it plays subject micro-moves and stops at Post-move.
- From Post-move, it plays opponent roll + all opponent micro-moves and stops at next subject Pre-move.
Previous turn
Section titled “Previous turn”playTurnBackward() finds the greatest boundary lower than currentMoveIndex and animates backward index-by-index with the same delay.
stateDiagram-v2
[*] --> PreRoll
PreRoll --> BlockedNotice: No legal moves
BlockedNotice --> NextOpponent: Auto-advance
PreRoll --> SubjectMove1: User presses Next
SubjectMove1 --> SubjectMove2: Optional
SubjectMove2 --> SubjectMove3: Optional
SubjectMove1 --> PostMove: No more legal moves
SubjectMove2 --> PostMove: No more legal moves
SubjectMove3 --> PostMove
PostMove --> OpponentRoll: User presses Next
OpponentRoll --> OpponentBlocked: No legal moves
OpponentBlocked --> OpponentBlockedNotice: Pause for feedback
OpponentBlockedNotice --> NextPreRoll: Auto-advance
OpponentRoll --> OpponentMoves: Auto playback
OpponentMoves --> NextPreRoll: Subject roll
NextPreRoll --> [*]: Pause for training
No Legal Moves Sequence
Section titled “No Legal Moves Sequence”When the trainer lands on a Blocked turn boundary, it triggers a specific automated sequence in TrainerStore.handleNextTurn():
- Dice Pause (800ms): Show the blocked dice array to the user.
- Status Notice (1800ms): Replace dice with a pulse-animated message (e.g., “White has no legal moves”).
- Step Pause (400ms): Brief pause before advancing.
- Auto-Advance: Automatically move to the next boundary.
Input and UI Sequence
Section titled “Input and UI Sequence”sequenceDiagram
participant U as User
participant V as TrainerGameView
participant S as ActiveGameStore
participant D as DiceBox
participant B as ChessgroundBoard
U->>V: Press Next (or ArrowRight)
V->>S: playTurnForward()
S->>S: find next subject-cycle boundary
loop per index until boundary
S->>B: update currentMoveIndex
S->>D: update dice state
S->>S: wait playback delay
end
S-->>V: stop at pre-move or post-move boundary
V-->>U: Pause at subject decision point
Edge Cases
Section titled “Edge Cases”-
No dice in state: state is not a pre-move boundary.
-
Repeated state snapshots: unchanged dice arrays are skipped as roll boundaries.
-
Game end: if no cycle boundary exists,
maxMoveIndexfallback keeps the game reachable. -
Trainer UI masking: when paused at subject Post-move, dice can be hidden to avoid showing opponent roll context too early.
-
Interrupt events (e.g., doubling accepted): represented as intermediate states; cycle boundaries stay tied to roll transitions.
-
Double declined: In X2 games, when a player offers a double (before rolling dice) and the opponent declines, the API produces trailing states with no moves and identical (stale) dice arrays. These states are detected via
doubleDeclineStartIndex— which identifies the first state where the dice array is unchanged from the previous state. Two boundary stops are added: the first trailing state (showing “offers double”) and the last state (showing “declined double”). During animated playback, the user sees both messages in sequence with a delay — mirroring the two-step display used for accepted doubles. The DiceBox automatically shows the doubling message instead of dice whendoublingStatusMessageis non-empty. -
King capture — multiple valid routes: When the training subject captures the opponent’s king, a turn may have multiple legal routes to reach the king (e.g. two queens rolled, where one route captures a blocking piece first). The API stores the route actually played, but any route that results in the king being taken is equally valid.
The API data pattern for a king-capture turn is:
State N — gameMoveHistoryMove: { from, to } ← actual capture move; FEN already has no kingState N+1 — gameMoveHistoryMove: null, FEN = same ← trailing "clear highlights" stateThe
nextBoundarythe trainer uses for validation is always the trailing State N+1. Its FEN reliably reflects the captured king via the absence of the king symbol (Kfor white,kfor black).Validation rule: if both the user’s predicted board and the actual game board lack the opponent’s king symbol, the guess is accepted as correct — no FEN equality comparison is performed.
Validation
Section titled “Validation”Core expectations are covered by frontend-pwa/src/lib/activeGameStore.test.ts:
- subject color follows board orientation,
- boundaries use subject Pre-move + Post-move stops,
- forward playback splits into subject phase and opponent autoplay phase,
- backward playback lands on previous subject-cycle boundary.