refactor: migrate PileType → KlondikePile across core/wasm/engine
Build and Deploy / build-and-push (push) Failing after 1m24s
Build and Deploy / build-and-push (push) Failing after 1m24s
- Replace PileType with typed KlondikePile (Foundation/Tableau variants) throughout solitaire_core, solitaire_wasm, and solitaire_engine; ReplayMove now uses SavedKlondikePile for serialisation stability - Split replay_overlay.rs into replay_overlay/ module (mod, format, input, update, tests) for maintainability - Add klondike dep to solitaire_engine and solitaire_data Cargo.toml - Add TestPileState infrastructure to game_state.rs for engine unit tests - Rebuild solitaire_wasm pkg (js + wasm artefacts updated) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+258
-191
@@ -14,7 +14,7 @@ use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||
use bevy::window::AppLifecycle;
|
||||
use chrono::Utc;
|
||||
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
|
||||
use solitaire_core::pile::PileType;
|
||||
use klondike::KlondikePile;
|
||||
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
|
||||
#[allow(deprecated)]
|
||||
use solitaire_data::latest_replay_path;
|
||||
@@ -526,7 +526,7 @@ fn handle_new_game(
|
||||
&& let Some(stock) = layout
|
||||
.0
|
||||
.pile_positions
|
||||
.get(&solitaire_core::pile::PileType::Stock)
|
||||
.get(&klondike::KlondikePile::Stock)
|
||||
{
|
||||
for mut tx in &mut card_transforms {
|
||||
tx.translation.x = stock.x;
|
||||
@@ -824,19 +824,17 @@ fn handle_draw(
|
||||
// Only relevant when stock is non-empty; a recycle moves waste back to
|
||||
// stock face-down, so no flip events are needed in that case.
|
||||
let drawn_ids: Vec<u32> = {
|
||||
let stock = game.0.piles.get(&PileType::Stock);
|
||||
match stock {
|
||||
Some(p) if !p.cards.is_empty() => {
|
||||
let draw_count = match game.0.draw_mode {
|
||||
DrawMode::DrawOne => 1_usize,
|
||||
DrawMode::DrawThree => 3_usize,
|
||||
};
|
||||
let n = p.cards.len();
|
||||
let take = n.min(draw_count);
|
||||
// The top `take` cards (at the end of the vec) will be drawn.
|
||||
p.cards[n - take..].iter().map(|c| c.id).collect()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
let stock = game.0.stock_cards();
|
||||
if stock.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let draw_count = match game.0.draw_mode {
|
||||
DrawMode::DrawOne => 1_usize,
|
||||
DrawMode::DrawThree => 3_usize,
|
||||
};
|
||||
let n = stock.len();
|
||||
let take = n.min(draw_count);
|
||||
stock[n - take..].iter().map(|c| c.id).collect()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -875,32 +873,30 @@ fn handle_move(
|
||||
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 flip_candidate_id = game.0.piles.get(&ev.from).and_then(|p| {
|
||||
let n = p.cards.len();
|
||||
let source_cards = pile_cards(&game.0, &ev.from);
|
||||
let flip_candidate_id = {
|
||||
let n = source_cards.len();
|
||||
if n > ev.count {
|
||||
let c = &p.cards[n - ev.count - 1];
|
||||
let c = &source_cards[n - ev.count - 1];
|
||||
if !c.face_up { Some(c.id) } else { None }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
match game.0.move_cards(ev.from.clone(), ev.to.clone(), ev.count) {
|
||||
};
|
||||
match game.0.move_cards(ev.from, ev.to, ev.count) {
|
||||
Ok(()) => {
|
||||
// Record the move in the in-flight replay buffer. Done
|
||||
// first so the entry is captured even if a subsequent
|
||||
// event-write or pile-lookup happens to bail out below.
|
||||
recording.moves.push(ReplayMove::Move {
|
||||
from: ev.from.clone(),
|
||||
to: ev.to.clone(),
|
||||
from: ev.from.into(),
|
||||
to: ev.to.into(),
|
||||
count: ev.count,
|
||||
});
|
||||
// Fire flip event if the candidate card is now face-up.
|
||||
if let Some(fid) = flip_candidate_id
|
||||
&& game
|
||||
.0
|
||||
.piles
|
||||
.get(&ev.from)
|
||||
.and_then(|p| p.cards.last())
|
||||
&& pile_cards(&game.0, &ev.from)
|
||||
.last()
|
||||
.is_some_and(|c| c.id == fid && c.face_up)
|
||||
{
|
||||
flipped.write(crate::events::CardFlippedEvent(fid));
|
||||
@@ -911,10 +907,10 @@ fn handle_move(
|
||||
// the King + a golden tint on the foundation marker plus a
|
||||
// short audio ping. Purely a UI / audio cue — does not
|
||||
// cross `solitaire_sync` and is not persisted.
|
||||
if let PileType::Foundation(slot) = ev.to
|
||||
&& let Some(pile) = game.0.piles.get(&ev.to)
|
||||
&& pile.cards.len() == 13
|
||||
&& let Some(suit) = pile.claimed_suit()
|
||||
if let KlondikePile::Foundation(slot) = ev.to
|
||||
&& let Some(slot) = foundation_slot(slot)
|
||||
&& game.0.pile(ev.to).len() == 13
|
||||
&& let Some(suit) = game.0.pile(ev.to).first().map(|c| c.suit)
|
||||
{
|
||||
foundation_done.write(FoundationCompletedEvent { slot, suit });
|
||||
}
|
||||
@@ -1016,6 +1012,22 @@ pub fn record_replay_on_win(
|
||||
}
|
||||
}
|
||||
|
||||
fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<solitaire_core::card::Card> {
|
||||
match pile {
|
||||
KlondikePile::Stock => game.waste_cards(),
|
||||
_ => game.pile(*pile),
|
||||
}
|
||||
}
|
||||
|
||||
fn foundation_slot(foundation: klondike::Foundation) -> Option<u8> {
|
||||
match foundation {
|
||||
klondike::Foundation::Foundation1 => Some(0),
|
||||
klondike::Foundation::Foundation2 => Some(1),
|
||||
klondike::Foundation::Foundation3 => Some(2),
|
||||
klondike::Foundation::Foundation4 => Some(3),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task #29 — No-moves detection
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1037,19 +1049,17 @@ pub fn record_replay_on_win(
|
||||
/// previous heuristic incorrectly did (Quat hit this with 4 cards
|
||||
/// remaining and the game just sat there).
|
||||
pub fn has_legal_moves(game: &GameState) -> bool {
|
||||
use solitaire_core::pile::PileType;
|
||||
|
||||
|
||||
// Drawing from a non-empty stock, and recycling a non-empty waste back to
|
||||
// stock, are always legal moves in standard Klondike (unlimited recycles).
|
||||
// A game can only be genuinely stuck when both stock AND waste are exhausted.
|
||||
let stock_empty = game
|
||||
.piles
|
||||
.get(&PileType::Stock)
|
||||
.is_none_or(|p| p.cards.is_empty());
|
||||
.stock_cards()
|
||||
.is_empty();
|
||||
let waste_empty = game
|
||||
.piles
|
||||
.get(&PileType::Waste)
|
||||
.is_none_or(|p| p.cards.is_empty());
|
||||
.waste_cards()
|
||||
.is_empty();
|
||||
if !stock_empty || !waste_empty {
|
||||
return true;
|
||||
}
|
||||
@@ -1287,7 +1297,8 @@ fn save_game_state_on_exit(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solitaire_core::pile::PileType;
|
||||
use klondike::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::klondike_adapter::{SavedKlondikePile, SavedTableau};
|
||||
|
||||
/// Build a minimal headless `App` with just `GamePlugin` installed.
|
||||
/// Disables persistence and overrides the seed so tests are deterministic
|
||||
@@ -1326,18 +1337,27 @@ mod tests {
|
||||
#[test]
|
||||
fn draw_request_advances_game_state() {
|
||||
let mut app = test_app(42);
|
||||
let stock_before = app.world().resource::<GameStateResource>().0.piles[&PileType::Stock]
|
||||
.cards
|
||||
let stock_before = app
|
||||
.world()
|
||||
.resource::<GameStateResource>()
|
||||
.0
|
||||
.stock_cards()
|
||||
.len();
|
||||
|
||||
app.world_mut().write_message(DrawRequestEvent);
|
||||
app.update();
|
||||
|
||||
let stock_after = app.world().resource::<GameStateResource>().0.piles[&PileType::Stock]
|
||||
.cards
|
||||
let stock_after = app
|
||||
.world()
|
||||
.resource::<GameStateResource>()
|
||||
.0
|
||||
.stock_cards()
|
||||
.len();
|
||||
let waste_after = app.world().resource::<GameStateResource>().0.piles[&PileType::Waste]
|
||||
.cards
|
||||
let waste_after = app
|
||||
.world()
|
||||
.resource::<GameStateResource>()
|
||||
.0
|
||||
.waste_cards()
|
||||
.len();
|
||||
assert_eq!(stock_after, stock_before - 1);
|
||||
assert_eq!(waste_after, 1);
|
||||
@@ -1361,16 +1381,16 @@ mod tests {
|
||||
app.world_mut().write_message(UndoRequestEvent);
|
||||
app.update();
|
||||
let g = &app.world().resource::<GameStateResource>().0;
|
||||
assert_eq!(g.piles[&PileType::Stock].cards.len(), 24);
|
||||
assert_eq!(g.piles[&PileType::Waste].cards.len(), 0);
|
||||
assert_eq!(g.stock_cards().len(), 24);
|
||||
assert_eq!(g.waste_cards().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_game_request_reseeds() {
|
||||
let mut app = test_app(1);
|
||||
let before: Vec<u32> = app.world().resource::<GameStateResource>().0.piles
|
||||
[&PileType::Tableau(0)]
|
||||
.cards
|
||||
let before: Vec<u32> = app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(
|
||||
Tableau::Tableau1,
|
||||
))
|
||||
.iter()
|
||||
.map(|c| c.id)
|
||||
.collect();
|
||||
@@ -1382,15 +1402,43 @@ mod tests {
|
||||
});
|
||||
app.update();
|
||||
|
||||
let after: Vec<u32> = app.world().resource::<GameStateResource>().0.piles
|
||||
[&PileType::Tableau(0)]
|
||||
.cards
|
||||
let after: Vec<u32> = app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(
|
||||
Tableau::Tableau1,
|
||||
))
|
||||
.iter()
|
||||
.map(|c| c.id)
|
||||
.collect();
|
||||
assert_ne!(before, after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_changed_updates_take_from_foundation_flag() {
|
||||
let mut app = test_app(1);
|
||||
assert!(
|
||||
app.world().resource::<GameStateResource>().0.take_from_foundation,
|
||||
"fresh game should inherit default take_from_foundation=true",
|
||||
);
|
||||
|
||||
let mut settings = solitaire_data::Settings::default();
|
||||
settings.take_from_foundation = false;
|
||||
app.world_mut()
|
||||
.write_message(crate::settings_plugin::SettingsChangedEvent(settings.clone()));
|
||||
app.update();
|
||||
assert!(
|
||||
!app.world().resource::<GameStateResource>().0.take_from_foundation,
|
||||
"settings event must forward take_from_foundation=false into live game state",
|
||||
);
|
||||
|
||||
settings.take_from_foundation = true;
|
||||
app.world_mut()
|
||||
.write_message(crate::settings_plugin::SettingsChangedEvent(settings));
|
||||
app.update();
|
||||
assert!(
|
||||
app.world().resource::<GameStateResource>().0.take_from_foundation,
|
||||
"settings event must forward take_from_foundation=true into live game state",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_elapsed_drains_accumulator_into_whole_seconds() {
|
||||
let mut elapsed = 0;
|
||||
@@ -1440,8 +1488,8 @@ mod tests {
|
||||
let mut app = test_app(42);
|
||||
// Stock -> Waste is InvalidDestination; no state change expected.
|
||||
app.world_mut().write_message(MoveRequestEvent {
|
||||
from: PileType::Stock,
|
||||
to: PileType::Waste,
|
||||
from: KlondikePile::Stock,
|
||||
to: KlondikePile::Stock,
|
||||
count: 1,
|
||||
});
|
||||
app.update();
|
||||
@@ -1581,46 +1629,34 @@ mod tests {
|
||||
// Build a tableau with two face-up cards.
|
||||
{
|
||||
let mut gs = app.world_mut().resource_mut::<GameStateResource>();
|
||||
let t = gs.0.piles.get_mut(&PileType::Tableau(0)).unwrap();
|
||||
t.cards.clear();
|
||||
t.cards.push(Card {
|
||||
id: 910,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::King,
|
||||
face_up: true,
|
||||
});
|
||||
t.cards.push(Card {
|
||||
id: 911,
|
||||
suit: Suit::Hearts,
|
||||
rank: Rank::Queen,
|
||||
face_up: true,
|
||||
});
|
||||
}
|
||||
app.world_mut()
|
||||
.resource_mut::<GameStateResource>()
|
||||
.0
|
||||
.piles
|
||||
.get_mut(&PileType::Tableau(1))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
{
|
||||
let mut gs = app.world_mut().resource_mut::<GameStateResource>();
|
||||
gs.0.piles
|
||||
.get_mut(&PileType::Tableau(1))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(Card {
|
||||
gs.0.set_test_tableau_cards(Tableau::Tableau1, vec![
|
||||
Card {
|
||||
id: 910,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::King,
|
||||
face_up: true,
|
||||
},
|
||||
Card {
|
||||
id: 911,
|
||||
suit: Suit::Hearts,
|
||||
rank: Rank::Queen,
|
||||
face_up: true,
|
||||
},
|
||||
]);
|
||||
gs.0.set_test_tableau_cards(
|
||||
Tableau::Tableau2,
|
||||
vec![Card {
|
||||
id: 912,
|
||||
suit: Suit::Spades,
|
||||
rank: Rank::King,
|
||||
face_up: true,
|
||||
});
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
app.world_mut().write_message(MoveRequestEvent {
|
||||
from: PileType::Tableau(0),
|
||||
to: PileType::Tableau(1),
|
||||
from: KlondikePile::Tableau(Tableau::Tableau1),
|
||||
to: KlondikePile::Tableau(Tableau::Tableau2),
|
||||
count: 1,
|
||||
});
|
||||
app.update();
|
||||
@@ -1659,31 +1695,36 @@ mod tests {
|
||||
// are exhausted and no visible card can be moved.
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
for slot in 0..4_u8 {
|
||||
game.piles
|
||||
.get_mut(&PileType::Foundation(slot))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
game.set_test_foundation_cards(foundation, Vec::new());
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
game.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
game.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
game.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
let stock = game.piles.get_mut(&PileType::Stock).unwrap();
|
||||
stock.cards.clear();
|
||||
game.set_test_waste_cards(Vec::new());
|
||||
let mut stock = Vec::new();
|
||||
for r in [Rank::Two, Rank::Three, Rank::Four, Rank::Five] {
|
||||
stock.cards.push(Card {
|
||||
stock.push(Card {
|
||||
id: 100 + r as u32,
|
||||
suit: Suit::Hearts,
|
||||
rank: r,
|
||||
face_up: false,
|
||||
});
|
||||
}
|
||||
game.set_test_stock_cards(stock);
|
||||
// Stock is non-empty, so drawing is always a valid move.
|
||||
assert!(
|
||||
has_legal_moves(&game),
|
||||
@@ -1697,34 +1738,38 @@ mod tests {
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
|
||||
// Empty stock and waste so draw is NOT available.
|
||||
game.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
game.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
game.set_test_stock_cards(Vec::new());
|
||||
game.set_test_waste_cards(Vec::new());
|
||||
|
||||
// Clear all tableau and foundations, put Ace of Clubs on tableau 0.
|
||||
for slot in 0..4_u8 {
|
||||
game.piles
|
||||
.get_mut(&PileType::Foundation(slot))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
game.set_test_foundation_cards(foundation, Vec::new());
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
game.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
game.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
game.piles
|
||||
.get_mut(&PileType::Tableau(0))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(Card {
|
||||
game.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![Card {
|
||||
id: 1,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Ace,
|
||||
face_up: true,
|
||||
});
|
||||
}],
|
||||
);
|
||||
|
||||
assert!(
|
||||
has_legal_moves(&game),
|
||||
@@ -1741,47 +1786,57 @@ mod tests {
|
||||
use solitaire_core::card::{Card, Rank, Suit};
|
||||
let mut game = GameState::new(1, DrawMode::DrawOne);
|
||||
|
||||
game.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
game.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
for slot in 0..4_u8 {
|
||||
game.piles
|
||||
.get_mut(&PileType::Foundation(slot))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
game.set_test_stock_cards(Vec::new());
|
||||
game.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
game.set_test_foundation_cards(foundation, Vec::new());
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
game.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
game.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
|
||||
// Tableau 0: face-up Queen of Spades (non-top) + face-up Jack of Hearts on top.
|
||||
// King of Diamonds is on Tableau 1 (empty otherwise), so Queen→King is the
|
||||
// only legal tableau move, and that move targets the Queen which is non-top.
|
||||
let t0 = game.piles.get_mut(&PileType::Tableau(0)).unwrap();
|
||||
t0.cards.push(Card {
|
||||
id: 10,
|
||||
suit: Suit::Spades,
|
||||
rank: Rank::Queen,
|
||||
face_up: true,
|
||||
});
|
||||
t0.cards.push(Card {
|
||||
id: 11,
|
||||
suit: Suit::Hearts,
|
||||
rank: Rank::Jack,
|
||||
face_up: true,
|
||||
});
|
||||
|
||||
let t1 = game.piles.get_mut(&PileType::Tableau(1)).unwrap();
|
||||
t1.cards.push(Card {
|
||||
id: 12,
|
||||
suit: Suit::Diamonds,
|
||||
rank: Rank::King,
|
||||
face_up: true,
|
||||
});
|
||||
game.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![
|
||||
Card {
|
||||
id: 10,
|
||||
suit: Suit::Spades,
|
||||
rank: Rank::Queen,
|
||||
face_up: true,
|
||||
},
|
||||
Card {
|
||||
id: 11,
|
||||
suit: Suit::Hearts,
|
||||
rank: Rank::Jack,
|
||||
face_up: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
game.set_test_tableau_cards(
|
||||
Tableau::Tableau2,
|
||||
vec![Card {
|
||||
id: 12,
|
||||
suit: Suit::Diamonds,
|
||||
rank: Rank::King,
|
||||
face_up: true,
|
||||
}],
|
||||
);
|
||||
|
||||
assert!(
|
||||
has_legal_moves(&game),
|
||||
@@ -1942,37 +1997,41 @@ mod tests {
|
||||
// there legally.
|
||||
{
|
||||
let mut gs = app.world_mut().resource_mut::<GameStateResource>();
|
||||
gs.0.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
gs.0.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
for slot in 0..4_u8 {
|
||||
gs.0.piles
|
||||
.get_mut(&PileType::Foundation(slot))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
gs.0.set_test_stock_cards(Vec::new());
|
||||
gs.0.set_test_waste_cards(Vec::new());
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
gs.0.set_test_foundation_cards(foundation, Vec::new());
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
gs.0.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
gs.0.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
gs.0.piles
|
||||
.get_mut(&PileType::Tableau(0))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(Card {
|
||||
gs.0.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![Card {
|
||||
id: 7_000,
|
||||
suit: Suit::Spades,
|
||||
rank: Rank::King,
|
||||
face_up: true,
|
||||
});
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
app.world_mut().write_message(MoveRequestEvent {
|
||||
from: PileType::Tableau(0),
|
||||
to: PileType::Tableau(1),
|
||||
from: KlondikePile::Tableau(Tableau::Tableau1),
|
||||
to: KlondikePile::Tableau(Tableau::Tableau2),
|
||||
count: 1,
|
||||
});
|
||||
app.update();
|
||||
@@ -2029,8 +2088,8 @@ mod tests {
|
||||
let mut app = test_app(42);
|
||||
// Stock → Waste is InvalidDestination; the live engine rejects it.
|
||||
app.world_mut().write_message(MoveRequestEvent {
|
||||
from: PileType::Stock,
|
||||
to: PileType::Waste,
|
||||
from: KlondikePile::Stock,
|
||||
to: KlondikePile::Stock,
|
||||
count: 1,
|
||||
});
|
||||
app.update();
|
||||
@@ -2117,8 +2176,8 @@ mod tests {
|
||||
let mut recording = app.world_mut().resource_mut::<RecordingReplay>();
|
||||
recording.moves.push(ReplayMove::StockClick);
|
||||
recording.moves.push(ReplayMove::Move {
|
||||
from: PileType::Waste,
|
||||
to: PileType::Tableau(2),
|
||||
from: SavedKlondikePile::Stock,
|
||||
to: SavedKlondikePile::Tableau(SavedTableau(2)),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
@@ -2157,8 +2216,8 @@ mod tests {
|
||||
assert!(matches!(loaded.moves[0], ReplayMove::StockClick));
|
||||
match &loaded.moves[1] {
|
||||
ReplayMove::Move { from, to, count } => {
|
||||
assert_eq!(*from, PileType::Waste);
|
||||
assert_eq!(*to, PileType::Tableau(2));
|
||||
assert_eq!(*from, SavedKlondikePile::Stock);
|
||||
assert_eq!(*to, SavedKlondikePile::Tableau(SavedTableau(2)));
|
||||
assert_eq!(*count, 1);
|
||||
}
|
||||
other => panic!("second entry must be a Move, got {other:?}"),
|
||||
@@ -2280,11 +2339,19 @@ mod tests {
|
||||
);
|
||||
// Cross-check: the dealt tableau must match GameState::new(999) byte-for-byte.
|
||||
let expected = GameState::new(999, DrawMode::DrawOne);
|
||||
for i in 0..7 {
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
assert_eq!(
|
||||
app.world().resource::<GameStateResource>().0.piles[&PileType::Tableau(i)].cards,
|
||||
expected.piles[&PileType::Tableau(i)].cards,
|
||||
"tableau column {i} must match the unfiltered seed",
|
||||
app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(tableau)),
|
||||
expected.pile(KlondikePile::Tableau(tableau)),
|
||||
"tableau column {tableau:?} must match the unfiltered seed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user