test(core,data): verify schema-v3 round-trip; pin upstream git deps
- solitaire_data: add game_state_v3_mid_game_round_trip — first test to exercise the schema-v3 instruction-replay path with a real mid-game state (draws + card move + undo); GameState::PartialEq validates all pile layouts, score, move_count, undo_count, and recycle_count - solitaire_data: add save_format_v2_is_rejected — schema-version gate test, parallel to the existing v1 rejection fixture - solitaire_core: add SavedInstruction proptest (256 random cases across all three instruction variants) and four boundary unit tests for out-of-range Tableau/Foundation/SkipCards values - solitaire_core: document pile() KlondikePile::Stock → waste mapping - solitaire_core: document replay_config() take_from_foundation=true invariant and the re-export policy for upstream types - Cargo.toml: pin card_game + klondike git deps to rev 99b49e62 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -295,6 +295,11 @@ impl GameState {
|
||||
}
|
||||
|
||||
fn replay_config(draw_mode: DrawMode) -> KlondikeConfig {
|
||||
// Always allow foundation returns during replay, regardless of the
|
||||
// player's current `take_from_foundation` setting. A move recorded
|
||||
// when the rule was enabled must replay correctly even if the player
|
||||
// later disables it; a restrictive replay config would reject it and
|
||||
// corrupt the save.
|
||||
KlondikeAdapter::config_for(draw_mode, true)
|
||||
}
|
||||
|
||||
@@ -362,6 +367,11 @@ impl GameState {
|
||||
Self::cards_with_face(state.stock().face_up().iter().map(card_from_kl), true)
|
||||
}
|
||||
|
||||
/// Returns the cards in the requested pile.
|
||||
///
|
||||
/// **Note on `KlondikePile::Stock`:** this variant returns the face-up
|
||||
/// *waste* pile, not the face-down draw stack. Use [`Self::stock_cards`]
|
||||
/// to read the face-down draw cards.
|
||||
pub fn pile(&self, pile: KlondikePile) -> Vec<Card> {
|
||||
#[cfg(feature = "test-support")]
|
||||
if let Some(ref state) = self.test_pile_state {
|
||||
|
||||
@@ -6,8 +6,13 @@ pub mod klondike_adapter;
|
||||
pub mod pile;
|
||||
pub mod solver;
|
||||
|
||||
// Re-export upstream types that cross the solitaire_core API boundary so
|
||||
// callers can import from one place without a direct `klondike` / `card_game` dep.
|
||||
// Re-export the upstream types that cross the solitaire_core API boundary so
|
||||
// downstream crates (engine, wasm) can import from one place without a direct
|
||||
// `klondike` / `card_game` dep.
|
||||
//
|
||||
// `KlondikePileStack`, `SkipCards`, and `TableauStack` are intentionally NOT
|
||||
// re-exported — they are only used internally in `klondike_adapter.rs` and do
|
||||
// not appear in any public method signature.
|
||||
pub use card_game::Session;
|
||||
pub use klondike::{Foundation, Klondike, KlondikePile, Tableau};
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use klondike::{Foundation, KlondikePile, Tableau};
|
||||
use klondike::{Foundation, KlondikePile, KlondikeInstruction, SkipCards, Tableau};
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::game_state::{DrawMode, GameState};
|
||||
use crate::klondike_adapter::{
|
||||
InvalidSavedInstruction, SavedDstFoundation, SavedDstTableau, SavedFoundation,
|
||||
SavedInstruction, SavedKlondikePile, SavedKlondikePileStack, SavedSkipCards, SavedTableau,
|
||||
SavedTableauStack,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
@@ -221,4 +226,117 @@ proptest! {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SavedInstruction ↔ KlondikeInstruction round-trip
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// Every valid `SavedInstruction` survives a round-trip through
|
||||
/// `KlondikeInstruction::try_from(SavedInstruction::from(original))`.
|
||||
///
|
||||
/// Covers all three variants (`RotateStock`, `DstFoundation`, `DstTableau`)
|
||||
/// and all legal sub-field ranges:
|
||||
/// - `SavedTableau`: 0–6
|
||||
/// - `SavedFoundation`: 0–3
|
||||
/// - `SavedSkipCards`: 0–12
|
||||
#[test]
|
||||
fn saved_instruction_round_trip(
|
||||
instruction in saved_instruction_strategy(),
|
||||
) {
|
||||
let klondike = KlondikeInstruction::try_from(instruction);
|
||||
prop_assert!(
|
||||
klondike.is_ok(),
|
||||
"TryFrom failed for valid SavedInstruction {instruction:?}: {:?}",
|
||||
klondike.err(),
|
||||
);
|
||||
let saved_again = SavedInstruction::from(klondike.expect("checked above"));
|
||||
prop_assert_eq!(
|
||||
saved_again,
|
||||
instruction,
|
||||
"round-trip produced a different SavedInstruction",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Proptest strategies for SavedInstruction and its sub-types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn saved_tableau_strategy() -> impl Strategy<Value = SavedTableau> {
|
||||
(0u8..=6).prop_map(SavedTableau)
|
||||
}
|
||||
|
||||
fn saved_foundation_strategy() -> impl Strategy<Value = SavedFoundation> {
|
||||
(0u8..=3).prop_map(SavedFoundation)
|
||||
}
|
||||
|
||||
fn saved_skip_cards_strategy() -> impl Strategy<Value = SavedSkipCards> {
|
||||
(0u8..=12).prop_map(SavedSkipCards)
|
||||
}
|
||||
|
||||
fn saved_klondike_pile_strategy() -> impl Strategy<Value = SavedKlondikePile> {
|
||||
prop_oneof![
|
||||
saved_tableau_strategy().prop_map(SavedKlondikePile::Tableau),
|
||||
Just(SavedKlondikePile::Stock),
|
||||
saved_foundation_strategy().prop_map(SavedKlondikePile::Foundation),
|
||||
]
|
||||
}
|
||||
|
||||
fn saved_klondike_pile_stack_strategy() -> impl Strategy<Value = SavedKlondikePileStack> {
|
||||
prop_oneof![
|
||||
(saved_tableau_strategy(), saved_skip_cards_strategy()).prop_map(|(tableau, skip_cards)| {
|
||||
SavedKlondikePileStack::Tableau(SavedTableauStack { tableau, skip_cards })
|
||||
}),
|
||||
Just(SavedKlondikePileStack::Stock),
|
||||
saved_foundation_strategy().prop_map(SavedKlondikePileStack::Foundation),
|
||||
]
|
||||
}
|
||||
|
||||
fn saved_instruction_strategy() -> impl Strategy<Value = SavedInstruction> {
|
||||
prop_oneof![
|
||||
Just(SavedInstruction::RotateStock),
|
||||
(saved_klondike_pile_strategy(), saved_foundation_strategy()).prop_map(
|
||||
|(src, foundation)| {
|
||||
SavedInstruction::DstFoundation(SavedDstFoundation { src, foundation })
|
||||
}
|
||||
),
|
||||
(saved_klondike_pile_stack_strategy(), saved_tableau_strategy()).prop_map(
|
||||
|(src, tableau)| {
|
||||
SavedInstruction::DstTableau(SavedDstTableau { src, tableau })
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Boundary error unit tests (exact out-of-range values)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod saved_instruction_boundary_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn saved_tableau_7_is_invalid() {
|
||||
let result = Tableau::try_from(SavedTableau(7));
|
||||
assert_eq!(result, Err(InvalidSavedInstruction::Tableau(7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_tableau_255_is_invalid() {
|
||||
let result = Tableau::try_from(SavedTableau(255));
|
||||
assert_eq!(result, Err(InvalidSavedInstruction::Tableau(255)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_foundation_4_is_invalid() {
|
||||
let result = Foundation::try_from(SavedFoundation(4));
|
||||
assert_eq!(result, Err(InvalidSavedInstruction::Foundation(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_skip_cards_13_is_invalid() {
|
||||
let result = SkipCards::try_from(SavedSkipCards(13));
|
||||
assert_eq!(result, Err(InvalidSavedInstruction::SkipCards(13)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user