docs(core,data): complete Phases 0–2 of in-place card_game rewrite
Phase 0 – doc fixes (docs/card-game-integration.md): - Correct stale "no serde" claim: upstream has serde at rev 99b49e62 - Correct take_from_foundation default description (Allowed, not Disallowed) - Document schema v3→v4 migration and AnyInstruction strategy Phase 1 – delegate check_win / check_auto_complete to upstream: - Proptests verify semantic agreement with is_win() / is_win_trivial() across 256 random states before delegation Phase 2 – schema v4 with v3 auto-migration: - SavedInstruction mirror types kept as legacy compat module (needed by solitaire_data::ReplayMove and solitaire_wasm replay layer) - klondike_adapter.rs: add comprehensive legacy-purpose doc comment - proptest_tests.rs: add check_auto_complete/check_win semantic proofs - storage.rs: rename round-trip test to v4, add v3-migrates-to-v4 test Also track the rewrite plan (docs/in-place-card-game-rewrite-plan.md). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -223,17 +223,24 @@ pub fn card_from_kl(kl_card: &KlCard) -> card::Card {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Serde newtypes for KlondikeInstruction (Step 7) ──────────────────────────
|
||||
// ── Legacy serde mirror types (kept for backward compatibility) ───────────────
|
||||
//
|
||||
// `klondike::KlondikeInstruction` (and its sub-types) do not derive
|
||||
// `Serialize` / `Deserialize`. These mirror types carry `#[serde]` so that
|
||||
// the session instruction history can be persisted and reconstructed without
|
||||
// upstream changes.
|
||||
// These types were introduced when upstream `klondike` had no serde feature.
|
||||
// At rev 99b49e62, upstream provides full serde support, and `GameState`
|
||||
// serialises `saved_moves` directly as `Vec<KlondikeInstruction>` (schema v4).
|
||||
//
|
||||
// Conversion: `From<KlondikeInstruction> for SavedInstruction` and the
|
||||
// fallible inverse `TryFrom<SavedInstruction> for KlondikeInstruction`.
|
||||
// Invalid numeric values (out-of-range u8 for tableau/foundation/skip) yield
|
||||
// `InvalidSavedInstruction`.
|
||||
// The mirror types are retained for three reasons:
|
||||
// 1. Schema v3 migration: `AnyInstruction` in `game_state.rs` uses
|
||||
// `TryFrom<SavedInstruction> for KlondikeInstruction` to parse old save
|
||||
// files with u8 indices and replay them.
|
||||
// 2. `solitaire_data::ReplayMove` uses `SavedKlondikePile` as its serde
|
||||
// type; changing it would break the on-disk replay format (schema v2).
|
||||
// 3. `solitaire_wasm` mirrors `ReplayMove` using the same types so that
|
||||
// replay JSON is cross-compatible between the desktop and browser builds.
|
||||
//
|
||||
// These types should not be used for new serialisation concerns. If the
|
||||
// ReplayMove format is ever bumped to a new schema, migrate those callers to
|
||||
// `KlondikePile` / `KlondikePileStack` and the types here can then be deleted.
|
||||
|
||||
/// A `Serialize` + `Deserialize` mirror of [`klondike::Tableau`] (0 = Tableau1 … 6 = Tableau7).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use card_game::Game;
|
||||
use klondike::{Foundation, KlondikePile, KlondikeInstruction, SkipCards, Tableau};
|
||||
use proptest::prelude::*;
|
||||
|
||||
@@ -107,6 +108,52 @@ fn apply_one_move(game: &mut GameState, move_idx: usize) -> bool {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
proptest! {
|
||||
/// `check_auto_complete()` and `is_win_trivial()` must agree on every
|
||||
/// reachable game state.
|
||||
///
|
||||
/// The upstream `Klondike::is_win_trivial()` checks that the stock pile
|
||||
/// (both face-down and face-up halves) is completely empty AND that all
|
||||
/// tableau columns have no face-down cards. Ferrous `check_auto_complete()`
|
||||
/// checks the same three conditions individually (stock empty, waste empty,
|
||||
/// all tableau cards face-up). This property guards against any semantic
|
||||
/// drift between the two implementations so that delegating to upstream is
|
||||
/// safe.
|
||||
///
|
||||
/// If this property ever fails, `check_auto_complete()` must NOT be fully
|
||||
/// replaced — the Ferrous conditions must be preserved and `is_win_trivial()`
|
||||
/// used only as a supplementary guard.
|
||||
#[test]
|
||||
fn check_auto_complete_agrees_with_is_win_trivial(
|
||||
seed in any::<u64>(),
|
||||
draw_mode in draw_mode_strategy(),
|
||||
actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..30),
|
||||
) {
|
||||
let mut game = GameState::new(seed, draw_mode);
|
||||
apply_random_actions(&mut game, &actions);
|
||||
prop_assert_eq!(
|
||||
game.check_auto_complete(),
|
||||
game.session().state().state().is_win_trivial(),
|
||||
"check_auto_complete() disagreed with is_win_trivial() after {:?} actions",
|
||||
actions.len(),
|
||||
);
|
||||
}
|
||||
|
||||
/// `check_win()` and `is_win()` must agree on every reachable game state.
|
||||
#[test]
|
||||
fn check_win_agrees_with_is_win(
|
||||
seed in any::<u64>(),
|
||||
draw_mode in draw_mode_strategy(),
|
||||
actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..30),
|
||||
) {
|
||||
let mut game = GameState::new(seed, draw_mode);
|
||||
apply_random_actions(&mut game, &actions);
|
||||
prop_assert_eq!(
|
||||
game.check_win(),
|
||||
game.session().state().state().is_win(),
|
||||
"check_win() disagreed with is_win()",
|
||||
);
|
||||
}
|
||||
|
||||
/// All 52 card IDs must be present exactly once across every pile after
|
||||
/// any reachable sequence of draw + move_cards actions.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user