fix(replay): store the deal via upstream card_game serializers (schema v4) #170

Merged
funman300 merged 1 commits from fix/replay-schema-v4-session-recording into master 2026-07-10 16:56:33 +00:00
Owner

Implements Rhys's directive: replays now serialise through the upstream card_game session serializers instead of re-dealing from the seed.

Problem

Replays stored seed + moves and playback rebuilt the board via GameState::new_with_mode(seed, ...). Any change to the seed→deal mapping (RNG bumps, upstream upgrades — e.g. the rand 0.10.1 merge in #162) silently invalidated every existing replay. On top of that:

  • Both replays on prod are May-era v1 rows with empty moves lists — every shared replay was already unplayable, and replay.js threw uncaught, leaving a dead board with no message.
  • The web game.js hand-assembled upload payloads with a hardcoded schema_version: 2 and Math.round(game.seed()), which corrupts u64 seeds beyond JS number precision.
  • GameMode::Difficulty(level) wins could never upload: the server parsed mode as a plain String, but data-carrying variants serialise as objects → 400.

Fix — replay schema v4

The upstream Session<Klondike> serde already stores {config, initial_state, instructions} — the dealt board, explicitly. That is now the replay payload.

  • core: SessionRecording newtype delegating to the upstream Session serde; GameState::recording() / GameState::from_recording(); from_instructions_unchecked fixture helper. serde_json added to dev-deps (tests only).
  • data: Replay v4 — recording replaces moves; seed/draw_mode/mode remain caption/index metadata only. v1–v3 files are rejected by the existing version gate.
  • engine: win-recording and sync upload freeze game.recording(); playback rebuilds from the recording; Playing carries the extracted move list (+ Box<Replay> for clippy::large_enum_variant).
  • wasm: new replay_export(time_seconds, date) builds the complete v4 upload payload so JS never assembles it; ReplayPlayer::from_json enforces schema_version == 4 with a descriptive error; new tests prove playback ignores the seed for dealing and rejects pre-v4 JSON.
  • web: game.js/play.html call replay_export; replay.js surfaces player-construction errors in the caption.
  • server: mode validation accepts both string and single-key-object variants.

Notes

  • Existing local v3 replays.json entries will show as an empty history (they were seed-fragile anyway). The two junk v1 rows on prod can be deleted or left — the viewer now explains why they will not play.
  • Web player ships with the next docker-build deploy; desktop/Android with the next release tag.

Gate: cargo test --workspace green (all 27 test binaries), cargo clippy --workspace --all-targets -- -D warnings clean, rustfmt applied.

🤖 Generated with Claude Code

Implements Rhys's directive: replays now serialise through the upstream `card_game` session serializers instead of re-dealing from the seed. ## Problem Replays stored `seed + moves` and playback rebuilt the board via `GameState::new_with_mode(seed, ...)`. Any change to the seed→deal mapping (RNG bumps, upstream upgrades — e.g. the rand 0.10.1 merge in #162) silently invalidated every existing replay. On top of that: - Both replays on prod are May-era v1 rows with **empty `moves` lists** — every shared replay was already unplayable, and `replay.js` threw uncaught, leaving a dead board with no message. - The web `game.js` hand-assembled upload payloads with a hardcoded `schema_version: 2` and `Math.round(game.seed())`, which corrupts u64 seeds beyond JS number precision. - `GameMode::Difficulty(level)` wins could never upload: the server parsed `mode` as a plain `String`, but data-carrying variants serialise as objects → 400. ## Fix — replay schema v4 The upstream `Session<Klondike>` serde already stores `{config, initial_state, instructions}` — the dealt board, explicitly. That is now the replay payload. - **core**: `SessionRecording` newtype delegating to the upstream `Session` serde; `GameState::recording()` / `GameState::from_recording()`; `from_instructions_unchecked` fixture helper. `serde_json` added to dev-deps (tests only). - **data**: `Replay` v4 — `recording` replaces `moves`; `seed`/`draw_mode`/`mode` remain caption/index metadata only. v1–v3 files are rejected by the existing version gate. - **engine**: win-recording and sync upload freeze `game.recording()`; playback rebuilds from the recording; `Playing` carries the extracted move list (+ `Box<Replay>` for `clippy::large_enum_variant`). - **wasm**: new `replay_export(time_seconds, date)` builds the complete v4 upload payload so JS never assembles it; `ReplayPlayer::from_json` enforces `schema_version == 4` with a descriptive error; new tests prove playback ignores the seed for dealing and rejects pre-v4 JSON. - **web**: `game.js`/`play.html` call `replay_export`; `replay.js` surfaces player-construction errors in the caption. - **server**: `mode` validation accepts both string and single-key-object variants. ## Notes - Existing local v3 `replays.json` entries will show as an empty history (they were seed-fragile anyway). The two junk v1 rows on prod can be deleted or left — the viewer now explains why they will not play. - Web player ships with the next docker-build deploy; desktop/Android with the next release tag. Gate: `cargo test --workspace` green (all 27 test binaries), `cargo clippy --workspace --all-targets -- -D warnings` clean, rustfmt applied. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
funman300 added 1 commit 2026-07-10 16:19:39 +00:00
Replays previously persisted only seed + moves and re-dealt the board
from the seed at playback time, so any change to the seed->deal mapping
(RNG bumps, upstream upgrades) silently invalidated every existing
replay. Schema v4 instead embeds a SessionRecording - the upstream
card_game Session serde ({config, initial_state, instructions}) - so
playback rebuilds the exact recorded board; seed/draw_mode/mode remain
caption metadata only.

- core: SessionRecording newtype delegating to Session<Klondike> serde;
  GameState::recording() / from_recording(); from_instructions_unchecked
  fixture helper; serde_json added to dev-deps (tests only)
- data: Replay v4 (recording replaces moves); v1-v3 files rejected by
  the existing version gate
- engine: win-recording and sync upload freeze game.recording();
  playback rebuilds from the recording; Playing carries the extracted
  move list (+ Box<Replay> for clippy large_enum_variant)
- wasm: replay_export() builds the full v4 upload payload so JS never
  hand-assembles it (the old game.js path hardcoded schema_version: 2
  and corrupted u64 seeds via Math.round); ReplayPlayer::from_json
  enforces schema_version == 4 with a descriptive error
- web: game.js/play.html use replay_export; replay.js surfaces player
  construction errors in the caption instead of dying silently
- server: mode validation accepts data-carrying GameMode variants
  (Difficulty uploads previously 400'd against the String field)

Both replays on prod are May-era v1 rows with empty move lists - every
shared replay was already unplayable; the viewer now says why.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
funman300 merged commit ce2b29f5df into master 2026-07-10 16:56:33 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: funman300/Ferrous-Solitaire#170