refactor: replace local DrawMode with upstream klondike::DrawStockConfig (#82)
DrawMode was a 1:1 mirror of klondike::DrawStockConfig (DrawOne/DrawThree). Delete it and use the upstream type everywhere; re-export DrawStockConfig from solitaire_core. config_for assigns draw_stock directly and draw_mode() returns session.config().inner.draw_stock. Serde is unchanged — DrawStockConfig serialises to the same "DrawOne"/"DrawThree" named variants, so persisted game_state.json / replay JSON stay byte-compatible (no migration). Field/method/variable names containing draw_mode are unchanged. 35 files, mechanical type swap across all crates. Implemented via a multi-agent workflow (core → per-crate consumers → verify). cargo test --workspace and clippy --workspace --all-targets -- -D warnings green. Closes #82 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::error::MoveError;
|
||||
use crate::klondike_adapter::{
|
||||
DrawMode, KlondikeAdapter, SavedInstruction,
|
||||
KlondikeAdapter, SavedInstruction,
|
||||
foundation_from_slot as adapter_foundation_from_slot,
|
||||
skip_cards_from_count as adapter_skip_cards_from_count,
|
||||
tableau_from_index as adapter_tableau_from_index,
|
||||
@@ -101,7 +101,7 @@ pub enum GameMode {
|
||||
/// `KlondikeInstruction` serde, which produces named enum variants.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct PersistedGameState {
|
||||
pub draw_mode: DrawMode,
|
||||
pub draw_mode: DrawStockConfig,
|
||||
pub mode: GameMode,
|
||||
pub elapsed_seconds: u64,
|
||||
pub seed: u64,
|
||||
@@ -134,7 +134,7 @@ enum AnyInstruction {
|
||||
/// them.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct PersistedGameStateIn {
|
||||
pub draw_mode: DrawMode,
|
||||
pub draw_mode: DrawStockConfig,
|
||||
#[serde(default)]
|
||||
pub mode: GameMode,
|
||||
pub elapsed_seconds: u64,
|
||||
@@ -306,12 +306,12 @@ impl<'de> Deserialize<'de> for GameState {
|
||||
|
||||
impl GameState {
|
||||
/// Creates a new Classic-mode game dealt from the given seed and draw mode.
|
||||
pub fn new(seed: u64, draw_mode: DrawMode) -> Self {
|
||||
pub fn new(seed: u64, draw_mode: DrawStockConfig) -> Self {
|
||||
Self::new_with_mode(seed, draw_mode, GameMode::Classic)
|
||||
}
|
||||
|
||||
/// Creates a new game with an explicit `GameMode`.
|
||||
pub fn new_with_mode(seed: u64, draw_mode: DrawMode, mode: GameMode) -> Self {
|
||||
pub fn new_with_mode(seed: u64, draw_mode: DrawStockConfig, mode: GameMode) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
elapsed_seconds: 0,
|
||||
@@ -325,11 +325,8 @@ impl GameState {
|
||||
|
||||
/// Whether the player draws one or three cards from the stock per turn.
|
||||
/// Derived from the underlying session config (set once at deal time).
|
||||
pub fn draw_mode(&self) -> DrawMode {
|
||||
match self.session.config().inner.draw_stock {
|
||||
DrawStockConfig::DrawOne => DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawThree => DrawMode::DrawThree,
|
||||
}
|
||||
pub fn draw_mode(&self) -> DrawStockConfig {
|
||||
self.session.config().inner.draw_stock
|
||||
}
|
||||
|
||||
/// Current game score, derived from the upstream session stats.
|
||||
@@ -405,11 +402,11 @@ impl GameState {
|
||||
!self.check_win() && self.check_auto_complete()
|
||||
}
|
||||
|
||||
fn new_session(seed: u64, draw_mode: DrawMode) -> Session<Klondike> {
|
||||
fn new_session(seed: u64, draw_mode: DrawStockConfig) -> Session<Klondike> {
|
||||
Session::new(Klondike::with_seed(seed), Self::session_config(draw_mode))
|
||||
}
|
||||
|
||||
fn session_config(draw_mode: DrawMode) -> SessionConfig<KlondikeConfig> {
|
||||
fn session_config(draw_mode: DrawStockConfig) -> SessionConfig<KlondikeConfig> {
|
||||
SessionConfig {
|
||||
inner: Self::replay_config(draw_mode),
|
||||
// The −15 WXP undo penalty is now applied by the upstream score
|
||||
@@ -419,7 +416,7 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
|
||||
fn replay_config(draw_mode: DrawMode) -> KlondikeConfig {
|
||||
fn replay_config(draw_mode: DrawStockConfig) -> 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
|
||||
@@ -587,7 +584,7 @@ impl GameState {
|
||||
/// mode. `draw_mode()` is otherwise fixed at deal time, so tests that need
|
||||
/// a specific mode use this instead of mutating a field.
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn set_test_draw_mode(&mut self, draw_mode: DrawMode) {
|
||||
pub fn set_test_draw_mode(&mut self, draw_mode: DrawStockConfig) {
|
||||
self.session = Self::new_session(self.seed, draw_mode);
|
||||
}
|
||||
|
||||
@@ -1148,7 +1145,7 @@ impl GameState {
|
||||
/// "Winnable deals only" retry loop.
|
||||
pub fn solve_fresh_deal(
|
||||
seed: u64,
|
||||
draw_mode: DrawMode,
|
||||
draw_mode: DrawStockConfig,
|
||||
moves_budget: u64,
|
||||
states_budget: u64,
|
||||
) -> SolveOutcome {
|
||||
@@ -1177,7 +1174,7 @@ mod tests {
|
||||
const MAX_STEPS: usize = 160;
|
||||
|
||||
for seed in 1..=MAX_SEED {
|
||||
let mut game = GameState::new(seed, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(seed, DrawStockConfig::DrawOne);
|
||||
game.take_from_foundation = true;
|
||||
|
||||
for _ in 0..MAX_STEPS {
|
||||
@@ -1226,7 +1223,7 @@ mod tests {
|
||||
/// iteration limit (shouldn't happen in practice).
|
||||
fn game_at_first_recycle() -> Option<GameState> {
|
||||
for seed in 1..=256_u64 {
|
||||
let mut game = GameState::new(seed, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(seed, DrawStockConfig::DrawOne);
|
||||
for _ in 0..200 {
|
||||
if game.stock_cards().is_empty() && !game.waste_cards().is_empty() {
|
||||
// This draw will recycle.
|
||||
@@ -1259,7 +1256,7 @@ mod tests {
|
||||
fn undo_applies_minus_15_penalty_via_upstream_score() {
|
||||
// A foundation move scores +10 upstream; undoing it nets the move score
|
||||
// back to 0 and adds the −15 undo penalty, which `score()` floors at 0.
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
// Find and play any scoring move, then undo it.
|
||||
let scoring_move = game
|
||||
.possible_instructions()
|
||||
@@ -1319,13 +1316,13 @@ mod tests {
|
||||
fn solve_fresh_deal_is_deterministic() {
|
||||
let a = GameState::solve_fresh_deal(
|
||||
7,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
DEFAULT_SOLVE_MOVES_BUDGET,
|
||||
DEFAULT_SOLVE_STATES_BUDGET,
|
||||
);
|
||||
let b = GameState::solve_fresh_deal(
|
||||
7,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
DEFAULT_SOLVE_MOVES_BUDGET,
|
||||
DEFAULT_SOLVE_STATES_BUDGET,
|
||||
);
|
||||
@@ -1335,7 +1332,7 @@ mod tests {
|
||||
#[test]
|
||||
fn winnable_verdict_carries_a_first_move() {
|
||||
// Contract: a first move is present iff the verdict is winnable.
|
||||
let outcome = GameState::solve_fresh_deal(7, DrawMode::DrawOne, 5_000, 5_000);
|
||||
let outcome = GameState::solve_fresh_deal(7, DrawStockConfig::DrawOne, 5_000, 5_000);
|
||||
let winnable = matches!(outcome, Ok(Some(_)));
|
||||
let has_move = outcome.ok().flatten().is_some();
|
||||
assert_eq!(winnable, has_move);
|
||||
@@ -1343,7 +1340,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn solve_first_move_uses_live_game_state() {
|
||||
let mut game = GameState::new(42, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
game.draw().expect("draw must succeed");
|
||||
|
||||
let outcome = game.solve_first_move(5_000, 5_000);
|
||||
@@ -1354,7 +1351,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn zero_state_budget_is_inconclusive() {
|
||||
let outcome = GameState::solve_fresh_deal(7, DrawMode::DrawOne, 5_000, 0);
|
||||
let outcome = GameState::solve_fresh_deal(7, DrawStockConfig::DrawOne, 5_000, 0);
|
||||
assert!(matches!(outcome, Err(SolveError::StatesBudgetExceeded)));
|
||||
}
|
||||
|
||||
@@ -1362,9 +1359,9 @@ mod tests {
|
||||
fn budget_is_passed_through_not_clamped() {
|
||||
// This seed is Inconclusive at 1k states but Winnable at 5k — proving the
|
||||
// budget reaches the solver unchanged.
|
||||
let easy = GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 1_000, 1_000);
|
||||
let easy = GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawStockConfig::DrawOne, 1_000, 1_000);
|
||||
let medium =
|
||||
GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawMode::DrawOne, 5_000, 5_000);
|
||||
GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawStockConfig::DrawOne, 5_000, 5_000);
|
||||
assert!(easy.is_err());
|
||||
assert!(matches!(medium, Ok(Some(_))));
|
||||
}
|
||||
|
||||
@@ -16,15 +16,6 @@ use klondike::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Whether cards are drawn one at a time or three at a time from the stock.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DrawMode {
|
||||
/// Draw one card from stock per turn.
|
||||
DrawOne,
|
||||
/// Draw three cards from stock per turn; only the top is playable.
|
||||
DrawThree,
|
||||
}
|
||||
|
||||
/// Bridges `solitaire_core` game config and scoring to the upstream `klondike` crate.
|
||||
///
|
||||
/// This type is intentionally zero-sized: it does not carry mutable runtime
|
||||
@@ -35,12 +26,9 @@ pub struct KlondikeAdapter;
|
||||
|
||||
impl KlondikeAdapter {
|
||||
/// Build a [`KlondikeConfig`] from draw mode and foundation house-rule setting.
|
||||
pub fn config_for(draw_mode: DrawMode, take_from_foundation: bool) -> KlondikeConfig {
|
||||
pub fn config_for(draw_mode: DrawStockConfig, take_from_foundation: bool) -> KlondikeConfig {
|
||||
KlondikeConfig {
|
||||
draw_stock: match draw_mode {
|
||||
DrawMode::DrawOne => DrawStockConfig::DrawOne,
|
||||
DrawMode::DrawThree => DrawStockConfig::DrawThree,
|
||||
},
|
||||
draw_stock: draw_mode,
|
||||
move_from_foundation: if take_from_foundation {
|
||||
MoveFromFoundationConfig::Allowed
|
||||
} else {
|
||||
|
||||
@@ -13,8 +13,7 @@ pub mod klondike_adapter;
|
||||
// when decoding instructions to piles in `instruction_to_piles`) and do not
|
||||
// appear in any public method signature.
|
||||
pub use card_game::{Card, Session, SolveError};
|
||||
pub use klondike::{Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau};
|
||||
pub use klondike_adapter::DrawMode;
|
||||
pub use klondike::{DrawStockConfig, Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau};
|
||||
|
||||
// Solvability check API (delegates to `card_game::Session::solve`); replaces the
|
||||
// former `solitaire_data::solver` wrapper module.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use card_game::{Card, Game};
|
||||
use klondike::{Foundation, KlondikePile, KlondikeInstruction, SkipCards, Tableau};
|
||||
use klondike::{DrawStockConfig, Foundation, KlondikePile, KlondikeInstruction, SkipCards, Tableau};
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::game_state::GameState;
|
||||
use crate::klondike_adapter::DrawMode;
|
||||
use crate::klondike_adapter::{
|
||||
InvalidSavedInstruction, SavedDstFoundation, SavedDstTableau, SavedFoundation,
|
||||
SavedInstruction, SavedKlondikePile, SavedKlondikePileStack, SavedSkipCards, SavedTableau,
|
||||
@@ -52,8 +51,8 @@ fn all_cards(game: &GameState) -> Vec<Card> {
|
||||
cards
|
||||
}
|
||||
|
||||
fn draw_mode_strategy() -> impl Strategy<Value = DrawMode> {
|
||||
prop_oneof![Just(DrawMode::DrawOne), Just(DrawMode::DrawThree)]
|
||||
fn draw_mode_strategy() -> impl Strategy<Value = DrawStockConfig> {
|
||||
prop_oneof![Just(DrawStockConfig::DrawOne), Just(DrawStockConfig::DrawThree)]
|
||||
}
|
||||
|
||||
/// Apply a sequence of random actions to a game, silently ignoring errors.
|
||||
|
||||
Reference in New Issue
Block a user