refactor(core): derive draw_mode/is_won/move_count/is_auto_completable from session
Remove the draw_mode, move_count, is_won, and is_auto_completable fields from GameState; they are now &self methods deriving from the underlying card_game session (draw_mode from session config, move_count from history length, is_won/is_auto_completable from check_win/check_auto_complete). Tests previously fabricated these via direct field writes, which is no longer possible. Add gated test-support overrides on TestPileState (won/auto_completable/move_count) plus setters set_test_won, set_test_auto_completable, set_test_move_count, and set_test_draw_mode (re-deals the seed). All compiled out in production builds. Fix the field->method ripple across solitaire_data, solitaire_wasm, and solitaire_engine. Add a test-support dev-dependency to solitaire_data for the won-game storage test. cargo test --workspace and cargo clippy --workspace -- -D warnings pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -154,7 +154,7 @@ impl Plugin for GamePlugin {
|
||||
let saved = path.as_deref().and_then(load_game_state_from);
|
||||
let prompt_worthy = saved
|
||||
.as_ref()
|
||||
.is_some_and(|g| g.move_count > 0 && !g.is_won);
|
||||
.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),
|
||||
@@ -302,7 +302,7 @@ fn tick_elapsed_time(
|
||||
*skip_next_delta = false;
|
||||
return;
|
||||
}
|
||||
let is_won = game.0.is_won;
|
||||
let is_won = game.0.is_won();
|
||||
advance_elapsed(
|
||||
&mut game.0.elapsed_seconds,
|
||||
&mut accumulator,
|
||||
@@ -424,7 +424,7 @@ fn handle_new_game(
|
||||
for ev in new_game.read() {
|
||||
// If an active game is in progress, intercept and show a confirm dialog.
|
||||
// A game is "active" when moves have been made and it is not yet won.
|
||||
let needs_confirm = game.0.move_count > 0 && !game.0.is_won;
|
||||
let needs_confirm = game.0.move_count() > 0 && !game.0.is_won();
|
||||
// Skip confirmation if a ConfirmNewGameScreen already exists (prevents
|
||||
// duplicates) or if the event itself was already confirmed by the
|
||||
// player pressing Y on the modal — without the `confirmed` check the
|
||||
@@ -464,7 +464,7 @@ fn handle_new_game(
|
||||
// where SettingsPlugin is not installed.
|
||||
let draw_mode = settings
|
||||
.as_ref()
|
||||
.map_or_else(|| game.0.draw_mode, |s| s.0.draw_mode);
|
||||
.map_or_else(|| game.0.draw_mode(), |s| s.0.draw_mode);
|
||||
let mode = ev.mode.unwrap_or(game.0.mode);
|
||||
|
||||
// Solver-backed retry: when the player has opted in to
|
||||
@@ -823,7 +823,7 @@ fn handle_draw(
|
||||
if stock.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let draw_count = match game.0.draw_mode {
|
||||
let draw_count = match game.0.draw_mode() {
|
||||
DrawMode::DrawOne => 1_usize,
|
||||
DrawMode::DrawThree => 3_usize,
|
||||
};
|
||||
@@ -865,7 +865,7 @@ fn handle_move(
|
||||
path: Option<Res<GameStatePath>>,
|
||||
) {
|
||||
for ev in moves.read() {
|
||||
let was_won = game.0.is_won;
|
||||
let was_won = game.0.is_won();
|
||||
// Identify the card that will be exposed (and may flip face-up) by the move.
|
||||
// It's the card just below the bottom of the moving stack in the source pile.
|
||||
let source_cards = pile_cards(&game.0, &ev.from);
|
||||
@@ -910,7 +910,7 @@ fn handle_move(
|
||||
foundation_done.write(FoundationCompletedEvent { slot, suit });
|
||||
}
|
||||
changed.write(StateChangedEvent);
|
||||
if !was_won && game.0.is_won {
|
||||
if !was_won && game.0.is_won() {
|
||||
won.write(GameWonEvent {
|
||||
score: game.0.score,
|
||||
time_seconds: game.0.elapsed_seconds,
|
||||
@@ -986,7 +986,7 @@ pub fn record_replay_on_win(
|
||||
let win_move_index = recording.moves.len().checked_sub(1);
|
||||
let replay = Replay::new(
|
||||
game.0.seed,
|
||||
game.0.draw_mode,
|
||||
game.0.draw_mode(),
|
||||
game.0.mode,
|
||||
ev.time_seconds,
|
||||
ev.score,
|
||||
@@ -1093,13 +1093,13 @@ fn check_no_moves(
|
||||
|
||||
// Despawn game-over overlay whenever moves become available again or game is won.
|
||||
let moves_ok = has_legal_moves(&game.0);
|
||||
if moves_ok || game.0.is_won {
|
||||
if moves_ok || game.0.is_won() {
|
||||
for entity in &game_over_screens {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
if game.0.is_won {
|
||||
if game.0.is_won() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1248,7 +1248,7 @@ fn auto_save_game_state(
|
||||
// or there's a pending restore the player hasn't answered — saving
|
||||
// the fresh-deal placeholder we seeded GameStateResource with at
|
||||
// startup would clobber the real saved game on disk.
|
||||
if paused.is_some_and(|p| p.0) || game.0.is_won || game.0.move_count == 0 || pending.0.is_some()
|
||||
if paused.is_some_and(|p| p.0) || game.0.is_won() || game.0.move_count() == 0 || pending.0.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1596,7 +1596,7 @@ mod tests {
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.move_count = 1;
|
||||
.set_test_move_count(1);
|
||||
|
||||
// Re-arm the timer past the threshold every frame and pump
|
||||
// updates until the save fires. Caps at 16 iterations — a
|
||||
@@ -1845,7 +1845,7 @@ mod tests {
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.move_count = 5;
|
||||
.set_test_move_count(5);
|
||||
app.world_mut().write_message(NewGameRequestEvent {
|
||||
seed: None,
|
||||
mode: None,
|
||||
@@ -1869,7 +1869,7 @@ mod tests {
|
||||
let mut app = test_app_with_input(42);
|
||||
// move_count stays at 0 (fresh game).
|
||||
assert_eq!(
|
||||
app.world().resource::<GameStateResource>().0.move_count,
|
||||
app.world().resource::<GameStateResource>().0.move_count(),
|
||||
0,
|
||||
"test assumes a fresh game with no moves"
|
||||
);
|
||||
@@ -2362,7 +2362,7 @@ mod tests {
|
||||
app.update();
|
||||
|
||||
// Game state was reseeded — move_count is 0 on the new game.
|
||||
assert_eq!(app.world().resource::<GameStateResource>().0.move_count, 0);
|
||||
assert_eq!(app.world().resource::<GameStateResource>().0.move_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2436,7 +2436,7 @@ mod tests {
|
||||
// The chosen seed is non-deterministic (system time),
|
||||
// but the new game must have been started cleanly:
|
||||
// move_count back to 0, undo stack empty.
|
||||
assert_eq!(app.world().resource::<GameStateResource>().0.move_count, 0);
|
||||
assert_eq!(app.world().resource::<GameStateResource>().0.move_count(), 0);
|
||||
assert_eq!(
|
||||
app.world()
|
||||
.resource::<GameStateResource>()
|
||||
@@ -2496,7 +2496,7 @@ mod tests {
|
||||
);
|
||||
// New game completed: a fresh deal carries 0 moves.
|
||||
assert_eq!(
|
||||
app.world().resource::<GameStateResource>().0.move_count,
|
||||
app.world().resource::<GameStateResource>().0.move_count(),
|
||||
0,
|
||||
"completed new game must be in fresh-deal state",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user