fix(replay): store the deal via upstream card_game serializers (schema v4) #170
Reference in New Issue
Block a user
Delete Branch "fix/replay-schema-v4-session-recording"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements Rhys's directive: replays now serialise through the upstream
card_gamesession serializers instead of re-dealing from the seed.Problem
Replays stored
seed + movesand playback rebuilt the board viaGameState::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:moveslists — every shared replay was already unplayable, andreplay.jsthrew uncaught, leaving a dead board with no message.game.jshand-assembled upload payloads with a hardcodedschema_version: 2andMath.round(game.seed()), which corrupts u64 seeds beyond JS number precision.GameMode::Difficulty(level)wins could never upload: the server parsedmodeas a plainString, 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.SessionRecordingnewtype delegating to the upstreamSessionserde;GameState::recording()/GameState::from_recording();from_instructions_uncheckedfixture helper.serde_jsonadded to dev-deps (tests only).Replayv4 —recordingreplacesmoves;seed/draw_mode/moderemain caption/index metadata only. v1–v3 files are rejected by the existing version gate.game.recording(); playback rebuilds from the recording;Playingcarries the extracted move list (+Box<Replay>forclippy::large_enum_variant).replay_export(time_seconds, date)builds the complete v4 upload payload so JS never assembles it;ReplayPlayer::from_jsonenforcesschema_version == 4with a descriptive error; new tests prove playback ignores the seed for dealing and rejects pre-v4 JSON.game.js/play.htmlcallreplay_export;replay.jssurfaces player-construction errors in the caption.modevalidation accepts both string and single-key-object variants.Notes
replays.jsonentries 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.Gate:
cargo test --workspacegreen (all 27 test binaries),cargo clippy --workspace --all-targets -- -D warningsclean, rustfmt applied.🤖 Generated with Claude Code
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>