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(_))));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user