refactor: persist replay/save moves as KlondikeInstruction, not pile coords (#89)
Pile-position types (Tableau, Foundation, KlondikePile, KlondikePileStack) are runtime-only and have no serde upstream. Per Rhys's guidance, the persistence layer now stores the moves (KlondikeInstruction) rather than board coordinates, decoding back to runtime pile positions on demand. Core / data: - game_state: instruction_history() -> Vec<KlondikeInstruction>; add instruction_to_piles() and apply_instruction(); drop AnyInstruction. - klondike_adapter: delete the entire Saved* serde mirror section (SavedTableau/Foundation/SkipCards/KlondikePile/TableauStack/ KlondikePileStack/DstFoundation/DstTableau/SavedInstruction). - replay: drop the bespoke ReplayMove serde mirror; Replay.moves is now Vec<KlondikeInstruction>; REPLAY_SCHEMA_VERSION 2 -> 3. - storage: game_state save format v3 rejected (v4/v5 only). Engine / wasm consumers: - record via KlondikeInstruction (stock click = RotateStock). - playback decodes each instruction to (from, to, count) against the live state via instruction_to_piles, then fires the canonical event; undecodable instructions are skipped with a warning, never panic. - remove all use solitaire_data::ReplayMove and Saved* imports. Workspace check, clippy -D warnings, and the full test suite all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,7 @@ use super::*;
|
||||
use crate::layout::LayoutResource;
|
||||
use crate::replay_playback::ReplayPlaybackState;
|
||||
use crate::resources::GameStateResource;
|
||||
use solitaire_core::KlondikePile;
|
||||
use solitaire_data::ReplayMove;
|
||||
use solitaire_core::{KlondikeInstruction, KlondikePile};
|
||||
|
||||
/// Overwrites the banner label whenever the resource changes — covers the
|
||||
/// `Playing → Completed` transition by swapping "▌ replay" for
|
||||
@@ -85,9 +84,15 @@ pub(crate) fn update_floating_progress_chip(
|
||||
// the most-recently-applied move sits at `cursor - 1`.
|
||||
let dest_pile = match state.as_ref() {
|
||||
ReplayPlaybackState::Playing { replay, cursor, .. } if *cursor > 0 => {
|
||||
// The destination pile is recoverable directly from the
|
||||
// instruction — no live state needed. `RotateStock` has no
|
||||
// destination (the chip hides over the stock pile).
|
||||
match &replay.moves[cursor - 1] {
|
||||
ReplayMove::Move { to, .. } => Some(*to),
|
||||
ReplayMove::StockClick => None,
|
||||
KlondikeInstruction::DstFoundation(dst) => {
|
||||
Some(KlondikePile::Foundation(dst.foundation))
|
||||
}
|
||||
KlondikeInstruction::DstTableau(dst) => Some(KlondikePile::Tableau(dst.tableau)),
|
||||
KlondikeInstruction::RotateStock => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -95,8 +100,7 @@ pub(crate) fn update_floating_progress_chip(
|
||||
|
||||
let Some(world_pos) = dest_pile
|
||||
.as_ref()
|
||||
.and_then(|p| KlondikePile::try_from(*p).ok())
|
||||
.and_then(|p| layout.0.pile_positions.get(&p).copied())
|
||||
.and_then(|p| layout.0.pile_positions.get(p).copied())
|
||||
else {
|
||||
// Nothing to point at — hide every chip and exit.
|
||||
for (_, mut visibility, _) in chips.iter_mut() {
|
||||
|
||||
Reference in New Issue
Block a user