refactor(core,engine,wasm): canonical FOUNDATIONS/TABLEAUS consts; adopt upstream SUITS/RANKS #137
@@ -19,5 +19,33 @@ pub use klondike::{DrawStockConfig, Foundation, Klondike, KlondikeInstruction, K
|
|||||||
// former `solitaire_data::solver` wrapper module.
|
// former `solitaire_data::solver` wrapper module.
|
||||||
pub use game_state::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome};
|
pub use game_state::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, SolveOutcome};
|
||||||
|
|
||||||
|
/// All four foundation slots, in slot order.
|
||||||
|
///
|
||||||
|
/// Canonical iteration source for `Foundation` — upstream `klondike` has no
|
||||||
|
/// `Foundation::ALL` (unlike `Suit::SUITS` / `Rank::RANKS` in `card_game`),
|
||||||
|
/// and inherent impls cannot be added to a foreign type, so the workspace
|
||||||
|
/// const lives here. Use this instead of hand-rolling `[Foundation; 4]`
|
||||||
|
/// arrays; scattered copies can silently diverge.
|
||||||
|
pub const FOUNDATIONS: [Foundation; 4] = [
|
||||||
|
Foundation::Foundation1,
|
||||||
|
Foundation::Foundation2,
|
||||||
|
Foundation::Foundation3,
|
||||||
|
Foundation::Foundation4,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// All seven tableau columns, in column order (left to right on screen).
|
||||||
|
///
|
||||||
|
/// Canonical iteration source for `Tableau` — see [`FOUNDATIONS`] for why
|
||||||
|
/// this lives here rather than upstream.
|
||||||
|
pub const TABLEAUS: [Tableau; 7] = [
|
||||||
|
Tableau::Tableau1,
|
||||||
|
Tableau::Tableau2,
|
||||||
|
Tableau::Tableau3,
|
||||||
|
Tableau::Tableau4,
|
||||||
|
Tableau::Tableau5,
|
||||||
|
Tableau::Tableau6,
|
||||||
|
Tableau::Tableau7,
|
||||||
|
];
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod proptest_tests;
|
mod proptest_tests;
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ pub const ALL_RANKS: [Rank; 13] = [
|
|||||||
Rank::King,
|
Rank::King,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Every suit in `Clubs, Diamonds, Hearts, Spades` order — matches
|
/// Iteration order for the SVG generator and the pin test only —
|
||||||
/// `card_plugin::load_card_images` so the suit index used here lines
|
/// output files are keyed by `suit_filename`, so no runtime index
|
||||||
/// up with `CardImageSet.faces[suit]`.
|
/// depends on this order. Kept local (not `Suit::SUITS`) because
|
||||||
|
/// reordering would churn the pinned snapshot ordering for no
|
||||||
|
/// benefit.
|
||||||
pub const ALL_SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
|
pub const ALL_SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
|
||||||
|
|
||||||
/// The rank component of the on-disk filename — `A`, `2`..`10`, `J`,
|
/// The rank component of the on-disk filename — `A`, `2`..`10`, `J`,
|
||||||
|
|||||||
@@ -137,6 +137,23 @@ pub const RED_SUIT_COLOUR_HC: Color = Color::srgb(1.000, 0.408, 0.408);
|
|||||||
/// high-contrast boost path.
|
/// high-contrast boost path.
|
||||||
pub const BLACK_SUIT_COLOUR: Color = Color::srgb(0.910, 0.910, 0.910);
|
pub const BLACK_SUIT_COLOUR: Color = Color::srgb(0.910, 0.910, 0.910);
|
||||||
|
|
||||||
|
/// Canonical outer index of `s` in [`CardImageSet::faces`].
|
||||||
|
///
|
||||||
|
/// Derived from the upstream `card_game::Suit` discriminants (0..=3 in
|
||||||
|
/// `Suit::SUITS` order), so every reader and writer of `faces` computes
|
||||||
|
/// the same layout from the same source. Three hand-rolled copies of this
|
||||||
|
/// mapping once lived in card_plugin and theme/plugin and were one
|
||||||
|
/// reorder away from drawing the wrong art.
|
||||||
|
pub(crate) const fn suit_index(s: solitaire_core::Suit) -> usize {
|
||||||
|
s as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical inner index of `r` in [`CardImageSet::faces`] — upstream
|
||||||
|
/// `card_game::Rank` discriminants are 1..=13 in `Rank::RANKS` order.
|
||||||
|
pub(crate) const fn rank_index(r: solitaire_core::Rank) -> usize {
|
||||||
|
r as usize - 1
|
||||||
|
}
|
||||||
|
|
||||||
/// Pre-loaded [`Handle<Image>`]s for card face and back PNG textures.
|
/// Pre-loaded [`Handle<Image>`]s for card face and back PNG textures.
|
||||||
///
|
///
|
||||||
/// Loaded once at startup by [`load_card_images`]. When this resource is
|
/// Loaded once at startup by [`load_card_images`]. When this resource is
|
||||||
@@ -146,8 +163,10 @@ pub const BLACK_SUIT_COLOUR: Color = Color::srgb(0.910, 0.910, 0.910);
|
|||||||
pub struct CardImageSet {
|
pub struct CardImageSet {
|
||||||
/// Per-card face images indexed by `[suit][rank]`.
|
/// Per-card face images indexed by `[suit][rank]`.
|
||||||
///
|
///
|
||||||
/// Suit order: Clubs=0, Diamonds=1, Hearts=2, Spades=3.
|
/// Layout is pinned to the upstream declaration order — index with
|
||||||
/// Rank order: Ace=0, Two=1 … King=12.
|
/// [`suit_index`] / [`rank_index`], never a hand-rolled match.
|
||||||
|
/// Suit order: `Suit::SUITS` (Spades=0, Hearts=1, Clubs=2, Diamonds=3).
|
||||||
|
/// Rank order: `Rank::RANKS` (Ace=0 … King=12).
|
||||||
pub faces: [[Handle<Image>; 13]; 4],
|
pub faces: [[Handle<Image>; 13]; 4],
|
||||||
/// One handle per unlockable card-back design (indices 0–4). These
|
/// One handle per unlockable card-back design (indices 0–4). These
|
||||||
/// correspond to the legacy `assets/cards/backs/back_N.png` art, indexed
|
/// correspond to the legacy `assets/cards/backs/back_N.png` art, indexed
|
||||||
|
|||||||
@@ -105,25 +105,12 @@ pub(super) fn load_card_images(asset_server: Option<Res<AssetServer>>, mut comma
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
|
// faces[suit_index(s)][rank_index(r)] — see the canonical helpers in
|
||||||
const RANKS: [Rank; 13] = [
|
// card_plugin::mod; building from SUITS/RANKS order matches them.
|
||||||
Rank::Ace,
|
|
||||||
Rank::Two,
|
|
||||||
Rank::Three,
|
|
||||||
Rank::Four,
|
|
||||||
Rank::Five,
|
|
||||||
Rank::Six,
|
|
||||||
Rank::Seven,
|
|
||||||
Rank::Eight,
|
|
||||||
Rank::Nine,
|
|
||||||
Rank::Ten,
|
|
||||||
Rank::Jack,
|
|
||||||
Rank::Queen,
|
|
||||||
Rank::King,
|
|
||||||
];
|
|
||||||
|
|
||||||
let faces: [[Handle<Image>; 13]; 4] = std::array::from_fn(|si| {
|
let faces: [[Handle<Image>; 13]; 4] = std::array::from_fn(|si| {
|
||||||
std::array::from_fn(|ri| asset_server.load(card_face_asset_path(RANKS[ri], SUITS[si])))
|
std::array::from_fn(|ri| {
|
||||||
|
asset_server.load(card_face_asset_path(Rank::RANKS[ri], Suit::SUITS[si]))
|
||||||
|
})
|
||||||
});
|
});
|
||||||
let backs =
|
let backs =
|
||||||
std::array::from_fn(|i| asset_server.load(format!("cards/backs/classic/back_{i}.png")));
|
std::array::from_fn(|i| asset_server.load(format!("cards/backs/classic/back_{i}.png")));
|
||||||
@@ -149,27 +136,8 @@ pub(super) fn card_sprite(
|
|||||||
) -> Sprite {
|
) -> Sprite {
|
||||||
if let Some(set) = card_images {
|
if let Some(set) = card_images {
|
||||||
let image = if face_up {
|
let image = if face_up {
|
||||||
let suit_idx = match card.suit() {
|
let suit_idx = suit_index(card.suit());
|
||||||
Suit::Clubs => 0,
|
let rank_idx = rank_index(card.rank());
|
||||||
Suit::Diamonds => 1,
|
|
||||||
Suit::Hearts => 2,
|
|
||||||
Suit::Spades => 3,
|
|
||||||
};
|
|
||||||
let rank_idx = match card.rank() {
|
|
||||||
Rank::Ace => 0,
|
|
||||||
Rank::Two => 1,
|
|
||||||
Rank::Three => 2,
|
|
||||||
Rank::Four => 3,
|
|
||||||
Rank::Five => 4,
|
|
||||||
Rank::Six => 5,
|
|
||||||
Rank::Seven => 6,
|
|
||||||
Rank::Eight => 7,
|
|
||||||
Rank::Nine => 8,
|
|
||||||
Rank::Ten => 9,
|
|
||||||
Rank::Jack => 10,
|
|
||||||
Rank::Queen => 11,
|
|
||||||
Rank::King => 12,
|
|
||||||
};
|
|
||||||
set.faces[suit_idx][rank_idx].clone()
|
set.faces[suit_idx][rank_idx].clone()
|
||||||
} else if let Some(theme_back) = &set.theme_back {
|
} else if let Some(theme_back) = &set.theme_back {
|
||||||
// Active theme provides its own back — always wins over the
|
// Active theme provides its own back — always wins over the
|
||||||
@@ -519,23 +487,10 @@ pub(super) fn all_cards(game: &GameState) -> Vec<(Card, bool)> {
|
|||||||
let mut cards: Vec<(Card, bool)> = Vec::with_capacity(52);
|
let mut cards: Vec<(Card, bool)> = Vec::with_capacity(52);
|
||||||
cards.extend(game.stock_cards());
|
cards.extend(game.stock_cards());
|
||||||
cards.extend(game.waste_cards());
|
cards.extend(game.waste_cards());
|
||||||
for foundation in [
|
for foundation in solitaire_core::FOUNDATIONS {
|
||||||
Foundation::Foundation1,
|
|
||||||
Foundation::Foundation2,
|
|
||||||
Foundation::Foundation3,
|
|
||||||
Foundation::Foundation4,
|
|
||||||
] {
|
|
||||||
cards.extend(game.pile(KlondikePile::Foundation(foundation)));
|
cards.extend(game.pile(KlondikePile::Foundation(foundation)));
|
||||||
}
|
}
|
||||||
for tableau in [
|
for tableau in solitaire_core::TABLEAUS {
|
||||||
Tableau::Tableau1,
|
|
||||||
Tableau::Tableau2,
|
|
||||||
Tableau::Tableau3,
|
|
||||||
Tableau::Tableau4,
|
|
||||||
Tableau::Tableau5,
|
|
||||||
Tableau::Tableau6,
|
|
||||||
Tableau::Tableau7,
|
|
||||||
] {
|
|
||||||
cards.extend(game.pile(KlondikePile::Tableau(tableau)));
|
cards.extend(game.pile(KlondikePile::Tableau(tableau)));
|
||||||
}
|
}
|
||||||
cards
|
cards
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use bevy::window::PrimaryWindow;
|
|||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use bevy::window::{MonitorSelection, WindowMode};
|
use bevy::window::{MonitorSelection, WindowMode};
|
||||||
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
|
||||||
|
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
||||||
use solitaire_core::{Card, Suit};
|
use solitaire_core::{Card, Suit};
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
@@ -1329,13 +1330,13 @@ const DOUBLE_TAP_FLASH_SECS: f32 = 0.35;
|
|||||||
pub fn best_destination(card: &Card, game: &GameState) -> Option<KlondikePile> {
|
pub fn best_destination(card: &Card, game: &GameState) -> Option<KlondikePile> {
|
||||||
let source = game.pile_containing_card(card.clone())?;
|
let source = game.pile_containing_card(card.clone())?;
|
||||||
|
|
||||||
for foundation in foundations() {
|
for foundation in FOUNDATIONS {
|
||||||
let dest = KlondikePile::Foundation(foundation);
|
let dest = KlondikePile::Foundation(foundation);
|
||||||
if game.can_move_cards(&source, &dest, 1) {
|
if game.can_move_cards(&source, &dest, 1) {
|
||||||
return Some(dest);
|
return Some(dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
let dest = KlondikePile::Tableau(tableau);
|
let dest = KlondikePile::Tableau(tableau);
|
||||||
if game.can_move_cards(&source, &dest, 1) {
|
if game.can_move_cards(&source, &dest, 1) {
|
||||||
return Some(dest);
|
return Some(dest);
|
||||||
@@ -1356,7 +1357,7 @@ pub fn best_tableau_destination_for_stack(
|
|||||||
game: &GameState,
|
game: &GameState,
|
||||||
stack_count: usize,
|
stack_count: usize,
|
||||||
) -> Option<(KlondikePile, usize)> {
|
) -> Option<(KlondikePile, usize)> {
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
let dest = KlondikePile::Tableau(tableau);
|
let dest = KlondikePile::Tableau(tableau);
|
||||||
if game.can_move_cards(from, &dest, stack_count) {
|
if game.can_move_cards(from, &dest, stack_count) {
|
||||||
return Some((dest, stack_count));
|
return Some((dest, stack_count));
|
||||||
@@ -1692,7 +1693,7 @@ pub(crate) fn hint_piles(
|
|||||||
fn legacy_all_hints(game: &GameState) -> Vec<(KlondikePile, KlondikePile)> {
|
fn legacy_all_hints(game: &GameState) -> Vec<(KlondikePile, KlondikePile)> {
|
||||||
let sources: Vec<KlondikePile> = {
|
let sources: Vec<KlondikePile> = {
|
||||||
let mut s = vec![KlondikePile::Stock];
|
let mut s = vec![KlondikePile::Stock];
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
s.push(KlondikePile::Tableau(tableau));
|
s.push(KlondikePile::Tableau(tableau));
|
||||||
}
|
}
|
||||||
s
|
s
|
||||||
@@ -1706,7 +1707,7 @@ fn legacy_all_hints(game: &GameState) -> Vec<(KlondikePile, KlondikePile)> {
|
|||||||
let Some(_card) = from_pile.last().filter(|(_, face_up)| *face_up) else {
|
let Some(_card) = from_pile.last().filter(|(_, face_up)| *face_up) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for foundation in foundations() {
|
for foundation in FOUNDATIONS {
|
||||||
let dest = KlondikePile::Foundation(foundation);
|
let dest = KlondikePile::Foundation(foundation);
|
||||||
if game.can_move_cards(from, &dest, 1) {
|
if game.can_move_cards(from, &dest, 1) {
|
||||||
hints.push((*from, dest));
|
hints.push((*from, dest));
|
||||||
@@ -1728,7 +1729,7 @@ fn legacy_all_hints(game: &GameState) -> Vec<(KlondikePile, KlondikePile)> {
|
|||||||
if already_has_foundation_hint {
|
if already_has_foundation_hint {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
let dest = KlondikePile::Tableau(tableau);
|
let dest = KlondikePile::Tableau(tableau);
|
||||||
if game.can_move_cards(from, &dest, 1) {
|
if game.can_move_cards(from, &dest, 1) {
|
||||||
hints.push((*from, dest));
|
hints.push((*from, dest));
|
||||||
@@ -1742,13 +1743,13 @@ fn legacy_all_hints(game: &GameState) -> Vec<(KlondikePile, KlondikePile)> {
|
|||||||
// should never hint Foundation→Foundation. Here we handle the return path
|
// should never hint Foundation→Foundation. Here we handle the return path
|
||||||
// separately so the guarded `take_from_foundation` rule is respected.
|
// separately so the guarded `take_from_foundation` rule is respected.
|
||||||
if game.take_from_foundation {
|
if game.take_from_foundation {
|
||||||
for foundation in foundations() {
|
for foundation in FOUNDATIONS {
|
||||||
let from = KlondikePile::Foundation(foundation);
|
let from = KlondikePile::Foundation(foundation);
|
||||||
let from_pile = pile_cards(game, &from);
|
let from_pile = pile_cards(game, &from);
|
||||||
let Some(_card) = from_pile.last().filter(|(_, face_up)| *face_up) else {
|
let Some(_card) = from_pile.last().filter(|(_, face_up)| *face_up) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
let dest = KlondikePile::Tableau(tableau);
|
let dest = KlondikePile::Tableau(tableau);
|
||||||
if game.can_move_cards(&from, &dest, 1) {
|
if game.can_move_cards(&from, &dest, 1) {
|
||||||
hints.push((from, dest));
|
hints.push((from, dest));
|
||||||
@@ -1782,26 +1783,7 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn foundations() -> [Foundation; 4] {
|
|
||||||
[
|
|
||||||
Foundation::Foundation1,
|
|
||||||
Foundation::Foundation2,
|
|
||||||
Foundation::Foundation3,
|
|
||||||
Foundation::Foundation4,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn tableaus() -> [Tableau; 7] {
|
|
||||||
[
|
|
||||||
Tableau::Tableau1,
|
|
||||||
Tableau::Tableau2,
|
|
||||||
Tableau::Tableau3,
|
|
||||||
Tableau::Tableau4,
|
|
||||||
Tableau::Tableau5,
|
|
||||||
Tableau::Tableau6,
|
|
||||||
Tableau::Tableau7,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn tableau_number(tableau: Tableau) -> u8 {
|
const fn tableau_number(tableau: Tableau) -> u8 {
|
||||||
match tableau {
|
match tableau {
|
||||||
|
|||||||
@@ -220,32 +220,11 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
game.set_test_foundation_cards(foundation, Vec::new());
|
game.set_test_foundation_cards(foundation, Vec::new());
|
||||||
}
|
}
|
||||||
for tableau in [
|
for tableau in solitaire_core::TABLEAUS {
|
||||||
Tableau::Tableau1,
|
|
||||||
Tableau::Tableau2,
|
|
||||||
Tableau::Tableau3,
|
|
||||||
Tableau::Tableau4,
|
|
||||||
Tableau::Tableau5,
|
|
||||||
Tableau::Tableau6,
|
|
||||||
Tableau::Tableau7,
|
|
||||||
] {
|
|
||||||
game.set_test_tableau_cards(tableau, Vec::new());
|
game.set_test_tableau_cards(tableau, Vec::new());
|
||||||
}
|
}
|
||||||
let suits = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
|
let suits = Suit::SUITS;
|
||||||
let ranks_below_king = [
|
let ranks_below_king = &Rank::RANKS[..12]; // everything below King
|
||||||
Rank::Ace,
|
|
||||||
Rank::Two,
|
|
||||||
Rank::Three,
|
|
||||||
Rank::Four,
|
|
||||||
Rank::Five,
|
|
||||||
Rank::Six,
|
|
||||||
Rank::Seven,
|
|
||||||
Rank::Eight,
|
|
||||||
Rank::Nine,
|
|
||||||
Rank::Ten,
|
|
||||||
Rank::Jack,
|
|
||||||
Rank::Queen,
|
|
||||||
];
|
|
||||||
for (foundation, suit) in [
|
for (foundation, suit) in [
|
||||||
Foundation::Foundation1,
|
Foundation::Foundation1,
|
||||||
Foundation::Foundation2,
|
Foundation::Foundation2,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ use bevy::math::Vec2;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
@@ -254,13 +255,13 @@ pub fn legal_destinations_for_card(
|
|||||||
game: &GameState,
|
game: &GameState,
|
||||||
) -> Vec<KlondikePile> {
|
) -> Vec<KlondikePile> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for foundation in foundations() {
|
for foundation in FOUNDATIONS {
|
||||||
let dest = KlondikePile::Foundation(foundation);
|
let dest = KlondikePile::Foundation(foundation);
|
||||||
if game.can_move_cards(source_pile, &dest, 1) {
|
if game.can_move_cards(source_pile, &dest, 1) {
|
||||||
out.push(dest);
|
out.push(dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
let dest = KlondikePile::Tableau(tableau);
|
let dest = KlondikePile::Tableau(tableau);
|
||||||
if game.can_move_cards(source_pile, &dest, 1) {
|
if game.can_move_cards(source_pile, &dest, 1) {
|
||||||
out.push(dest);
|
out.push(dest);
|
||||||
@@ -360,26 +361,7 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const fn foundations() -> [Foundation; 4] {
|
|
||||||
[
|
|
||||||
Foundation::Foundation1,
|
|
||||||
Foundation::Foundation2,
|
|
||||||
Foundation::Foundation3,
|
|
||||||
Foundation::Foundation4,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn tableaus() -> [Tableau; 7] {
|
|
||||||
[
|
|
||||||
Tableau::Tableau1,
|
|
||||||
Tableau::Tableau2,
|
|
||||||
Tableau::Tableau3,
|
|
||||||
Tableau::Tableau4,
|
|
||||||
Tableau::Tableau5,
|
|
||||||
Tableau::Tableau6,
|
|
||||||
Tableau::Tableau7,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the `(destination, anchor)` list for a fresh radial open.
|
/// Builds the `(destination, anchor)` list for a fresh radial open.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
use solitaire_core::KlondikePile;
|
||||||
|
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
||||||
use solitaire_core::Suit;
|
use solitaire_core::Suit;
|
||||||
|
|
||||||
use crate::events::{HintVisualEvent, StateChangedEvent};
|
use crate::events::{HintVisualEvent, StateChangedEvent};
|
||||||
@@ -280,10 +281,10 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
|||||||
|
|
||||||
let mut piles: Vec<KlondikePile> = Vec::with_capacity(12);
|
let mut piles: Vec<KlondikePile> = Vec::with_capacity(12);
|
||||||
piles.push(KlondikePile::Stock);
|
piles.push(KlondikePile::Stock);
|
||||||
for foundation in foundations() {
|
for foundation in FOUNDATIONS {
|
||||||
piles.push(KlondikePile::Foundation(foundation));
|
piles.push(KlondikePile::Foundation(foundation));
|
||||||
}
|
}
|
||||||
for tableau in tableaus() {
|
for tableau in TABLEAUS {
|
||||||
piles.push(KlondikePile::Tableau(tableau));
|
piles.push(KlondikePile::Tableau(tableau));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,31 +577,13 @@ fn pile_cards(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn foundations() -> [Foundation; 4] {
|
|
||||||
[
|
|
||||||
Foundation::Foundation1,
|
|
||||||
Foundation::Foundation2,
|
|
||||||
Foundation::Foundation3,
|
|
||||||
Foundation::Foundation4,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn tableaus() -> [Tableau; 7] {
|
|
||||||
[
|
|
||||||
Tableau::Tableau1,
|
|
||||||
Tableau::Tableau2,
|
|
||||||
Tableau::Tableau3,
|
|
||||||
Tableau::Tableau4,
|
|
||||||
Tableau::Tableau5,
|
|
||||||
Tableau::Tableau6,
|
|
||||||
Tableau::Tableau7,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::game_plugin::GamePlugin;
|
use crate::game_plugin::GamePlugin;
|
||||||
|
use solitaire_core::{Foundation, Tableau};
|
||||||
|
|
||||||
/// Minimal headless app — omits windowing so pile markers are spawned with
|
/// Minimal headless app — omits windowing so pile markers are spawned with
|
||||||
/// the default 1280×800 layout and no camera is created.
|
/// the default 1280×800 layout and no camera is created.
|
||||||
@@ -940,7 +923,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn suit_symbol_all_four_are_distinct() {
|
fn suit_symbol_all_four_are_distinct() {
|
||||||
let symbols: Vec<&str> = [Suit::Spades, Suit::Hearts, Suit::Diamonds, Suit::Clubs]
|
let symbols: Vec<&str> = Suit::SUITS
|
||||||
.iter()
|
.iter()
|
||||||
.map(suit_symbol)
|
.map(suit_symbol)
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -62,22 +62,10 @@ impl CardKey {
|
|||||||
/// Iterator over all 52 valid keys, in suit-major / rank-ascending order.
|
/// Iterator over all 52 valid keys, in suit-major / rank-ascending order.
|
||||||
/// Used to enumerate the manifest's required entries.
|
/// Used to enumerate the manifest's required entries.
|
||||||
pub fn all() -> impl Iterator<Item = CardKey> {
|
pub fn all() -> impl Iterator<Item = CardKey> {
|
||||||
const SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
|
// Order-independent enumeration — consumers check completeness and
|
||||||
const RANKS: [Rank; 13] = [
|
// round-trips, never positions.
|
||||||
Rank::Ace,
|
const SUITS: [Suit; 4] = Suit::SUITS;
|
||||||
Rank::Two,
|
const RANKS: [Rank; 13] = Rank::RANKS;
|
||||||
Rank::Three,
|
|
||||||
Rank::Four,
|
|
||||||
Rank::Five,
|
|
||||||
Rank::Six,
|
|
||||||
Rank::Seven,
|
|
||||||
Rank::Eight,
|
|
||||||
Rank::Nine,
|
|
||||||
Rank::Ten,
|
|
||||||
Rank::Jack,
|
|
||||||
Rank::Queen,
|
|
||||||
Rank::King,
|
|
||||||
];
|
|
||||||
SUITS
|
SUITS
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|s| RANKS.into_iter().map(move |r| CardKey::new(s, r)))
|
.flat_map(|s| RANKS.into_iter().map(move |r| CardKey::new(s, r)))
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use solitaire_core::{Rank, Suit};
|
|||||||
use crate::assets::{
|
use crate::assets::{
|
||||||
bundled_theme_url, classic_theme_svg_bytes, dark_theme_svg_bytes, rasterize_svg, user_theme_dir,
|
bundled_theme_url, classic_theme_svg_bytes, dark_theme_svg_bytes, rasterize_svg, user_theme_dir,
|
||||||
};
|
};
|
||||||
use crate::card_plugin::CardImageSet;
|
use crate::card_plugin::{CardImageSet, rank_index, suit_index};
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
|
|
||||||
use super::loader::CardThemeLoader;
|
use super::loader::CardThemeLoader;
|
||||||
@@ -242,22 +242,8 @@ fn sync_card_image_set_with_active_theme(
|
|||||||
/// `theme_back` when present, so writing here is sufficient to make
|
/// `theme_back` when present, so writing here is sufficient to make
|
||||||
/// every face-down card pick up the theme's art on the next sync.
|
/// every face-down card pick up the theme's art on the next sync.
|
||||||
fn apply_theme_to_card_image_set(theme: &CardTheme, image_set: &mut CardImageSet) {
|
fn apply_theme_to_card_image_set(theme: &CardTheme, image_set: &mut CardImageSet) {
|
||||||
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
|
for suit in Suit::SUITS {
|
||||||
for rank in [
|
for rank in Rank::RANKS {
|
||||||
Rank::Ace,
|
|
||||||
Rank::Two,
|
|
||||||
Rank::Three,
|
|
||||||
Rank::Four,
|
|
||||||
Rank::Five,
|
|
||||||
Rank::Six,
|
|
||||||
Rank::Seven,
|
|
||||||
Rank::Eight,
|
|
||||||
Rank::Nine,
|
|
||||||
Rank::Ten,
|
|
||||||
Rank::Jack,
|
|
||||||
Rank::Queen,
|
|
||||||
Rank::King,
|
|
||||||
] {
|
|
||||||
if let Some(handle) = theme.faces.get(&CardKey::new(suit, rank)) {
|
if let Some(handle) = theme.faces.get(&CardKey::new(suit, rank)) {
|
||||||
image_set.faces[suit_index(suit)][rank_index(rank)] = handle.clone();
|
image_set.faces[suit_index(suit)][rank_index(rank)] = handle.clone();
|
||||||
}
|
}
|
||||||
@@ -266,36 +252,7 @@ fn apply_theme_to_card_image_set(theme: &CardTheme, image_set: &mut CardImageSet
|
|||||||
image_set.theme_back = Some(theme.back.clone());
|
image_set.theme_back = Some(theme.back.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Index used by [`CardImageSet::faces`] for a given suit. Mirrors
|
|
||||||
/// the `card_plugin` doc comment: Clubs=0, Diamonds=1, Hearts=2, Spades=3.
|
|
||||||
const fn suit_index(s: Suit) -> usize {
|
|
||||||
match s {
|
|
||||||
Suit::Clubs => 0,
|
|
||||||
Suit::Diamonds => 1,
|
|
||||||
Suit::Hearts => 2,
|
|
||||||
Suit::Spades => 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Index used by [`CardImageSet::faces`] for a given rank.
|
|
||||||
/// Ace=0, Two=1 … King=12.
|
|
||||||
const fn rank_index(r: Rank) -> usize {
|
|
||||||
match r {
|
|
||||||
Rank::Ace => 0,
|
|
||||||
Rank::Two => 1,
|
|
||||||
Rank::Three => 2,
|
|
||||||
Rank::Four => 3,
|
|
||||||
Rank::Five => 4,
|
|
||||||
Rank::Six => 5,
|
|
||||||
Rank::Seven => 6,
|
|
||||||
Rank::Eight => 7,
|
|
||||||
Rank::Nine => 8,
|
|
||||||
Rank::Ten => 9,
|
|
||||||
Rank::Jack => 10,
|
|
||||||
Rank::Queen => 11,
|
|
||||||
Rank::King => 12,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Switches the active theme to the one served at
|
/// Switches the active theme to the one served at
|
||||||
/// `themes://<theme_id>/theme.ron`. Returns the new `Handle<CardTheme>`
|
/// `themes://<theme_id>/theme.ron`. Returns the new `Handle<CardTheme>`
|
||||||
@@ -446,21 +403,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn suit_index_ranges_match_card_plugin_layout() {
|
fn suit_index_matches_upstream_suits_order() {
|
||||||
assert_eq!(suit_index(Suit::Clubs), 0);
|
for (i, s) in Suit::SUITS.iter().enumerate() {
|
||||||
assert_eq!(suit_index(Suit::Diamonds), 1);
|
assert_eq!(suit_index(*s), i, "faces outer layout = Suit::SUITS order");
|
||||||
assert_eq!(suit_index(Suit::Hearts), 2);
|
}
|
||||||
assert_eq!(suit_index(Suit::Spades), 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rank_index_starts_at_ace_zero_and_ends_at_king_twelve() {
|
fn rank_index_matches_upstream_ranks_order() {
|
||||||
assert_eq!(rank_index(Rank::Ace), 0);
|
for (i, r) in Rank::RANKS.iter().enumerate() {
|
||||||
assert_eq!(rank_index(Rank::Two), 1);
|
assert_eq!(rank_index(*r), i, "faces inner layout = Rank::RANKS order");
|
||||||
assert_eq!(rank_index(Rank::Ten), 9);
|
}
|
||||||
assert_eq!(rank_index(Rank::Jack), 10);
|
|
||||||
assert_eq!(rank_index(Rank::Queen), 11);
|
|
||||||
assert_eq!(rank_index(Rank::King), 12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+21
-69
@@ -19,7 +19,7 @@
|
|||||||
//! is the contract.
|
//! is the contract.
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
|
use solitaire_core::{KlondikeInstruction, KlondikePile};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use solitaire_core::{Card, Deck, Rank, Suit};
|
use solitaire_core::{Card, Deck, Rank, Suit};
|
||||||
use solitaire_core::error::MoveError;
|
use solitaire_core::error::MoveError;
|
||||||
@@ -145,21 +145,10 @@ impl ReplayPlayer {
|
|||||||
let pile_cards = |t: KlondikePile| -> Vec<CardSnapshot> {
|
let pile_cards = |t: KlondikePile| -> Vec<CardSnapshot> {
|
||||||
self.game.pile(t).iter().map(CardSnapshot::from).collect()
|
self.game.pile(t).iter().map(CardSnapshot::from).collect()
|
||||||
};
|
};
|
||||||
let foundations: [Vec<CardSnapshot>; 4] = [
|
let foundations: [Vec<CardSnapshot>; 4] = solitaire_core::FOUNDATIONS
|
||||||
pile_cards(KlondikePile::Foundation(Foundation::Foundation1)),
|
.map(|f| pile_cards(KlondikePile::Foundation(f)));
|
||||||
pile_cards(KlondikePile::Foundation(Foundation::Foundation2)),
|
let tableaus: [Vec<CardSnapshot>; 7] =
|
||||||
pile_cards(KlondikePile::Foundation(Foundation::Foundation3)),
|
solitaire_core::TABLEAUS.map(|t| pile_cards(KlondikePile::Tableau(t)));
|
||||||
pile_cards(KlondikePile::Foundation(Foundation::Foundation4)),
|
|
||||||
];
|
|
||||||
let tableaus: [Vec<CardSnapshot>; 7] = [
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau1)),
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau2)),
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau3)),
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau4)),
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau5)),
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau6)),
|
|
||||||
pile_cards(KlondikePile::Tableau(Tableau::Tableau7)),
|
|
||||||
];
|
|
||||||
StateSnapshot {
|
StateSnapshot {
|
||||||
step_idx: self.step_idx,
|
step_idx: self.step_idx,
|
||||||
total_steps: self.moves.len(),
|
total_steps: self.moves.len(),
|
||||||
@@ -353,21 +342,9 @@ fn legal_moves_for_game(game: &GameState) -> Vec<DebugMove> {
|
|||||||
fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> DebugInvariantReport {
|
fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> DebugInvariantReport {
|
||||||
let stock = game.stock_cards();
|
let stock = game.stock_cards();
|
||||||
let waste = game.waste_cards();
|
let waste = game.waste_cards();
|
||||||
let foundations = [
|
let foundations =
|
||||||
game.pile(KlondikePile::Foundation(Foundation::Foundation1)),
|
solitaire_core::FOUNDATIONS.map(|f| game.pile(KlondikePile::Foundation(f)));
|
||||||
game.pile(KlondikePile::Foundation(Foundation::Foundation2)),
|
let tableaus = solitaire_core::TABLEAUS.map(|t| game.pile(KlondikePile::Tableau(t)));
|
||||||
game.pile(KlondikePile::Foundation(Foundation::Foundation3)),
|
|
||||||
game.pile(KlondikePile::Foundation(Foundation::Foundation4)),
|
|
||||||
];
|
|
||||||
let tableaus = [
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau1)),
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau2)),
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau3)),
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau4)),
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau5)),
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau6)),
|
|
||||||
game.pile(KlondikePile::Tableau(Tableau::Tableau7)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut seen: std::collections::HashSet<Card> = std::collections::HashSet::new();
|
let mut seen: std::collections::HashSet<Card> = std::collections::HashSet::new();
|
||||||
let mut duplicate_cards = Vec::new();
|
let mut duplicate_cards = Vec::new();
|
||||||
@@ -488,21 +465,9 @@ impl SolitaireGame {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(CardSnapshot::from)
|
.map(CardSnapshot::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
foundations: [
|
foundations: solitaire_core::FOUNDATIONS
|
||||||
cards(KlondikePile::Foundation(Foundation::Foundation1)),
|
.map(|f| cards(KlondikePile::Foundation(f))),
|
||||||
cards(KlondikePile::Foundation(Foundation::Foundation2)),
|
tableaus: solitaire_core::TABLEAUS.map(|t| cards(KlondikePile::Tableau(t))),
|
||||||
cards(KlondikePile::Foundation(Foundation::Foundation3)),
|
|
||||||
cards(KlondikePile::Foundation(Foundation::Foundation4)),
|
|
||||||
],
|
|
||||||
tableaus: [
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau1)),
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau2)),
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau3)),
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau4)),
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau5)),
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau6)),
|
|
||||||
cards(KlondikePile::Tableau(Tableau::Tableau7)),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,34 +478,21 @@ impl SolitaireGame {
|
|||||||
let slot: u8 = s["foundation-".len()..]
|
let slot: u8 = s["foundation-".len()..]
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| format!("bad pile: {s}"))?;
|
.map_err(|_| format!("bad pile: {s}"))?;
|
||||||
if slot >= 4 {
|
let foundation = solitaire_core::FOUNDATIONS
|
||||||
return Err(format!("foundation slot out of range: {slot}"));
|
.get(slot as usize)
|
||||||
}
|
.copied()
|
||||||
Ok(KlondikePile::Foundation(match slot {
|
.ok_or_else(|| format!("foundation slot out of range: {slot}"))?;
|
||||||
0 => Foundation::Foundation1,
|
Ok(KlondikePile::Foundation(foundation))
|
||||||
1 => Foundation::Foundation2,
|
|
||||||
2 => Foundation::Foundation3,
|
|
||||||
3 => Foundation::Foundation4,
|
|
||||||
_ => return Err(format!("foundation slot out of range: {slot}")),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
_ if s.starts_with("tableau-") => {
|
_ if s.starts_with("tableau-") => {
|
||||||
let col: usize = s["tableau-".len()..]
|
let col: usize = s["tableau-".len()..]
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| format!("bad pile: {s}"))?;
|
.map_err(|_| format!("bad pile: {s}"))?;
|
||||||
if col >= 7 {
|
let tableau = solitaire_core::TABLEAUS
|
||||||
return Err(format!("tableau col out of range: {col}"));
|
.get(col)
|
||||||
}
|
.copied()
|
||||||
Ok(KlondikePile::Tableau(match col {
|
.ok_or_else(|| format!("tableau col out of range: {col}"))?;
|
||||||
0 => Tableau::Tableau1,
|
Ok(KlondikePile::Tableau(tableau))
|
||||||
1 => Tableau::Tableau2,
|
|
||||||
2 => Tableau::Tableau3,
|
|
||||||
3 => Tableau::Tableau4,
|
|
||||||
4 => Tableau::Tableau5,
|
|
||||||
5 => Tableau::Tableau6,
|
|
||||||
6 => Tableau::Tableau7,
|
|
||||||
_ => return Err(format!("tableau col out of range: {col}")),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
_ => Err(format!("unknown pile: {s}")),
|
_ => Err(format!("unknown pile: {s}")),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user