refactor(core): move solver to solitaire_data, DrawMode to klondike_adapter, remove pile/solver/schema_version

- Delete solitaire_core::solver — moved wholesale to solitaire_data::solver (re-exported at crate root)
- Delete solitaire_core::pile — no external users
- Move DrawMode from game_state to klondike_adapter; re-export as solitaire_core::DrawMode
- Remove schema_version field from GameState (redundant — deserializer stamps it from the constant)
- Update all callers across solitaire_data, solitaire_engine, solitaire_assetgen, solitaire_wasm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-09 09:38:04 -07:00
parent 37a21b9b42
commit 920f2c8597
40 changed files with 105 additions and 210 deletions
+3 -3
View File
@@ -819,7 +819,7 @@ mod tests {
app.world_mut()
.resource_mut::<GameStateResource>()
.0
.draw_mode = solitaire_core::game_state::DrawMode::DrawThree;
.draw_mode = solitaire_core::DrawMode::DrawThree;
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -868,7 +868,7 @@ mod tests {
app.world_mut()
.resource_mut::<GameStateResource>()
.0
.draw_mode = solitaire_core::game_state::DrawMode::DrawThree;
.draw_mode = solitaire_core::DrawMode::DrawThree;
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -1393,7 +1393,7 @@ mod tests {
use crate::replay_playback::ReplayPlaybackState;
use chrono::NaiveDate;
use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::{DrawMode, game_state::GameMode};
use solitaire_data::{Replay, ReplayMove};
/// Headless app variant that injects a default `ReplayPlaybackState`
+1 -1
View File
@@ -169,7 +169,7 @@ mod tests {
use crate::table_plugin::TablePlugin;
use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
fn headless_app() -> App {
let mut app = App::new();
+12 -12
View File
@@ -18,7 +18,7 @@ use bevy::sprite::Anchor;
use bevy::window::WindowResized;
use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
use crate::card_animation::CardAnimation;
@@ -2526,7 +2526,7 @@ mod tests {
#[test]
fn card_positions_includes_all_52_cards_at_game_start() {
// At game start waste is empty, so all 52 cards are across stock + tableau.
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
assert_eq!(positions.len(), 52);
@@ -2534,7 +2534,7 @@ mod tests {
#[test]
fn waste_draw_one_only_renders_top_card() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawOne);
// Draw 3 cards so the waste pile has 3 cards.
for _ in 0..3 {
@@ -2572,7 +2572,7 @@ mod tests {
#[test]
fn waste_draw_three_renders_up_to_three_fanned_cards() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawThree);
// 5 draw() calls in Draw-Three mode accumulates multiple waste cards.
for _ in 0..5 {
@@ -2625,7 +2625,7 @@ mod tests {
// Regression: slot.saturating_sub(1) always hid slot-0 even when the
// pile was too small to have a buffer card, collapsing 2 visible cards
// onto x=0 instead of fanning them.
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawThree);
// Draw exactly once — in Draw-Three mode with a full stock this gives
// 3 waste cards (still ≤ visible=3, so no hidden buffer needed).
@@ -2667,7 +2667,7 @@ mod tests {
/// top card so that hiding it (`Visibility::Hidden`) leaves no visible gap.
#[test]
fn waste_draw_one_buffer_card_at_same_xy_as_top() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawOne);
// Draw 3 times so the waste pile has 3 cards and the buffer exists.
for _ in 0..3 {
@@ -2698,7 +2698,7 @@ mod tests {
#[test]
fn card_positions_tableau_cards_are_fanned_downward() {
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
@@ -3103,7 +3103,7 @@ mod tests {
#[test]
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
let positions = card_positions(&g, &layout);
@@ -3552,7 +3552,7 @@ mod tests {
#[test]
fn stock_card_count_helper_reads_zero_for_empty_stock() {
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
let mut g_empty_stock = g.clone();
g_empty_stock.set_test_stock_cards(Vec::new());
assert_eq!(stock_card_count(&g_empty_stock), 0);
@@ -3837,7 +3837,7 @@ mod tests {
#[test]
fn waste_pile_cards_have_strictly_increasing_z() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawThree);
for _ in 0..5 {
let _ = g.draw();
@@ -3881,7 +3881,7 @@ mod tests {
/// offsets or flips the fan direction is caught immediately.
#[test]
fn waste_cards_do_not_overlap_stock_column_on_portrait() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawThree);
for _ in 0..5 {
let _ = g.draw();
@@ -3917,7 +3917,7 @@ mod tests {
#[test]
fn waste_pile_draw_one_cards_have_distinct_z() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut g = GameState::new(42, DrawMode::DrawOne);
for _ in 0..3 {
let _ = g.draw();
+1 -1
View File
@@ -117,7 +117,7 @@ mod tests {
use crate::game_plugin::GamePlugin;
use crate::progress_plugin::ProgressPlugin;
use crate::table_plugin::TablePlugin;
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
fn headless_app() -> App {
let mut app = App::new();
+3 -3
View File
@@ -35,7 +35,7 @@
use bevy::prelude::*;
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
use crate::card_plugin::RightClickHighlight;
use crate::layout::{Layout, LayoutResource};
@@ -562,7 +562,7 @@ mod tests {
#[test]
fn cursor_over_draggable_returns_false_for_empty_game() {
use crate::layout::compute_layout;
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
let game = GameState::new(42, DrawMode::DrawOne);
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
@@ -580,7 +580,7 @@ mod tests {
use crate::layout::compute_layout;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}};
/// Builds an `App` with `MinimalPlugins` and the overlay system
/// registered, plus the resources the system needs. Callers
@@ -362,7 +362,7 @@ mod tests {
use crate::progress_plugin::ProgressPlugin;
use crate::table_plugin::TablePlugin;
#[allow(unused_imports)]
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
fn headless_app() -> App {
let mut app = App::new();
+2 -2
View File
@@ -850,7 +850,7 @@ mod tests {
fn shake_anim_skipped_under_reduce_motion() {
use bevy::ecs::message::Messages;
use solitaire_core::Tableau;
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
use solitaire_data::Settings;
let mut app = App::new();
@@ -904,7 +904,7 @@ mod tests {
#[test]
fn foundation_flourish_skipped_under_reduce_motion() {
use bevy::ecs::message::Messages;
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
use solitaire_data::Settings;
let mut app = App::new();
+3 -3
View File
@@ -14,8 +14,8 @@ use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use bevy::window::AppLifecycle;
use solitaire_core::KlondikePile;
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}};
use solitaire_data::solver::{SolverConfig, SolverResult, try_solve};
#[allow(deprecated)]
use solitaire_data::latest_replay_path;
use solitaire_data::{
@@ -316,7 +316,7 @@ fn seed_from_system_time() -> u64 {
}
/// Walks forward from `initial_seed` (incrementing by 1 with wrapping
/// arithmetic) until the [`solitaire_core::solver`] returns a verdict
/// arithmetic) until the [`solitaire_data::solver`] returns a verdict
/// the engine accepts as winnable, or until [`SOLVER_DEAL_RETRY_CAP`]
/// attempts have elapsed.
///
+1 -1
View File
@@ -16,7 +16,7 @@
use bevy::input::ButtonInput;
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::prelude::*;
use solitaire_core::game_state::{DifficultyLevel, DrawMode};
use solitaire_core::{DrawMode, game_state::DifficultyLevel};
use solitaire_data::save_settings_to;
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
+2 -2
View File
@@ -10,7 +10,7 @@ use bevy::prelude::*;
use bevy::window::WindowResized;
use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::Suit;
use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::{DrawMode, game_state::GameMode};
use crate::auto_complete_plugin::AutoCompleteState;
#[cfg(not(target_arch = "wasm32"))]
@@ -2726,7 +2726,7 @@ mod tests {
use crate::game_plugin::GamePlugin;
use crate::table_plugin::TablePlugin;
use chrono::Local;
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
fn headless_app() -> App {
let mut app = App::new();
+5 -5
View File
@@ -52,7 +52,7 @@ use crate::settings_plugin::SettingsResource;
use crate::time_attack_plugin::TimeAttackResource;
use crate::touch_selection_plugin::TouchSelectionState;
use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_SUCCESS, STATE_WARNING};
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
/// System-set labels used to anchor external systems relative to the touch
/// drag pipeline without duplicating the internal chain ordering.
@@ -79,13 +79,13 @@ fn dragged_card_z(index: usize) -> f32 {
/// Solver budgets used by the H-key hint system.
///
/// Wraps `solitaire_core::solver::SolverConfig` as a Bevy resource so
/// Wraps `solitaire_data::solver::SolverConfig` as a Bevy resource so
/// tests can inject tighter budgets to exercise the heuristic-fallback
/// path. Production initialises this to `SolverConfig::default()` (100k
/// move / 200k state budgets, the same numbers the new-game retry loop
/// uses).
#[derive(Resource, Debug, Clone, Default)]
pub struct HintSolverConfig(pub solitaire_core::solver::SolverConfig);
pub struct HintSolverConfig(pub solitaire_data::solver::SolverConfig);
/// Registers keyboard, mouse, and touch input systems.
///
@@ -1789,7 +1789,7 @@ mod tests {
use super::*;
use crate::layout::compute_layout;
use solitaire_core::{Foundation, Tableau};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
fn clear_test_piles(game: &mut GameState) {
game.set_test_stock_cards(Vec::new());
@@ -2029,7 +2029,7 @@ mod tests {
#[test]
fn find_draggable_draw_three_waste_top_card_hit_at_fanned_position() {
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::{DrawMode, game_state::GameMode};
let mut game = GameState::new_with_mode(1, DrawMode::DrawThree, GameMode::Classic);
// Three waste cards; top (id=202) is rightmost in the fan.
game.set_test_waste_cards(vec![
+3 -3
View File
@@ -21,7 +21,7 @@
//! active opens the overlay as normal.
use bevy::prelude::*;
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
use solitaire_data::save_game_state_to;
use crate::events::{
@@ -965,7 +965,7 @@ mod tests {
/// Provides a fresh `GameStateResource` (not won) so the modal can
/// open. `move_count` doesn't matter — the gate is just `!is_won`.
fn forfeit_app() -> App {
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
let mut app = App::new();
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
app.init_resource::<ButtonInput<KeyCode>>();
@@ -1020,7 +1020,7 @@ mod tests {
/// hotkey was received but is currently a no-op.
#[test]
fn forfeit_request_emits_toast_and_skips_modal_when_game_is_won() {
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
let mut app = App::new();
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
app.init_resource::<ButtonInput<KeyCode>>();
+2 -2
View File
@@ -28,7 +28,7 @@ use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use solitaire_core::KlondikePile;
use solitaire_core::game_state::GameState;
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve_from_state};
use solitaire_data::solver::{SolverConfig, SolverResult, try_solve_from_state};
use crate::card_plugin::CardEntity;
use crate::events::{HintVisualEvent, InfoToastEvent, StateChangedEvent};
@@ -188,7 +188,7 @@ mod tests {
use crate::input_plugin::HintSolverConfig;
use solitaire_core::{Foundation, Tableau};
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
/// Build a minimal Bevy app exercising only the polling system
/// and the resources/messages it touches.
+2 -2
View File
@@ -23,8 +23,8 @@
use bevy::input::ButtonInput;
use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use solitaire_core::game_state::DrawMode;
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
use solitaire_core::DrawMode;
use solitaire_data::solver::{SolverConfig, SolverResult, try_solve};
use crate::events::{NewGameRequestEvent, StartPlayBySeedRequestEvent};
use crate::font_plugin::FontResource;
+1 -1
View File
@@ -795,7 +795,7 @@ mod tests {
use crate::layout::compute_layout;
use bevy::ecs::message::Messages;
use solitaire_core::card::{Card as CoreCard, Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
/// Build a minimal Bevy app wired with `RadialMenuPlugin` and the
/// resources / messages it depends on. No window, no camera — the
+3 -3
View File
@@ -2,7 +2,7 @@ use super::*;
use chrono::NaiveDate;
use solitaire_core::{Foundation, KlondikePile, Tableau};
use solitaire_core::card::{Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::{DrawMode, game_state::GameMode};
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
use solitaire_data::{Replay, ReplayMove};
@@ -2314,7 +2314,7 @@ fn format_suit_glyph_all_suits() {
fn format_foundations_row_empty_board() {
let game = solitaire_core::game_state::GameState::new_with_mode(
42,
solitaire_core::game_state::DrawMode::DrawOne,
solitaire_core::DrawMode::DrawOne,
solitaire_core::game_state::GameMode::Classic,
);
assert_eq!(format_foundations_row(&game), "F: -- -- -- --");
@@ -2326,7 +2326,7 @@ fn format_foundations_row_empty_board() {
fn format_stock_waste_row_initial_state() {
let game = solitaire_core::game_state::GameState::new_with_mode(
42,
solitaire_core::game_state::DrawMode::DrawOne,
solitaire_core::DrawMode::DrawOne,
solitaire_core::game_state::GameMode::Classic,
);
let text = format_stock_waste_row(&game);
+1 -1
View File
@@ -556,7 +556,7 @@ mod tests {
use bevy::time::TimeUpdateStrategy;
use chrono::NaiveDate;
use solitaire_core::{KlondikePile, Tableau};
use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::{DrawMode, game_state::GameMode};
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
use std::time::Duration;
+1 -1
View File
@@ -980,7 +980,7 @@ mod tests {
use bevy::ecs::message::Messages;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
/// Build a minimal app with `SelectionPlugin` only — no GamePlugin, no
/// AssetServer. The `MoveRequestEvent` / `StateChangedEvent` /
+2 -2
View File
@@ -15,7 +15,7 @@ use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::prelude::*;
use bevy::ui::{ComputedNode, UiGlobalTransform};
use bevy::window::{WindowMoved, WindowResized};
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
use solitaire_data::{
AnimSpeed, REPLAY_MOVE_INTERVAL_STEP_SECS, Settings, TIME_BONUS_MULTIPLIER_STEP,
TOOLTIP_DELAY_STEP_SECS, WindowGeometry, load_settings_from, save_settings_to, settings::Theme,
@@ -241,7 +241,7 @@ enum SettingsButton {
ToggleTouchInputMode,
/// Toggle the [`Settings::winnable_deals_only`] flag. When on, new
/// random Classic-mode deals are filtered through
/// [`solitaire_core::solver::try_solve`] until one is provably
/// [`solitaire_data::solver::try_solve`] until one is provably
/// winnable (or the retry cap is hit). Off by default.
ToggleWinnableDealsOnly,
/// Toggle the inverse of [`Settings::disable_smart_default_size`].
+2 -2
View File
@@ -1327,7 +1327,7 @@ mod tests {
app.world_mut()
.resource_mut::<crate::resources::GameStateResource>()
.0
.draw_mode = solitaire_core::game_state::DrawMode::DrawThree;
.draw_mode = solitaire_core::DrawMode::DrawThree;
app.world_mut().write_message(GameWonEvent {
score: 500,
@@ -1952,7 +1952,7 @@ mod tests {
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 8).expect("valid date");
let mut r = solitaire_data::Replay::new(
1,
solitaire_core::game_state::DrawMode::DrawOne,
solitaire_core::DrawMode::DrawOne,
solitaire_core::game_state::GameMode::Classic,
time_seconds,
0,
+1 -1
View File
@@ -604,7 +604,7 @@ mod tests {
/// would silently drop the link.
#[test]
fn upload_result_writes_share_url_into_replay_and_persists() {
use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::{DrawMode, game_state::GameMode};
use solitaire_data::{
Replay, ReplayHistory, load_replay_history_from, save_replay_history_to,
};
+1 -1
View File
@@ -299,7 +299,7 @@ mod tests {
use crate::game_plugin::GamePlugin;
use crate::progress_plugin::ProgressPlugin;
use crate::table_plugin::TablePlugin;
use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::{DrawMode, game_state::GameState};
fn headless_app() -> App {
let mut app = App::new();
+3 -3
View File
@@ -1210,7 +1210,7 @@ mod tests {
.insert_resource(StatsResource(StatsSnapshot::default()))
.insert_resource(GameStateResource(GameState::new(
0,
solitaire_core::game_state::DrawMode::DrawOne,
solitaire_core::DrawMode::DrawOne,
)))
.insert_resource(ProgressResource(PlayerProgress::default()));
app.update();
@@ -1539,7 +1539,7 @@ mod tests {
.challenge_index = 4;
// Switch game mode to Challenge.
{
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
}
@@ -1585,7 +1585,7 @@ mod tests {
/// mode-multiplier rows.
#[test]
fn cache_win_data_captures_undo_count_and_mode() {
use solitaire_core::game_state::DrawMode;
use solitaire_core::DrawMode;
let mut app = make_app();
// Set up a Zen-mode game with 2 undos used.