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:
@@ -37,8 +37,9 @@
|
||||
|
||||
use bevy::input::ButtonInput;
|
||||
use bevy::prelude::*;
|
||||
use klondike::{Foundation, KlondikePile, Tableau};
|
||||
use solitaire_core::card::Card;
|
||||
use solitaire_core::game_state::GameState;
|
||||
use solitaire_core::pile::PileType;
|
||||
|
||||
use crate::card_plugin::CardEntity;
|
||||
use crate::events::{InfoToastEvent, MoveRequestEvent, StateChangedEvent};
|
||||
@@ -59,7 +60,7 @@ use crate::ui_theme::{ACCENT_PRIMARY, STATE_SUCCESS, STATE_WARNING};
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct SelectionState {
|
||||
/// The pile whose top face-up card is currently selected, or `None`.
|
||||
pub selected_pile: Option<PileType>,
|
||||
pub selected_pile: Option<KlondikePile>,
|
||||
}
|
||||
|
||||
/// Sentinel value used in [`crate::resources::DragState::active_touch_id`]
|
||||
@@ -86,7 +87,7 @@ pub enum KeyboardDragState {
|
||||
/// `legal_destinations` and `Enter` fires the move.
|
||||
Lifted {
|
||||
/// Pile the cards were lifted from.
|
||||
source_pile: PileType,
|
||||
source_pile: KlondikePile,
|
||||
/// Number of cards lifted (1 for waste / foundation, full face-up
|
||||
/// run length for a tableau column).
|
||||
count: usize,
|
||||
@@ -97,7 +98,7 @@ pub enum KeyboardDragState {
|
||||
/// placed on. Always at least one entry while in this variant —
|
||||
/// if no legal destinations exist the state machine refuses to
|
||||
/// enter `Lifted` in the first place.
|
||||
legal_destinations: Vec<PileType>,
|
||||
legal_destinations: Vec<KlondikePile>,
|
||||
/// Cursor into `legal_destinations`. Always `< legal_destinations.len()`.
|
||||
destination_index: usize,
|
||||
},
|
||||
@@ -109,7 +110,7 @@ impl KeyboardDragState {
|
||||
///
|
||||
/// [`Lifted`]: KeyboardDragState::Lifted
|
||||
/// [`Idle`]: KeyboardDragState::Idle
|
||||
pub fn focused_destination(&self) -> Option<&PileType> {
|
||||
pub fn focused_destination(&self) -> Option<&KlondikePile> {
|
||||
match self {
|
||||
Self::Idle => None,
|
||||
Self::Lifted {
|
||||
@@ -172,13 +173,26 @@ impl Plugin for SelectionPlugin {
|
||||
/// The ordered list of piles that are considered for keyboard cycling.
|
||||
///
|
||||
/// Order: Waste → Foundation slots 0–3 → Tableau 0–6.
|
||||
fn cycled_piles() -> Vec<PileType> {
|
||||
let mut piles = vec![PileType::Waste];
|
||||
for slot in 0..4_u8 {
|
||||
piles.push(PileType::Foundation(slot));
|
||||
fn cycled_piles() -> Vec<KlondikePile> {
|
||||
let mut piles = vec![KlondikePile::Stock];
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
piles.push(KlondikePile::Foundation(foundation));
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
piles.push(PileType::Tableau(i));
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
piles.push(KlondikePile::Tableau(tableau));
|
||||
}
|
||||
piles
|
||||
}
|
||||
@@ -188,7 +202,7 @@ fn cycled_piles() -> Vec<PileType> {
|
||||
///
|
||||
/// If `current` is `None` the first available pile is returned.
|
||||
/// If `available` is empty, `None` is returned.
|
||||
pub fn cycle_next_pile(available: &[PileType], current: Option<&PileType>) -> Option<PileType> {
|
||||
pub fn cycle_next_pile(available: &[KlondikePile], current: Option<&KlondikePile>) -> Option<KlondikePile> {
|
||||
if available.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -209,7 +223,7 @@ pub fn cycle_next_pile(available: &[PileType], current: Option<&PileType>) -> Op
|
||||
for offset in 0..n {
|
||||
let candidate = &order[(start + offset) % n];
|
||||
if available.contains(candidate) {
|
||||
return Some(candidate.clone());
|
||||
return Some(*candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -221,14 +235,14 @@ pub fn cycle_next_pile(available: &[PileType], current: Option<&PileType>) -> Op
|
||||
///
|
||||
/// Both `current` and `next` must be `Some`; if either is `None` this returns
|
||||
/// `false`.
|
||||
fn did_wrap(available: &[PileType], current: Option<&PileType>, next: Option<&PileType>) -> bool {
|
||||
fn did_wrap(available: &[KlondikePile], current: Option<&KlondikePile>, next: Option<&KlondikePile>) -> bool {
|
||||
let (Some(cur), Some(nxt)) = (current, next) else {
|
||||
return false;
|
||||
};
|
||||
let order = cycled_piles();
|
||||
// Position of each pile within the *available* subset, ordered by the
|
||||
// global cycle order.
|
||||
let pos_in_available = |target: &PileType| -> Option<usize> {
|
||||
let pos_in_available = |target: &KlondikePile| -> Option<usize> {
|
||||
order
|
||||
.iter()
|
||||
.filter(|p| available.contains(p))
|
||||
@@ -325,7 +339,7 @@ fn handle_selection_keys(
|
||||
if keys.just_pressed(KeyCode::Enter) {
|
||||
if let Some(dest) = legal_destinations.get(*destination_index).cloned() {
|
||||
moves.write(MoveRequestEvent {
|
||||
from: source_pile.clone(),
|
||||
from: *source_pile,
|
||||
to: dest,
|
||||
count: *count,
|
||||
});
|
||||
@@ -356,28 +370,24 @@ fn handle_selection_keys(
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Build the list of piles that currently have a face-up draggable top card.
|
||||
let available: Vec<PileType> = {
|
||||
let available: Vec<KlondikePile> = {
|
||||
let all = [
|
||||
PileType::Waste,
|
||||
PileType::Foundation(0),
|
||||
PileType::Foundation(1),
|
||||
PileType::Foundation(2),
|
||||
PileType::Foundation(3),
|
||||
PileType::Tableau(0),
|
||||
PileType::Tableau(1),
|
||||
PileType::Tableau(2),
|
||||
PileType::Tableau(3),
|
||||
PileType::Tableau(4),
|
||||
PileType::Tableau(5),
|
||||
PileType::Tableau(6),
|
||||
KlondikePile::Stock,
|
||||
KlondikePile::Foundation(Foundation::Foundation1),
|
||||
KlondikePile::Foundation(Foundation::Foundation2),
|
||||
KlondikePile::Foundation(Foundation::Foundation3),
|
||||
KlondikePile::Foundation(Foundation::Foundation4),
|
||||
KlondikePile::Tableau(Tableau::Tableau1),
|
||||
KlondikePile::Tableau(Tableau::Tableau2),
|
||||
KlondikePile::Tableau(Tableau::Tableau3),
|
||||
KlondikePile::Tableau(Tableau::Tableau4),
|
||||
KlondikePile::Tableau(Tableau::Tableau5),
|
||||
KlondikePile::Tableau(Tableau::Tableau6),
|
||||
KlondikePile::Tableau(Tableau::Tableau7),
|
||||
];
|
||||
all.into_iter()
|
||||
.filter(|p| {
|
||||
game.0
|
||||
.piles
|
||||
.get(p)
|
||||
.and_then(|pile| pile.cards.last())
|
||||
.is_some_and(|c| c.face_up)
|
||||
pile_cards(&game.0, p).last().is_some_and(|c| c.face_up)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
@@ -406,18 +416,16 @@ fn handle_selection_keys(
|
||||
// tableau stack target. Preserved so the muscle memory built around
|
||||
// `Tab` → `Space` keeps working; `Enter` is now the lift trigger.
|
||||
if keys.just_pressed(KeyCode::Space)
|
||||
&& let Some(ref pile) = selection.selected_pile.clone()
|
||||
&& let Some(card) = game
|
||||
.0
|
||||
.piles
|
||||
.get(pile)
|
||||
.and_then(|p| p.cards.last())
|
||||
.filter(|c| c.face_up)
|
||||
&& let Some(ref pile) = selection.selected_pile
|
||||
{
|
||||
let selected_cards = pile_cards(&game.0, pile);
|
||||
let Some(card) = selected_cards.last().filter(|c| c.face_up) else {
|
||||
return;
|
||||
};
|
||||
// Priority 1: foundation move (single card).
|
||||
if let Some(dest) = try_foundation_dest(card, &game.0) {
|
||||
moves.write(MoveRequestEvent {
|
||||
from: pile.clone(),
|
||||
from: *pile,
|
||||
to: dest,
|
||||
count: 1,
|
||||
});
|
||||
@@ -425,17 +433,16 @@ fn handle_selection_keys(
|
||||
return;
|
||||
}
|
||||
// Priority 2: tableau stack move.
|
||||
let run_len = face_up_run_len(game.0.piles.get(pile).map_or(&[], |p| p.cards.as_slice()));
|
||||
let bottom_card = game.0.piles.get(pile).and_then(|p| {
|
||||
let start = p.cards.len().saturating_sub(run_len);
|
||||
p.cards.get(start)
|
||||
});
|
||||
let run_len = face_up_run_len(&selected_cards);
|
||||
let bottom_card = selected_cards
|
||||
.get(selected_cards.len().saturating_sub(run_len))
|
||||
.cloned();
|
||||
if let Some(bottom) = bottom_card
|
||||
&& let Some((dest, count)) =
|
||||
best_tableau_destination_for_stack(bottom, pile, &game.0, run_len)
|
||||
best_tableau_destination_for_stack(&bottom, pile, &game.0, run_len)
|
||||
{
|
||||
moves.write(MoveRequestEvent {
|
||||
from: pile.clone(),
|
||||
from: *pile,
|
||||
to: dest,
|
||||
count,
|
||||
});
|
||||
@@ -445,7 +452,7 @@ fn handle_selection_keys(
|
||||
// Fallback for non-tableau sources.
|
||||
if let Some(dest) = best_destination(card, &game.0) {
|
||||
moves.write(MoveRequestEvent {
|
||||
from: pile.clone(),
|
||||
from: *pile,
|
||||
to: dest,
|
||||
count: 1,
|
||||
});
|
||||
@@ -456,25 +463,23 @@ fn handle_selection_keys(
|
||||
|
||||
// Enter — lift the focused pile into destination-pick mode.
|
||||
if keys.just_pressed(KeyCode::Enter)
|
||||
&& let Some(ref source) = selection.selected_pile.clone()
|
||||
&& let Some(ref source) = selection.selected_pile
|
||||
{
|
||||
let Some(pile_cards) = game.0.piles.get(source) else {
|
||||
let source_cards = pile_cards(&game.0, source);
|
||||
if source_cards.is_empty() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
// Determine the lift range: tableau lifts the full face-up run, all
|
||||
// other sources lift only the top card.
|
||||
let run_len = face_up_run_len(pile_cards.cards.as_slice());
|
||||
let count = if matches!(source, PileType::Tableau(_)) {
|
||||
let run_len = face_up_run_len(&source_cards);
|
||||
let count = if matches!(source, KlondikePile::Tableau(_)) {
|
||||
run_len.max(1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
if pile_cards.cards.is_empty() {
|
||||
return;
|
||||
}
|
||||
let start = pile_cards.cards.len().saturating_sub(count);
|
||||
let lifted_cards: Vec<u32> = pile_cards.cards[start..].iter().map(|c| c.id).collect();
|
||||
let Some(bottom) = pile_cards.cards.get(start) else {
|
||||
let start = source_cards.len().saturating_sub(count);
|
||||
let lifted_cards: Vec<u32> = source_cards[start..].iter().map(|c| c.id).collect();
|
||||
let Some(bottom) = source_cards.get(start) else {
|
||||
return;
|
||||
};
|
||||
let legal = legal_destinations_for(bottom, source, &game.0, count);
|
||||
@@ -486,7 +491,7 @@ fn handle_selection_keys(
|
||||
// Populate `DragState` with the keyboard sentinel so the existing
|
||||
// mouse-drag systems treat this as "not their drag".
|
||||
drag.cards = lifted_cards.clone();
|
||||
drag.origin_pile = Some(source.clone());
|
||||
drag.origin_pile = Some(*source);
|
||||
drag.cursor_offset = Vec2::ZERO;
|
||||
drag.origin_z = 1.0;
|
||||
drag.press_pos = Vec2::ZERO;
|
||||
@@ -494,7 +499,7 @@ fn handle_selection_keys(
|
||||
drag.active_touch_id = Some(KEYBOARD_DRAG_TOUCH_ID);
|
||||
|
||||
*kbd_drag = KeyboardDragState::Lifted {
|
||||
source_pile: source.clone(),
|
||||
source_pile: *source,
|
||||
count,
|
||||
cards: lifted_cards,
|
||||
legal_destinations: legal,
|
||||
@@ -520,21 +525,34 @@ fn handle_selection_keys(
|
||||
/// press the right-arrow key once or twice.
|
||||
pub(crate) fn legal_destinations_for(
|
||||
_bottom: &solitaire_core::card::Card,
|
||||
source: &PileType,
|
||||
source: &KlondikePile,
|
||||
game: &GameState,
|
||||
stack_count: usize,
|
||||
) -> Vec<PileType> {
|
||||
) -> Vec<KlondikePile> {
|
||||
let mut out = Vec::new();
|
||||
if stack_count == 1 {
|
||||
for slot in 0..4_u8 {
|
||||
let dest = PileType::Foundation(slot);
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
let dest = KlondikePile::Foundation(foundation);
|
||||
if game.can_move_cards(source, &dest, 1) {
|
||||
out.push(dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..7_usize {
|
||||
let dest = PileType::Tableau(i);
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
let dest = KlondikePile::Tableau(tableau);
|
||||
if game.can_move_cards(source, &dest, stack_count) {
|
||||
out.push(dest);
|
||||
}
|
||||
@@ -572,10 +590,15 @@ fn face_up_run_len(cards: &[solitaire_core::card::Card]) -> usize {
|
||||
fn try_foundation_dest(
|
||||
card: &solitaire_core::card::Card,
|
||||
game: &solitaire_core::game_state::GameState,
|
||||
) -> Option<PileType> {
|
||||
) -> Option<KlondikePile> {
|
||||
let source = game.pile_containing_card(card.id)?;
|
||||
for slot in 0..4_u8 {
|
||||
let dest = PileType::Foundation(slot);
|
||||
for foundation in [
|
||||
Foundation::Foundation1,
|
||||
Foundation::Foundation2,
|
||||
Foundation::Foundation3,
|
||||
Foundation::Foundation4,
|
||||
] {
|
||||
let dest = KlondikePile::Foundation(foundation);
|
||||
if game.can_move_cards(&source, &dest, 1) {
|
||||
return Some(dest);
|
||||
}
|
||||
@@ -656,9 +679,9 @@ fn update_selection_highlight(
|
||||
// Resolve the source pile from KeyboardDragState (when lifted) or
|
||||
// SelectionState (otherwise). Lifted takes precedence so the gold
|
||||
// outline follows the actual lifted cards.
|
||||
let source_pile: Option<PileType> = match &*kbd_drag {
|
||||
KeyboardDragState::Lifted { source_pile, .. } => Some(source_pile.clone()),
|
||||
KeyboardDragState::Idle => selection.selected_pile.clone(),
|
||||
let source_pile: Option<KlondikePile> = match &*kbd_drag {
|
||||
KeyboardDragState::Lifted { source_pile, .. } => Some(*source_pile),
|
||||
KeyboardDragState::Idle => selection.selected_pile,
|
||||
};
|
||||
|
||||
if let Some(ref pile) = source_pile
|
||||
@@ -694,14 +717,18 @@ fn update_selection_highlight(
|
||||
|
||||
/// Returns the top face-up card on `pile`, or `None` if the pile is
|
||||
/// empty or its top card is face-down.
|
||||
fn top_face_up_card<'a>(
|
||||
pile: &PileType,
|
||||
game: &'a GameState,
|
||||
) -> Option<&'a solitaire_core::card::Card> {
|
||||
game.piles
|
||||
.get(pile)
|
||||
.and_then(|p| p.cards.last())
|
||||
.filter(|c| c.face_up)
|
||||
fn top_face_up_card(
|
||||
pile: &KlondikePile,
|
||||
game: &GameState,
|
||||
) -> Option<Card> {
|
||||
pile_cards(game, pile).last().filter(|c| c.face_up).cloned()
|
||||
}
|
||||
|
||||
fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<Card> {
|
||||
match pile {
|
||||
KlondikePile::Stock => game.waste_cards(),
|
||||
_ => game.pile(*pile),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a `SelectionHighlight` sprite as a child of the entity carrying
|
||||
@@ -740,15 +767,15 @@ fn spawn_highlight_on_card(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn piles_from(names: &[&str]) -> Vec<PileType> {
|
||||
fn piles_from(names: &[&str]) -> Vec<KlondikePile> {
|
||||
names
|
||||
.iter()
|
||||
.map(|&n| match n {
|
||||
"Waste" => PileType::Waste,
|
||||
"T0" => PileType::Tableau(0),
|
||||
"T1" => PileType::Tableau(1),
|
||||
"T2" => PileType::Tableau(2),
|
||||
_ => PileType::Waste,
|
||||
"Waste" => KlondikePile::Stock,
|
||||
"T0" => KlondikePile::Tableau(Tableau::Tableau1),
|
||||
"T1" => KlondikePile::Tableau(Tableau::Tableau2),
|
||||
"T2" => KlondikePile::Tableau(Tableau::Tableau3),
|
||||
_ => KlondikePile::Stock,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -762,23 +789,23 @@ mod tests {
|
||||
// With [Waste, Tableau(0), Tableau(1)] available, starting from None → Waste.
|
||||
let available = piles_from(&["Waste", "T0", "T1"]);
|
||||
let result = cycle_next_pile(&available, None);
|
||||
assert_eq!(result, Some(PileType::Waste));
|
||||
assert_eq!(result, Some(KlondikePile::Stock));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_next_pile_from_waste() {
|
||||
// Starting from Waste → Tableau(0).
|
||||
let available = piles_from(&["Waste", "T0", "T1"]);
|
||||
let result = cycle_next_pile(&available, Some(&PileType::Waste));
|
||||
assert_eq!(result, Some(PileType::Tableau(0)));
|
||||
let result = cycle_next_pile(&available, Some(&KlondikePile::Stock));
|
||||
assert_eq!(result, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_next_pile_wraps() {
|
||||
// Starting from Tableau(1) → Waste (wraps back to start).
|
||||
let available = piles_from(&["Waste", "T0", "T1"]);
|
||||
let result = cycle_next_pile(&available, Some(&PileType::Tableau(1)));
|
||||
assert_eq!(result, Some(PileType::Waste));
|
||||
let result = cycle_next_pile(&available, Some(&KlondikePile::Tableau(Tableau::Tableau2)));
|
||||
assert_eq!(result, Some(KlondikePile::Stock));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -803,7 +830,7 @@ mod tests {
|
||||
|
||||
// Press 1: no current selection → first pile, no wrap.
|
||||
let sel1 = cycle_next_pile(&available, None);
|
||||
assert_eq!(sel1, Some(PileType::Waste));
|
||||
assert_eq!(sel1, Some(KlondikePile::Stock));
|
||||
assert!(
|
||||
!did_wrap(&available, None, sel1.as_ref()),
|
||||
"first Tab should not wrap"
|
||||
@@ -811,7 +838,7 @@ mod tests {
|
||||
|
||||
// Press 2: Waste → Tableau(0), no wrap.
|
||||
let sel2 = cycle_next_pile(&available, sel1.as_ref());
|
||||
assert_eq!(sel2, Some(PileType::Tableau(0)));
|
||||
assert_eq!(sel2, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
||||
assert!(
|
||||
!did_wrap(&available, sel1.as_ref(), sel2.as_ref()),
|
||||
"second Tab should not wrap"
|
||||
@@ -819,7 +846,7 @@ mod tests {
|
||||
|
||||
// Press 3: Tableau(0) → Tableau(1), still no wrap.
|
||||
let sel3 = cycle_next_pile(&available, sel2.as_ref());
|
||||
assert_eq!(sel3, Some(PileType::Tableau(1)));
|
||||
assert_eq!(sel3, Some(KlondikePile::Tableau(Tableau::Tableau2)));
|
||||
assert!(
|
||||
!did_wrap(&available, sel2.as_ref(), sel3.as_ref()),
|
||||
"third Tab (T0→T1) should not wrap"
|
||||
@@ -827,7 +854,7 @@ mod tests {
|
||||
|
||||
// Press 4: Tableau(1) → Waste, this IS the wrap.
|
||||
let sel4 = cycle_next_pile(&available, sel3.as_ref());
|
||||
assert_eq!(sel4, Some(PileType::Waste));
|
||||
assert_eq!(sel4, Some(KlondikePile::Stock));
|
||||
assert!(
|
||||
did_wrap(&available, sel3.as_ref(), sel4.as_ref()),
|
||||
"fourth Tab should wrap back to Waste"
|
||||
@@ -836,9 +863,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cycle_next_pile_single_element_wraps_to_itself() {
|
||||
let available = vec![PileType::Waste];
|
||||
let result = cycle_next_pile(&available, Some(&PileType::Waste));
|
||||
assert_eq!(result, Some(PileType::Waste));
|
||||
let available = vec![KlondikePile::Stock];
|
||||
let result = cycle_next_pile(&available, Some(&KlondikePile::Stock));
|
||||
assert_eq!(result, Some(KlondikePile::Stock));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -986,46 +1013,47 @@ mod tests {
|
||||
fn deterministic_state() -> GameState {
|
||||
let mut g = GameState::new(0, DrawMode::DrawOne);
|
||||
// Clear stock, waste, all tableaus.
|
||||
g.piles.get_mut(&PileType::Stock).unwrap().cards.clear();
|
||||
g.piles.get_mut(&PileType::Waste).unwrap().cards.clear();
|
||||
for i in 0..7 {
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(i))
|
||||
.unwrap()
|
||||
.cards
|
||||
.clear();
|
||||
g.set_test_stock_cards(Vec::new());
|
||||
g.set_test_waste_cards(Vec::new());
|
||||
for tableau in [
|
||||
Tableau::Tableau1,
|
||||
Tableau::Tableau2,
|
||||
Tableau::Tableau3,
|
||||
Tableau::Tableau4,
|
||||
Tableau::Tableau5,
|
||||
Tableau::Tableau6,
|
||||
Tableau::Tableau7,
|
||||
] {
|
||||
g.set_test_tableau_cards(tableau, Vec::new());
|
||||
}
|
||||
// Place test cards.
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(0))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(Card {
|
||||
g.set_test_tableau_cards(
|
||||
Tableau::Tableau1,
|
||||
vec![Card {
|
||||
id: 100,
|
||||
suit: Suit::Clubs,
|
||||
rank: Rank::Five,
|
||||
face_up: true,
|
||||
});
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(1))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(Card {
|
||||
}],
|
||||
);
|
||||
g.set_test_tableau_cards(
|
||||
Tableau::Tableau2,
|
||||
vec![Card {
|
||||
id: 101,
|
||||
suit: Suit::Hearts,
|
||||
rank: Rank::Six,
|
||||
face_up: true,
|
||||
});
|
||||
g.piles
|
||||
.get_mut(&PileType::Tableau(2))
|
||||
.unwrap()
|
||||
.cards
|
||||
.push(Card {
|
||||
}],
|
||||
);
|
||||
g.set_test_tableau_cards(
|
||||
Tableau::Tableau3,
|
||||
vec![Card {
|
||||
id: 102,
|
||||
suit: Suit::Diamonds,
|
||||
rank: Rank::Six,
|
||||
face_up: true,
|
||||
});
|
||||
}],
|
||||
);
|
||||
g
|
||||
}
|
||||
|
||||
@@ -1084,7 +1112,7 @@ mod tests {
|
||||
.clone();
|
||||
// The cycle order starts at Waste, but Waste is empty so the next
|
||||
// available pile (Tableau(0)) is selected.
|
||||
assert_eq!(selected, Some(PileType::Tableau(0)));
|
||||
assert_eq!(selected, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
||||
assert_eq!(
|
||||
*app.world().resource::<KeyboardDragState>(),
|
||||
KeyboardDragState::Idle
|
||||
@@ -1104,7 +1132,7 @@ mod tests {
|
||||
// Manually focus Tableau(0) so we don't depend on Tab.
|
||||
app.world_mut()
|
||||
.resource_mut::<SelectionState>()
|
||||
.selected_pile = Some(PileType::Tableau(0));
|
||||
.selected_pile = Some(KlondikePile::Tableau(Tableau::Tableau1));
|
||||
|
||||
press_key(&mut app, KeyCode::Enter);
|
||||
app.update();
|
||||
@@ -1119,7 +1147,7 @@ mod tests {
|
||||
legal_destinations,
|
||||
destination_index,
|
||||
} => {
|
||||
assert_eq!(source_pile, PileType::Tableau(0));
|
||||
assert_eq!(source_pile, KlondikePile::Tableau(Tableau::Tableau1));
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(cards, vec![100]);
|
||||
assert!(
|
||||
@@ -1134,7 +1162,7 @@ mod tests {
|
||||
// DragState must mirror the lifted cards and carry the keyboard sentinel.
|
||||
let drag = app.world().resource::<DragState>();
|
||||
assert_eq!(drag.cards, vec![100]);
|
||||
assert_eq!(drag.origin_pile, Some(PileType::Tableau(0)));
|
||||
assert_eq!(drag.origin_pile, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
||||
assert_eq!(drag.active_touch_id, Some(KEYBOARD_DRAG_TOUCH_ID));
|
||||
}
|
||||
|
||||
@@ -1151,7 +1179,7 @@ mod tests {
|
||||
app.update();
|
||||
app.world_mut()
|
||||
.resource_mut::<SelectionState>()
|
||||
.selected_pile = Some(PileType::Tableau(0));
|
||||
.selected_pile = Some(KlondikePile::Tableau(Tableau::Tableau1));
|
||||
press_key(&mut app, KeyCode::Enter);
|
||||
app.update();
|
||||
|
||||
@@ -1171,7 +1199,7 @@ mod tests {
|
||||
|
||||
let events = collect_move_events(&mut app);
|
||||
assert_eq!(events.len(), 1, "exactly one MoveRequestEvent must fire");
|
||||
assert_eq!(events[0].from, PileType::Tableau(0));
|
||||
assert_eq!(events[0].from, KlondikePile::Tableau(Tableau::Tableau1));
|
||||
assert_eq!(events[0].to, expected_dest);
|
||||
assert_eq!(events[0].count, 1);
|
||||
|
||||
@@ -1196,7 +1224,7 @@ mod tests {
|
||||
app.update();
|
||||
app.world_mut()
|
||||
.resource_mut::<SelectionState>()
|
||||
.selected_pile = Some(PileType::Tableau(0));
|
||||
.selected_pile = Some(KlondikePile::Tableau(Tableau::Tableau1));
|
||||
press_key(&mut app, KeyCode::Enter);
|
||||
app.update();
|
||||
assert!(app.world().resource::<KeyboardDragState>().is_lifted());
|
||||
@@ -1213,7 +1241,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
app.world().resource::<SelectionState>().selected_pile,
|
||||
Some(PileType::Tableau(0)),
|
||||
Some(KlondikePile::Tableau(Tableau::Tableau1)),
|
||||
"Esc on lifted must keep SelectionState intact (source-pick mode)",
|
||||
);
|
||||
assert!(
|
||||
@@ -1236,7 +1264,7 @@ mod tests {
|
||||
{
|
||||
let mut drag = app.world_mut().resource_mut::<DragState>();
|
||||
drag.cards = vec![100];
|
||||
drag.origin_pile = Some(PileType::Tableau(0));
|
||||
drag.origin_pile = Some(KlondikePile::Tableau(Tableau::Tableau1));
|
||||
drag.committed = true;
|
||||
drag.active_touch_id = None;
|
||||
}
|
||||
@@ -1269,7 +1297,7 @@ mod tests {
|
||||
app.update();
|
||||
app.world_mut()
|
||||
.resource_mut::<SelectionState>()
|
||||
.selected_pile = Some(PileType::Tableau(0));
|
||||
.selected_pile = Some(KlondikePile::Tableau(Tableau::Tableau1));
|
||||
press_key(&mut app, KeyCode::Enter);
|
||||
app.update();
|
||||
|
||||
@@ -1278,7 +1306,7 @@ mod tests {
|
||||
app.update();
|
||||
assert_eq!(
|
||||
app.world().resource::<SelectionState>().selected_pile,
|
||||
Some(PileType::Tableau(0)),
|
||||
Some(KlondikePile::Tableau(Tableau::Tableau1)),
|
||||
"first Esc only cancels the lift",
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user