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:
@@ -819,7 +819,7 @@ mod tests {
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.set_test_draw_mode(solitaire_core::DrawMode::DrawThree);
|
||||
.set_test_draw_mode(solitaire_core::DrawStockConfig::DrawThree);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
@@ -868,7 +868,7 @@ mod tests {
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.set_test_draw_mode(solitaire_core::DrawMode::DrawThree);
|
||||
.set_test_draw_mode(solitaire_core::DrawStockConfig::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::{DrawMode, game_state::GameMode};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
use solitaire_data::{Replay, ReplayMove};
|
||||
|
||||
/// Headless app variant that injects a default `ReplayPlaybackState`
|
||||
@@ -1409,7 +1409,7 @@ mod tests {
|
||||
fn dummy_replay() -> Replay {
|
||||
Replay::new(
|
||||
1,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
GameMode::Classic,
|
||||
10,
|
||||
100,
|
||||
|
||||
@@ -169,7 +169,7 @@ mod tests {
|
||||
use crate::table_plugin::TablePlugin;
|
||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::card::{Deck, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
@@ -183,7 +183,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn seeded_state_with_auto_move() -> (GameState, (KlondikePile, KlondikePile)) {
|
||||
let mut g = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut g = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
@@ -227,7 +227,7 @@ mod tests {
|
||||
#[test]
|
||||
fn detect_activates_when_auto_completable() {
|
||||
let mut app = headless_app();
|
||||
let mut g = GameState::new(42, DrawMode::DrawOne);
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
g.set_test_auto_completable(true);
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 = g;
|
||||
app.world_mut().write_message(StateChangedEvent);
|
||||
|
||||
@@ -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::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
|
||||
use crate::card_animation::CardAnimation;
|
||||
@@ -789,8 +789,8 @@ fn sync_cards(
|
||||
// and its rank/suit peek behind the incoming card.
|
||||
let waste_buffer_id: Option<Card> = {
|
||||
let visible = match game.draw_mode() {
|
||||
DrawMode::DrawOne => 1_usize,
|
||||
DrawMode::DrawThree => 3_usize,
|
||||
DrawStockConfig::DrawOne => 1_usize,
|
||||
DrawStockConfig::DrawThree => 3_usize,
|
||||
};
|
||||
let waste_cards = game.waste_cards();
|
||||
(waste_cards.len() > visible)
|
||||
@@ -958,8 +958,8 @@ fn card_positions(game: &GameState, layout: &Layout) -> Vec<((Card, bool), Vec2,
|
||||
// shows up to 3 fanned in X (matching the standard Klondike presentation).
|
||||
let render_start = if is_waste {
|
||||
let visible = match game.draw_mode() {
|
||||
DrawMode::DrawOne => 1_usize,
|
||||
DrawMode::DrawThree => 3_usize,
|
||||
DrawStockConfig::DrawOne => 1_usize,
|
||||
DrawStockConfig::DrawThree => 3_usize,
|
||||
};
|
||||
// Render one extra card so that the card sliding off the waste
|
||||
// during a draw animation is still present in the world at z=0
|
||||
@@ -972,7 +972,7 @@ fn card_positions(game: &GameState, layout: &Layout) -> Vec<((Card, bool), Vec2,
|
||||
let mut y_offset = 0.0_f32;
|
||||
let rendered_len = cards[render_start..].len();
|
||||
for (slot, (card, face_up)) in cards[render_start..].iter().enumerate() {
|
||||
let x_offset = if is_waste && matches!(game.draw_mode(), DrawMode::DrawThree) {
|
||||
let x_offset = if is_waste && matches!(game.draw_mode(), DrawStockConfig::DrawThree) {
|
||||
// When len > visible, slot 0 is a hidden buffer card kept at
|
||||
// x=0 to prevent a flash during the draw tween. When len ≤
|
||||
// visible (small pile), every card is visible and should fan
|
||||
@@ -2566,7 +2566,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::DrawMode::DrawOne);
|
||||
let g = GameState::new(42, solitaire_core::DrawStockConfig::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);
|
||||
@@ -2574,8 +2574,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn waste_draw_one_only_renders_top_card() {
|
||||
use solitaire_core::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawOne);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
// Draw 3 cards so the waste pile has 3 cards.
|
||||
for _ in 0..3 {
|
||||
let _ = g.draw();
|
||||
@@ -2612,8 +2612,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn waste_draw_three_renders_up_to_three_fanned_cards() {
|
||||
use solitaire_core::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawThree);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawThree);
|
||||
// 5 draw() calls in Draw-Three mode accumulates multiple waste cards.
|
||||
for _ in 0..5 {
|
||||
let _ = g.draw();
|
||||
@@ -2666,8 +2666,8 @@ 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::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawThree);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::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).
|
||||
let _ = g.draw();
|
||||
@@ -2709,8 +2709,8 @@ 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::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawOne);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
// Draw 3 times so the waste pile has 3 cards and the buffer exists.
|
||||
for _ in 0..3 {
|
||||
let _ = g.draw();
|
||||
@@ -2740,7 +2740,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn card_positions_tableau_cards_are_fanned_downward() {
|
||||
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
|
||||
let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
|
||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
let positions = card_positions(&g, &layout);
|
||||
|
||||
@@ -3083,7 +3083,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn facedown_cards_use_tighter_fan_than_uniform_faceup_fan() {
|
||||
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
|
||||
let g = GameState::new(42, solitaire_core::DrawStockConfig::DrawOne);
|
||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
let positions = card_positions(&g, &layout);
|
||||
|
||||
@@ -3533,7 +3533,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stock_card_count_helper_reads_zero_for_empty_stock() {
|
||||
let g = GameState::new(42, solitaire_core::DrawMode::DrawOne);
|
||||
let g = GameState::new(42, solitaire_core::DrawStockConfig::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);
|
||||
@@ -3805,8 +3805,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn waste_pile_cards_have_strictly_increasing_z() {
|
||||
use solitaire_core::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawThree);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawThree);
|
||||
for _ in 0..5 {
|
||||
let _ = g.draw();
|
||||
}
|
||||
@@ -3849,8 +3849,8 @@ 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::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawThree);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawThree);
|
||||
for _ in 0..5 {
|
||||
let _ = g.draw();
|
||||
}
|
||||
@@ -3885,8 +3885,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn waste_pile_draw_one_cards_have_distinct_z() {
|
||||
use solitaire_core::DrawMode;
|
||||
let mut g = GameState::new(42, DrawMode::DrawOne);
|
||||
use solitaire_core::DrawStockConfig;
|
||||
let mut g = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
for _ in 0..3 {
|
||||
let _ = g.draw();
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ mod tests {
|
||||
use crate::game_plugin::GamePlugin;
|
||||
use crate::progress_plugin::ProgressPlugin;
|
||||
use crate::table_plugin::TablePlugin;
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
@@ -135,7 +135,7 @@ mod tests {
|
||||
fn challenge_win_advances_index() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
|
||||
GameState::new_with_mode(1, DrawStockConfig::DrawOne, GameMode::Challenge);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
@@ -224,7 +224,7 @@ mod tests {
|
||||
.0
|
||||
.challenge_index = 2;
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
|
||||
GameState::new_with_mode(1, DrawStockConfig::DrawOne, GameMode::Challenge);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
|
||||
@@ -36,7 +36,7 @@ use bevy::prelude::*;
|
||||
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
|
||||
use solitaire_core::card::Card;
|
||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
use crate::card_plugin::RightClickHighlight;
|
||||
use crate::layout::{Layout, LayoutResource};
|
||||
@@ -437,7 +437,7 @@ fn tableau_or_stack_pos(
|
||||
base.x,
|
||||
base.y - layout.card_size.y * layout.tableau_fan_frac * (index as f32),
|
||||
)
|
||||
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawMode::DrawThree {
|
||||
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree {
|
||||
let pile_len = game.waste_cards().len();
|
||||
let visible_start = pile_len.saturating_sub(3);
|
||||
let slot = index.saturating_sub(visible_start) as f32;
|
||||
@@ -563,9 +563,9 @@ mod tests {
|
||||
#[test]
|
||||
fn cursor_over_draggable_returns_false_for_empty_game() {
|
||||
use crate::layout::compute_layout;
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
// A cursor far off-screen should never hit anything.
|
||||
assert!(!cursor_over_draggable(
|
||||
@@ -581,7 +581,7 @@ mod tests {
|
||||
|
||||
use crate::layout::compute_layout;
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}};
|
||||
use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
|
||||
|
||||
/// Builds an `App` with `MinimalPlugins` and the overlay system
|
||||
/// registered, plus the resources the system needs. Callers
|
||||
@@ -629,7 +629,7 @@ mod tests {
|
||||
// 5 of Spades (black) onto Tableau(2)'s 6 of Clubs (also black)
|
||||
// — same colour family, illegal. Tableau(2) must NOT be
|
||||
// highlighted.
|
||||
let mut game = GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::Classic);
|
||||
let mut game = GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::Classic);
|
||||
set_tableau_top(
|
||||
&mut game,
|
||||
2,
|
||||
|
||||
@@ -362,7 +362,7 @@ mod tests {
|
||||
use crate::progress_plugin::ProgressPlugin;
|
||||
use crate::table_plugin::TablePlugin;
|
||||
#[allow(unused_imports)]
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
|
||||
// Replace the GameState with one whose seed matches the daily seed.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(daily_seed, DrawMode::DrawOne);
|
||||
GameState::new(daily_seed, DrawStockConfig::DrawOne);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
@@ -419,7 +419,7 @@ mod tests {
|
||||
let daily_seed = app.world().resource::<DailyChallengeResource>().seed;
|
||||
// Use a deliberately different seed.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(daily_seed.wrapping_add(7777), DrawMode::DrawOne);
|
||||
GameState::new(daily_seed.wrapping_add(7777), DrawStockConfig::DrawOne);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
@@ -442,7 +442,7 @@ mod tests {
|
||||
let mut app = headless_app();
|
||||
let daily_seed = app.world().resource::<DailyChallengeResource>().seed;
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(daily_seed, DrawMode::DrawOne);
|
||||
GameState::new(daily_seed, DrawStockConfig::DrawOne);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
|
||||
@@ -846,13 +846,13 @@ mod tests {
|
||||
fn shake_anim_skipped_under_reduce_motion() {
|
||||
use bevy::ecs::message::Messages;
|
||||
use solitaire_core::Tableau;
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
use solitaire_data::Settings;
|
||||
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins)
|
||||
.add_plugins(FeedbackAnimPlugin);
|
||||
app.insert_resource(GameStateResource(GameState::new(1, DrawMode::DrawOne)));
|
||||
app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
|
||||
app.insert_resource(SettingsResource(Settings {
|
||||
reduce_motion_mode: true,
|
||||
..Settings::default()
|
||||
@@ -900,13 +900,13 @@ mod tests {
|
||||
#[test]
|
||||
fn foundation_flourish_skipped_under_reduce_motion() {
|
||||
use bevy::ecs::message::Messages;
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
use solitaire_data::Settings;
|
||||
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins)
|
||||
.add_plugins(FeedbackAnimPlugin);
|
||||
app.insert_resource(GameStateResource(GameState::new(1, DrawMode::DrawOne)));
|
||||
app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
|
||||
app.insert_resource(SettingsResource(Settings {
|
||||
reduce_motion_mode: true,
|
||||
..Settings::default()
|
||||
|
||||
@@ -14,7 +14,7 @@ use bevy::prelude::*;
|
||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||
use bevy::window::AppLifecycle;
|
||||
use solitaire_core::KlondikePile;
|
||||
use solitaire_core::{DrawMode, game_state::{GameMode, GameState}};
|
||||
use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
|
||||
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET};
|
||||
#[allow(deprecated)]
|
||||
use solitaire_data::latest_replay_path;
|
||||
@@ -157,12 +157,12 @@ impl Plugin for GamePlugin {
|
||||
.is_some_and(|g| g.move_count() > 0 && !g.is_won());
|
||||
let (initial_state, pending_restore) = if prompt_worthy {
|
||||
(
|
||||
GameState::new(seed_from_system_time(), DrawMode::DrawOne),
|
||||
GameState::new(seed_from_system_time(), DrawStockConfig::DrawOne),
|
||||
saved,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
saved.unwrap_or_else(|| GameState::new(seed_from_system_time(), DrawMode::DrawOne)),
|
||||
saved.unwrap_or_else(|| GameState::new(seed_from_system_time(), DrawStockConfig::DrawOne)),
|
||||
None,
|
||||
)
|
||||
};
|
||||
@@ -388,7 +388,7 @@ fn poll_pending_new_game_seed(
|
||||
|
||||
/// Pure helper extracted for testability — `new_game_with_solver_*`
|
||||
/// engine tests in the same file exercise this path.
|
||||
pub(crate) fn choose_winnable_seed(initial_seed: u64, draw_mode: DrawMode) -> u64 {
|
||||
pub(crate) fn choose_winnable_seed(initial_seed: u64, draw_mode: DrawStockConfig) -> u64 {
|
||||
let mut seed = initial_seed;
|
||||
for _ in 0..SOLVER_DEAL_RETRY_CAP {
|
||||
match GameState::solve_fresh_deal(
|
||||
@@ -830,8 +830,8 @@ fn handle_draw(
|
||||
Vec::new()
|
||||
} else {
|
||||
let draw_count = match game.0.draw_mode() {
|
||||
DrawMode::DrawOne => 1_usize,
|
||||
DrawMode::DrawThree => 3_usize,
|
||||
DrawStockConfig::DrawOne => 1_usize,
|
||||
DrawStockConfig::DrawThree => 3_usize,
|
||||
};
|
||||
let n = stock.len();
|
||||
let take = n.min(draw_count);
|
||||
@@ -1324,7 +1324,7 @@ mod tests {
|
||||
app.insert_resource(PendingRestoredGame(None));
|
||||
// Override the system-time seed with a known value.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(seed, DrawMode::DrawOne);
|
||||
GameState::new(seed, DrawStockConfig::DrawOne);
|
||||
app
|
||||
}
|
||||
|
||||
@@ -1540,7 +1540,7 @@ mod tests {
|
||||
app.insert_resource(GameStatePath(Some(path.clone())));
|
||||
// Override the seed so we can verify it was written.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(7654, DrawMode::DrawOne);
|
||||
GameState::new(7654, DrawStockConfig::DrawOne);
|
||||
|
||||
app.world_mut().write_message(AppExit::Success);
|
||||
app.update();
|
||||
@@ -1559,7 +1559,7 @@ mod tests {
|
||||
|
||||
let path = tmp_gs_path("new_game_delete");
|
||||
// Pre-create a saved file.
|
||||
save_game_state_to(&path, &GameState::new(1, DrawMode::DrawOne)).unwrap();
|
||||
save_game_state_to(&path, &GameState::new(1, DrawStockConfig::DrawOne)).unwrap();
|
||||
assert!(path.exists());
|
||||
|
||||
let mut app = test_app(1);
|
||||
@@ -1693,7 +1693,7 @@ mod tests {
|
||||
fn has_legal_moves_returns_true_for_fresh_game() {
|
||||
// A fresh deal always has a non-empty stock (24 cards), so drawing
|
||||
// is always a legal move regardless of the initial face-up tableau cards.
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
assert!(
|
||||
has_legal_moves(&game),
|
||||
"fresh deal must contain at least one legal move"
|
||||
@@ -1707,7 +1707,7 @@ mod tests {
|
||||
// immediately placed. The game is only stuck when both stock AND waste
|
||||
// are exhausted and no visible card can be moved.
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
@@ -1743,7 +1743,7 @@ mod tests {
|
||||
#[test]
|
||||
fn has_legal_moves_returns_true_when_ace_can_go_to_foundation() {
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
// Empty stock and waste so draw is NOT available.
|
||||
game.set_test_stock_cards(Vec::new());
|
||||
@@ -1787,7 +1787,7 @@ mod tests {
|
||||
// card of its column the previous code would return false (softlock)
|
||||
// even though the player can still move that run.
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
game.set_test_stock_cards(Vec::new());
|
||||
game.set_test_waste_cards(Vec::new());
|
||||
@@ -2185,7 +2185,7 @@ mod tests {
|
||||
assert_eq!(loaded.seed, 7654, "seed must match the live game state");
|
||||
assert_eq!(
|
||||
loaded.draw_mode,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
"draw_mode must be captured"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2326,7 +2326,7 @@ mod tests {
|
||||
"with solver toggle off, the requested seed must be honoured exactly"
|
||||
);
|
||||
// Cross-check: the dealt tableau must match GameState::new(999) byte-for-byte.
|
||||
let expected = GameState::new(999, DrawMode::DrawOne);
|
||||
let expected = GameState::new(999, DrawStockConfig::DrawOne);
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
@@ -2403,7 +2403,7 @@ mod tests {
|
||||
//
|
||||
// Seed 394 was previously Unwinnable under the old DFS; now it resolves
|
||||
// as Inconclusive, so the helper must accept it immediately.
|
||||
let chosen = choose_winnable_seed(394, DrawMode::DrawOne);
|
||||
let chosen = choose_winnable_seed(394, DrawStockConfig::DrawOne);
|
||||
assert_eq!(
|
||||
chosen, 394,
|
||||
"seed 394 resolves as Inconclusive; choose_winnable_seed must accept it as-is"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
use bevy::input::ButtonInput;
|
||||
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
|
||||
use bevy::prelude::*;
|
||||
use solitaire_core::{DrawMode, game_state::DifficultyLevel};
|
||||
use solitaire_core::{DrawStockConfig, game_state::DifficultyLevel};
|
||||
use solitaire_data::save_settings_to;
|
||||
|
||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||
@@ -432,7 +432,7 @@ fn build_home_context<'a>(
|
||||
zen_best: stats.map_or(0, |s| s.0.zen_best_score),
|
||||
challenge_best: stats.map_or(0, |s| s.0.challenge_best_score),
|
||||
daily_today,
|
||||
draw_mode: settings.map(|s| s.0.draw_mode).unwrap_or(DrawMode::DrawOne),
|
||||
draw_mode: settings.map(|s| s.0.draw_mode).unwrap_or(DrawStockConfig::DrawOne),
|
||||
font_res,
|
||||
difficulty_expanded,
|
||||
last_difficulty: settings.and_then(|s| s.0.last_difficulty),
|
||||
@@ -620,9 +620,9 @@ fn handle_home_draw_mode_buttons(
|
||||
return;
|
||||
};
|
||||
let target = if want_one {
|
||||
DrawMode::DrawOne
|
||||
DrawStockConfig::DrawOne
|
||||
} else {
|
||||
DrawMode::DrawThree
|
||||
DrawStockConfig::DrawThree
|
||||
};
|
||||
if settings.0.draw_mode == target {
|
||||
return; // already in this mode — avoid a redundant respawn.
|
||||
@@ -857,7 +857,7 @@ struct HomeContext<'a> {
|
||||
challenge_best: u32,
|
||||
daily_streak: u32,
|
||||
daily_today: Option<DailyToday>,
|
||||
draw_mode: DrawMode,
|
||||
draw_mode: DrawStockConfig,
|
||||
font_res: Option<&'a FontResource>,
|
||||
/// Whether the difficulty section header is currently expanded.
|
||||
difficulty_expanded: bool,
|
||||
@@ -1038,7 +1038,7 @@ fn spawn_draw_mode_row(parent: &mut ChildSpawnerCommands, ctx: &HomeContext<'_>)
|
||||
..default()
|
||||
};
|
||||
|
||||
let active_one = matches!(ctx.draw_mode, DrawMode::DrawOne);
|
||||
let active_one = matches!(ctx.draw_mode, DrawStockConfig::DrawOne);
|
||||
|
||||
parent
|
||||
.spawn(Node {
|
||||
|
||||
@@ -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::{DrawMode, game_state::GameMode};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
|
||||
use crate::auto_complete_plugin::AutoCompleteState;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -2284,8 +2284,8 @@ fn update_hud(
|
||||
if let Ok(mut t) = mode_q.single_mut() {
|
||||
**t = match g.mode {
|
||||
GameMode::Classic => match g.draw_mode() {
|
||||
DrawMode::DrawOne => String::new(),
|
||||
DrawMode::DrawThree => "Draw 3".to_string(),
|
||||
DrawStockConfig::DrawOne => String::new(),
|
||||
DrawStockConfig::DrawThree => "Draw 3".to_string(),
|
||||
},
|
||||
GameMode::Zen => "ZEN".to_string(),
|
||||
GameMode::Challenge => "CHALLENGE".to_string(),
|
||||
@@ -2334,7 +2334,7 @@ fn update_hud(
|
||||
|
||||
// --- Draw-cycle indicator (Draw-Three mode only) ---
|
||||
if let Ok(mut t) = draw_cycle_q.single_mut() {
|
||||
**t = if g.is_won() || g.draw_mode() != DrawMode::DrawThree {
|
||||
**t = if g.is_won() || g.draw_mode() != DrawStockConfig::DrawThree {
|
||||
// Hide when not in Draw-Three or after the game is won.
|
||||
String::new()
|
||||
} else {
|
||||
@@ -2726,7 +2726,7 @@ mod tests {
|
||||
use crate::game_plugin::GamePlugin;
|
||||
use crate::table_plugin::TablePlugin;
|
||||
use chrono::Local;
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
@@ -2747,7 +2747,7 @@ mod tests {
|
||||
fn update_hud_runs_after_game_mutation_without_panic() {
|
||||
let mut app = headless_app();
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(42, DrawMode::DrawOne);
|
||||
GameState::new(42, DrawStockConfig::DrawOne);
|
||||
app.update();
|
||||
}
|
||||
|
||||
@@ -2784,7 +2784,7 @@ mod tests {
|
||||
use solitaire_core::game_state::GameMode;
|
||||
let mut app = headless_app();
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(42, DrawMode::DrawThree, GameMode::Classic);
|
||||
GameState::new_with_mode(42, DrawStockConfig::DrawThree, GameMode::Classic);
|
||||
app.update();
|
||||
assert_eq!(read_hud_text::<HudMode>(&mut app), "Draw 3");
|
||||
}
|
||||
@@ -2794,7 +2794,7 @@ mod tests {
|
||||
use solitaire_core::game_state::GameMode;
|
||||
let mut app = headless_app();
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(42, DrawMode::DrawOne, GameMode::Zen);
|
||||
GameState::new_with_mode(42, DrawStockConfig::DrawOne, GameMode::Zen);
|
||||
app.update();
|
||||
// Zen mode spec: "No score display" → text must be empty.
|
||||
assert_eq!(read_hud_text::<HudScore>(&mut app), "");
|
||||
@@ -3037,7 +3037,7 @@ mod tests {
|
||||
let mut app = headless_app();
|
||||
// Draw-One, no recycles yet — text must be empty.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(42, DrawMode::DrawOne);
|
||||
GameState::new(42, DrawStockConfig::DrawOne);
|
||||
app.update();
|
||||
assert_eq!(read_hud_text::<HudRecycles>(&mut app), "");
|
||||
}
|
||||
@@ -3047,7 +3047,7 @@ mod tests {
|
||||
let mut app = headless_app();
|
||||
// Draw-Three, no recycles yet — text must also be empty.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new(42, DrawMode::DrawThree);
|
||||
GameState::new(42, DrawStockConfig::DrawThree);
|
||||
app.update();
|
||||
assert_eq!(read_hud_text::<HudRecycles>(&mut app), "");
|
||||
}
|
||||
@@ -3055,7 +3055,7 @@ mod tests {
|
||||
#[test]
|
||||
fn recycles_hud_shows_count_draw_three() {
|
||||
let mut app = headless_app();
|
||||
let mut gs = GameState::new(42, DrawMode::DrawThree);
|
||||
let mut gs = GameState::new(42, DrawStockConfig::DrawThree);
|
||||
gs.force_test_recycles(3);
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 = gs;
|
||||
app.update();
|
||||
@@ -3066,7 +3066,7 @@ mod tests {
|
||||
fn recycles_hud_shows_count_draw_one() {
|
||||
let mut app = headless_app();
|
||||
// Draw-One with recycle_count > 0 must now show the counter too.
|
||||
let mut gs = GameState::new(42, DrawMode::DrawOne);
|
||||
let mut gs = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
gs.force_test_recycles(2);
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 = gs;
|
||||
app.update();
|
||||
|
||||
@@ -54,7 +54,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::DrawMode;
|
||||
use solitaire_core::DrawStockConfig;
|
||||
|
||||
/// System-set labels used to anchor external systems relative to the touch
|
||||
/// drag pipeline without duplicating the internal chain ordering.
|
||||
@@ -1173,7 +1173,7 @@ fn card_position(
|
||||
y_offset -= layout.card_size.y * step;
|
||||
}
|
||||
Vec2::new(base.x, base.y + y_offset)
|
||||
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawMode::DrawThree {
|
||||
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree {
|
||||
// In Draw-Three mode the top 3 waste cards are fanned in X to match
|
||||
// card_plugin::card_positions(). Hit-testing must use the same offsets
|
||||
// so clicking the visually rightmost (top) card actually registers.
|
||||
@@ -1830,7 +1830,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::layout::compute_layout;
|
||||
use solitaire_core::{Foundation, Tableau};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
fn clear_test_piles(game: &mut GameState) {
|
||||
game.set_test_stock_cards(Vec::new());
|
||||
@@ -1898,7 +1898,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_draggable_picks_top_of_tableau() {
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
|
||||
// In tableau 6, the visually topmost card is the last (face-up) one.
|
||||
@@ -1912,7 +1912,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_draggable_skips_face_down_cards() {
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
|
||||
// Tableau 6 has 7 cards: 6 face-down (indices 0..5) + 1 face-up at
|
||||
@@ -1934,7 +1934,7 @@ mod tests {
|
||||
// at 0.12 — so for any column with face-down cards above the
|
||||
// face-up bottom card, clicking the visible card face missed the
|
||||
// hit-test box and only the bottom strip of the card responded.
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
|
||||
// Tableau 6 starts with 6 face-down + 1 face-up. The face-up card
|
||||
@@ -1952,7 +1952,7 @@ mod tests {
|
||||
#[test]
|
||||
fn find_draggable_returns_run_when_picking_mid_stack() {
|
||||
// Manually construct a tableau with three face-up cards all stacked.
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let king = Card::new(D::Deck1, Suit::Spades, Rank::King);
|
||||
@@ -1979,7 +1979,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_draggable_skips_non_top_waste_card() {
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two);
|
||||
@@ -1998,7 +1998,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_drop_target_hits_empty_tableau_pile_marker() {
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
// Move all cards out of tableau 0 so its marker is the only drop area.
|
||||
let mut game = game;
|
||||
@@ -2015,7 +2015,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_drop_target_returns_none_for_origin() {
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
let pos = layout.pile_positions[&KlondikePile::Tableau(Tableau::Tableau4)];
|
||||
let target = find_drop_target(
|
||||
@@ -2029,7 +2029,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pile_drop_rect_extends_for_tableau_with_cards() {
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
// Tableau 6 has 7 cards.
|
||||
let (_, size) = pile_drop_rect(&KlondikePile::Tableau(Tableau::Tableau7), &layout, &game);
|
||||
@@ -2046,8 +2046,8 @@ mod tests {
|
||||
fn find_draggable_draw_three_waste_top_card_hit_at_fanned_position() {
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameMode};
|
||||
let mut game = GameState::new_with_mode(1, DrawMode::DrawThree, GameMode::Classic);
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
let mut game = GameState::new_with_mode(1, DrawStockConfig::DrawThree, GameMode::Classic);
|
||||
// Three waste cards; top (four_clubs) is rightmost in the fan.
|
||||
let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two);
|
||||
let three_hearts = Card::new(D::Deck1, Suit::Hearts, Rank::Three);
|
||||
@@ -2072,7 +2072,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn find_draggable_returns_none_for_click_on_empty_pile() {
|
||||
let mut game = GameState::new(42, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
// Clear tableau 0 so it's an empty slot.
|
||||
game.set_test_tableau_cards(Tableau::Tableau1, Vec::new());
|
||||
@@ -2086,7 +2086,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pile_drop_rect_is_card_sized_for_non_tableau() {
|
||||
let game = GameState::new(42, DrawMode::DrawOne);
|
||||
let game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||
for pile in [
|
||||
KlondikePile::Stock,
|
||||
@@ -2105,7 +2105,7 @@ mod tests {
|
||||
fn best_destination_returns_none_when_no_legal_move() {
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
// Clear everything except one card that has nowhere to go.
|
||||
clear_test_piles(&mut game);
|
||||
@@ -2123,7 +2123,7 @@ mod tests {
|
||||
fn best_tableau_destination_for_stack_skips_source_pile() {
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
clear_test_piles(&mut game);
|
||||
|
||||
@@ -2149,7 +2149,7 @@ mod tests {
|
||||
fn best_tableau_destination_for_stack_returns_none_when_no_legal_move() {
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
clear_test_piles(&mut game);
|
||||
|
||||
@@ -2178,7 +2178,7 @@ mod tests {
|
||||
fn find_hint_finds_ace_to_foundation() {
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
// Place Ace of Clubs on top of tableau 0.
|
||||
clear_test_piles(&mut game);
|
||||
@@ -2222,7 +2222,7 @@ mod tests {
|
||||
fn all_hints_suggests_draw_when_no_moves_and_stock_nonempty() {
|
||||
use solitaire_core::card::Deck as D;
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
|
||||
// Remove all foundation, tableau, and waste cards so no pile-to-pile
|
||||
// move exists. Leave one card in the stock.
|
||||
@@ -2431,7 +2431,7 @@ mod tests {
|
||||
app.insert_resource(crate::layout::LayoutResource(
|
||||
crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
|
||||
));
|
||||
app.insert_resource(GameStateResource(GameState::new(42, DrawMode::DrawOne)));
|
||||
app.insert_resource(GameStateResource(GameState::new(42, DrawStockConfig::DrawOne)));
|
||||
app.add_systems(Update, handle_keyboard_hint);
|
||||
|
||||
// Simulate the H key being pressed this frame.
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! active opens the overlay as normal.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use solitaire_core::DrawMode;
|
||||
use solitaire_core::DrawStockConfig;
|
||||
use solitaire_data::save_game_state_to;
|
||||
|
||||
use crate::events::{
|
||||
@@ -86,10 +86,10 @@ struct ForfeitConfirmButton;
|
||||
/// Returns the human-readable label for a draw mode.
|
||||
///
|
||||
/// Used on the pause overlay draw-mode toggle button.
|
||||
pub fn draw_mode_label(mode: DrawMode) -> &'static str {
|
||||
pub fn draw_mode_label(mode: DrawStockConfig) -> &'static str {
|
||||
match mode {
|
||||
DrawMode::DrawOne => "Draw 1",
|
||||
DrawMode::DrawThree => "Draw 3",
|
||||
DrawStockConfig::DrawOne => "Draw 1",
|
||||
DrawStockConfig::DrawThree => "Draw 3",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,9 +273,9 @@ fn handle_pause_draw_buttons(
|
||||
}
|
||||
let Some(mut settings) = settings else { return };
|
||||
let new_mode = if pressed_one {
|
||||
DrawMode::DrawOne
|
||||
DrawStockConfig::DrawOne
|
||||
} else {
|
||||
DrawMode::DrawThree
|
||||
DrawStockConfig::DrawThree
|
||||
};
|
||||
if settings.0.draw_mode == new_mode {
|
||||
return;
|
||||
@@ -477,7 +477,7 @@ fn spawn_pause_screen(
|
||||
commands: &mut Commands,
|
||||
level: Option<u32>,
|
||||
streak: Option<u32>,
|
||||
draw_mode: Option<DrawMode>,
|
||||
draw_mode: Option<DrawStockConfig>,
|
||||
font_res: Option<&FontResource>,
|
||||
) {
|
||||
spawn_modal(commands, PauseScreen, ui_theme::Z_PAUSE, |card| {
|
||||
@@ -516,7 +516,7 @@ fn spawn_pause_screen(
|
||||
/// `Tertiary` (recessed), giving an obvious selection state at a glance.
|
||||
fn spawn_draw_mode_row(
|
||||
parent: &mut ChildSpawnerCommands,
|
||||
mode: DrawMode,
|
||||
mode: DrawStockConfig,
|
||||
font_res: Option<&FontResource>,
|
||||
) {
|
||||
let label_font = TextFont {
|
||||
@@ -530,8 +530,8 @@ fn spawn_draw_mode_row(
|
||||
..default()
|
||||
};
|
||||
let (one_variant, three_variant) = match mode {
|
||||
DrawMode::DrawOne => (ButtonVariant::Secondary, ButtonVariant::Tertiary),
|
||||
DrawMode::DrawThree => (ButtonVariant::Tertiary, ButtonVariant::Secondary),
|
||||
DrawStockConfig::DrawOne => (ButtonVariant::Secondary, ButtonVariant::Tertiary),
|
||||
DrawStockConfig::DrawThree => (ButtonVariant::Tertiary, ButtonVariant::Secondary),
|
||||
};
|
||||
parent
|
||||
.spawn(Node {
|
||||
@@ -800,20 +800,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn draw_mode_label_draw_one() {
|
||||
assert_eq!(draw_mode_label(DrawMode::DrawOne), "Draw 1");
|
||||
assert_eq!(draw_mode_label(DrawStockConfig::DrawOne), "Draw 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_mode_label_draw_three() {
|
||||
assert_eq!(draw_mode_label(DrawMode::DrawThree), "Draw 3");
|
||||
assert_eq!(draw_mode_label(DrawStockConfig::DrawThree), "Draw 3");
|
||||
}
|
||||
|
||||
/// Both variants are covered so the match is exhaustive — this test would
|
||||
/// fail to compile if a new DrawMode variant were added without updating
|
||||
/// fail to compile if a new DrawStockConfig variant were added without updating
|
||||
/// `draw_mode_label`.
|
||||
#[test]
|
||||
fn draw_mode_label_covers_all_variants() {
|
||||
for mode in [DrawMode::DrawOne, DrawMode::DrawThree] {
|
||||
for mode in [DrawStockConfig::DrawOne, DrawStockConfig::DrawThree] {
|
||||
let label = draw_mode_label(mode);
|
||||
assert!(
|
||||
!label.is_empty(),
|
||||
@@ -842,7 +842,7 @@ mod tests {
|
||||
app.world_mut()
|
||||
.resource_mut::<SettingsResource>()
|
||||
.0
|
||||
.draw_mode = DrawMode::DrawOne;
|
||||
.draw_mode = DrawStockConfig::DrawOne;
|
||||
|
||||
// Set paused so handle_pause_draw_toggle acts.
|
||||
app.world_mut().resource_mut::<PausedResource>().0 = true;
|
||||
@@ -856,7 +856,7 @@ mod tests {
|
||||
let mode = &app.world().resource::<SettingsResource>().0.draw_mode;
|
||||
assert_eq!(
|
||||
*mode,
|
||||
DrawMode::DrawThree,
|
||||
DrawStockConfig::DrawThree,
|
||||
"pressing Draw 3 must set mode to DrawThree"
|
||||
);
|
||||
|
||||
@@ -869,7 +869,7 @@ mod tests {
|
||||
let mode2 = &app.world().resource::<SettingsResource>().0.draw_mode;
|
||||
assert_eq!(
|
||||
*mode2,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
"pressing Draw 1 must set mode to DrawOne"
|
||||
);
|
||||
|
||||
@@ -965,11 +965,11 @@ 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::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
app.insert_resource(GameStateResource(GameState::new(1, DrawMode::DrawOne)));
|
||||
app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
@@ -1020,11 +1020,11 @@ 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::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
game.set_test_won(true);
|
||||
app.insert_resource(GameStateResource(game));
|
||||
app.update();
|
||||
|
||||
@@ -180,7 +180,7 @@ mod tests {
|
||||
use crate::input_plugin::HintSolverConfig;
|
||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
/// Build a minimal Bevy app exercising only the polling system
|
||||
/// and the resources/messages it touches.
|
||||
@@ -209,7 +209,7 @@ mod tests {
|
||||
/// foundations hold A..Q for each suit, four Kings sit on
|
||||
/// tableau columns 0..3, stock and waste empty.
|
||||
fn near_finished_state() -> GameState {
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
|
||||
game.set_test_stock_cards(Vec::new());
|
||||
game.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
use bevy::input::ButtonInput;
|
||||
use bevy::prelude::*;
|
||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||
use solitaire_core::DrawMode;
|
||||
use solitaire_core::DrawStockConfig;
|
||||
use solitaire_core::game_state::GameState;
|
||||
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome};
|
||||
|
||||
@@ -340,7 +340,7 @@ fn tick_debounce_and_spawn_solver_task(
|
||||
|
||||
let draw_mode = settings
|
||||
.as_ref()
|
||||
.map_or(DrawMode::DrawOne, |s| s.0.draw_mode);
|
||||
.map_or(DrawStockConfig::DrawOne, |s| s.0.draw_mode);
|
||||
let task = AsyncComputeTaskPool::get().spawn(async move {
|
||||
GameState::solve_fresh_deal(
|
||||
seed,
|
||||
|
||||
@@ -797,7 +797,7 @@ mod tests {
|
||||
use crate::layout::compute_layout;
|
||||
use bevy::ecs::message::Messages;
|
||||
use solitaire_core::card::{Card as CoreCard, Deck, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
/// Build a minimal Bevy app wired with `RadialMenuPlugin` and the
|
||||
/// resources / messages it depends on. No window, no camera — the
|
||||
@@ -820,7 +820,7 @@ mod tests {
|
||||
/// destination — Foundation(0) — under the standard rules
|
||||
/// (`can_place_on_foundation` accepts the Ace on an empty foundation).
|
||||
fn ace_only_state() -> GameState {
|
||||
let mut g = GameState::new(0, DrawMode::DrawOne);
|
||||
let mut g = GameState::new(0, DrawStockConfig::DrawOne);
|
||||
// Wipe everything.
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
@@ -854,7 +854,7 @@ mod tests {
|
||||
/// Place a face-down King on Tableau(0). `find_top_face_up_card_at`
|
||||
/// must skip it.
|
||||
fn face_down_only_state() -> GameState {
|
||||
let mut g = GameState::new(0, DrawMode::DrawOne);
|
||||
let mut g = GameState::new(0, DrawStockConfig::DrawOne);
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
use chrono::NaiveDate;
|
||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::card::{Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameMode};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
||||
use solitaire_data::{Replay, ReplayMove};
|
||||
|
||||
@@ -13,7 +13,7 @@ use solitaire_data::{Replay, ReplayMove};
|
||||
fn synthetic_replay(move_count: usize) -> Replay {
|
||||
Replay::new(
|
||||
42,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
GameMode::Classic,
|
||||
120,
|
||||
1_000,
|
||||
@@ -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::DrawMode::DrawOne,
|
||||
solitaire_core::DrawStockConfig::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::DrawMode::DrawOne,
|
||||
solitaire_core::DrawStockConfig::DrawOne,
|
||||
solitaire_core::game_state::GameMode::Classic,
|
||||
);
|
||||
let text = format_stock_waste_row(&game);
|
||||
|
||||
@@ -556,7 +556,7 @@ mod tests {
|
||||
use bevy::time::TimeUpdateStrategy;
|
||||
use chrono::NaiveDate;
|
||||
use solitaire_core::{KlondikePile, Tableau};
|
||||
use solitaire_core::{DrawMode, game_state::GameMode};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -598,7 +598,7 @@ mod tests {
|
||||
fn sample_replay_three_moves() -> Replay {
|
||||
Replay::new(
|
||||
12345,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
GameMode::Classic,
|
||||
60,
|
||||
500,
|
||||
@@ -771,7 +771,7 @@ mod tests {
|
||||
let mut app = headless_app();
|
||||
let one_move = Replay::new(
|
||||
42,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
GameMode::Classic,
|
||||
10,
|
||||
100,
|
||||
@@ -880,7 +880,7 @@ mod tests {
|
||||
fn ten_draws_replay() -> Replay {
|
||||
Replay::new(
|
||||
7,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
GameMode::Classic,
|
||||
10,
|
||||
100,
|
||||
|
||||
@@ -935,7 +935,7 @@ mod tests {
|
||||
|
||||
use bevy::ecs::message::Messages;
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
/// Build a minimal app with `SelectionPlugin` only — no GamePlugin, no
|
||||
/// AssetServer. The `MoveRequestEvent` / `StateChangedEvent` /
|
||||
@@ -968,7 +968,7 @@ mod tests {
|
||||
/// Ace first). It cannot go to an empty tableau (only Kings).
|
||||
/// Empty tableaus T3..T6 only accept Kings, so they are filtered out.
|
||||
fn deterministic_state() -> GameState {
|
||||
let mut g = GameState::new(0, DrawMode::DrawOne);
|
||||
let mut g = GameState::new(0, DrawStockConfig::DrawOne);
|
||||
// Clear stock, waste, all tableaus.
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
|
||||
@@ -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::DrawMode;
|
||||
use solitaire_core::DrawStockConfig;
|
||||
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,
|
||||
@@ -1086,8 +1086,8 @@ fn handle_settings_buttons(
|
||||
}
|
||||
SettingsButton::ToggleDrawMode => {
|
||||
settings.0.draw_mode = match settings.0.draw_mode {
|
||||
DrawMode::DrawOne => DrawMode::DrawThree,
|
||||
DrawMode::DrawThree => DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne => DrawStockConfig::DrawThree,
|
||||
DrawStockConfig::DrawThree => DrawStockConfig::DrawOne,
|
||||
};
|
||||
persist(&path, &settings.0);
|
||||
changed.write(SettingsChangedEvent(settings.0.clone()));
|
||||
@@ -1310,10 +1310,10 @@ fn handle_sync_buttons(
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_mode_label(mode: &DrawMode) -> String {
|
||||
fn draw_mode_label(mode: &DrawStockConfig) -> String {
|
||||
match mode {
|
||||
DrawMode::DrawOne => "Draw 1".into(),
|
||||
DrawMode::DrawThree => "Draw 3".into(),
|
||||
DrawStockConfig::DrawOne => "Draw 1".into(),
|
||||
DrawStockConfig::DrawThree => "Draw 3".into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1327,7 +1327,7 @@ mod tests {
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::resources::GameStateResource>()
|
||||
.0
|
||||
.set_test_draw_mode(solitaire_core::DrawMode::DrawThree);
|
||||
.set_test_draw_mode(solitaire_core::DrawStockConfig::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::DrawMode::DrawOne,
|
||||
solitaire_core::DrawStockConfig::DrawOne,
|
||||
solitaire_core::game_state::GameMode::Classic,
|
||||
time_seconds,
|
||||
0,
|
||||
|
||||
@@ -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::{DrawMode, game_state::GameMode};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||
use solitaire_data::{
|
||||
Replay, ReplayHistory, load_replay_history_from, save_replay_history_to,
|
||||
};
|
||||
@@ -617,7 +617,7 @@ mod tests {
|
||||
// share_url — the upload-poll path must populate it.
|
||||
let initial = Replay::new(
|
||||
42,
|
||||
DrawMode::DrawOne,
|
||||
DrawStockConfig::DrawOne,
|
||||
GameMode::Classic,
|
||||
60,
|
||||
500,
|
||||
|
||||
@@ -299,7 +299,7 @@ mod tests {
|
||||
use crate::game_plugin::GamePlugin;
|
||||
use crate::progress_plugin::ProgressPlugin;
|
||||
use crate::table_plugin::TablePlugin;
|
||||
use solitaire_core::{DrawMode, game_state::GameState};
|
||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||
|
||||
fn headless_app() -> App {
|
||||
let mut app = App::new();
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
};
|
||||
// The current game must be in TimeAttack mode for auto-deal to fire.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack);
|
||||
GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::TimeAttack);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
@@ -454,7 +454,7 @@ mod tests {
|
||||
let mut app = headless_app();
|
||||
// Default session is inactive. Game is TimeAttack mode — still no count.
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack);
|
||||
GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::TimeAttack);
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
score: 500,
|
||||
|
||||
@@ -1205,7 +1205,7 @@ mod tests {
|
||||
.insert_resource(StatsResource(StatsSnapshot::default()))
|
||||
.insert_resource(GameStateResource(GameState::new(
|
||||
0,
|
||||
solitaire_core::DrawMode::DrawOne,
|
||||
solitaire_core::DrawStockConfig::DrawOne,
|
||||
)))
|
||||
.insert_resource(ProgressResource(PlayerProgress::default()));
|
||||
app.update();
|
||||
@@ -1534,9 +1534,9 @@ mod tests {
|
||||
.challenge_index = 4;
|
||||
// Switch game mode to Challenge.
|
||||
{
|
||||
use solitaire_core::DrawMode;
|
||||
use solitaire_core::DrawStockConfig;
|
||||
app.world_mut().resource_mut::<GameStateResource>().0 =
|
||||
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
|
||||
GameState::new_with_mode(1, DrawStockConfig::DrawOne, GameMode::Challenge);
|
||||
}
|
||||
|
||||
app.world_mut().write_message(GameWonEvent {
|
||||
@@ -1580,13 +1580,13 @@ mod tests {
|
||||
/// mode-multiplier rows.
|
||||
#[test]
|
||||
fn cache_win_data_captures_undo_count_and_mode() {
|
||||
use solitaire_core::DrawMode;
|
||||
use solitaire_core::DrawStockConfig;
|
||||
|
||||
let mut app = make_app();
|
||||
// Set up a Zen-mode game with 2 undos used.
|
||||
{
|
||||
let mut game = app.world_mut().resource_mut::<GameStateResource>();
|
||||
game.0 = GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::Zen);
|
||||
game.0 = GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::Zen);
|
||||
game.0.force_test_undos(2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user