fix(replay): store the deal via upstream card_game serializers (schema v4)
Test / test (pull_request) Successful in 36m34s
Test / test (pull_request) Successful in 36m34s
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>
This commit is contained in:
@@ -853,17 +853,15 @@ fn replay_recording_freezes_into_replay_on_game_won() {
|
||||
let mut app = test_app(7654);
|
||||
app.insert_resource(ReplayPath(Some(path.clone())));
|
||||
|
||||
// Push two recorded instructions manually so we can verify they
|
||||
// survive the freeze/save round-trip without having to drive a
|
||||
// real win. Both are `RotateStock` — the only instruction
|
||||
// constructible without the runtime-only `klondike` pile-stack
|
||||
// types (which the engine intentionally does not depend on); the
|
||||
// round-trip shape is identical for any instruction variant.
|
||||
{
|
||||
let mut recording = app.world_mut().resource_mut::<RecordingReplay>();
|
||||
recording.moves.push(KlondikeInstruction::RotateStock);
|
||||
recording.moves.push(KlondikeInstruction::RotateStock);
|
||||
}
|
||||
// Drive two real draws so the *session* history (the source the
|
||||
// freeze now serialises from, via `GameState::recording()`) holds
|
||||
// two instructions. `RotateStock` is the only instruction the
|
||||
// engine can drive without the runtime-only `klondike` pile-stack
|
||||
// types; the round-trip shape is identical for any variant.
|
||||
app.world_mut().write_message(DrawRequestEvent);
|
||||
app.update();
|
||||
app.world_mut().write_message(DrawRequestEvent);
|
||||
app.update();
|
||||
|
||||
// Fire the win event the engine emits when the last foundation
|
||||
// completes — `record_replay_on_win` listens for it.
|
||||
@@ -895,9 +893,15 @@ fn replay_recording_freezes_into_replay_on_game_won() {
|
||||
loaded.time_seconds, 250,
|
||||
"time_seconds must come from the win event"
|
||||
);
|
||||
assert_eq!(loaded.moves.len(), 2, "every recorded move must round-trip");
|
||||
assert!(matches!(loaded.moves[0], KlondikeInstruction::RotateStock));
|
||||
assert!(matches!(loaded.moves[1], KlondikeInstruction::RotateStock));
|
||||
let instructions = loaded.recording.instructions();
|
||||
assert_eq!(instructions.len(), 2, "every recorded move must round-trip");
|
||||
assert!(matches!(instructions[0], KlondikeInstruction::RotateStock));
|
||||
assert!(matches!(instructions[1], KlondikeInstruction::RotateStock));
|
||||
assert_eq!(
|
||||
loaded.win_move_index,
|
||||
Some(1),
|
||||
"win move index must point at the last recorded instruction",
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
Reference in New Issue
Block a user