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:
@@ -383,7 +383,7 @@ pub struct ReplayOverlayMoveLogPrevRow {
|
||||
/// Marker on a "next move" row below the active row. `offset`
|
||||
/// is the 1-based distance forward from the active row:
|
||||
/// `offset = 1` is the move that will apply next
|
||||
/// (`replay.moves[cursor]`, displayed as `cursor + 1`),
|
||||
/// (`moves[cursor]`, displayed as `cursor + 1`),
|
||||
/// `offset = 2` is the one after that, and so on. Up to
|
||||
/// [`MOVE_LOG_NEXT_ROWS`] rows render below the active row.
|
||||
///
|
||||
@@ -1258,11 +1258,11 @@ fn keybind_footer_hint_text() -> &'static str {
|
||||
/// `win_move_index >= total` (defensive — shouldn't happen) doesn't
|
||||
/// position the marker outside the track.
|
||||
fn win_move_marker_pct(state: &ReplayPlaybackState) -> Option<f32> {
|
||||
let ReplayPlaybackState::Playing { replay, .. } = state else {
|
||||
let ReplayPlaybackState::Playing { replay, moves, .. } = state else {
|
||||
return None;
|
||||
};
|
||||
let idx = replay.win_move_index?;
|
||||
let total = replay.moves.len();
|
||||
let total = moves.len();
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user