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:
funman300
2026-06-11 16:01:11 -07:00
parent d045781119
commit 5c992cbdca
35 changed files with 257 additions and 274 deletions
+22 -25
View File
@@ -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(_))));
}