refactor(core): complete card_game::Card migration across engine + wasm
Finish the half-applied Card refactor. solitaire_core::card::Card is now an alias for the opaque card_game::Card: suit()/rank() are methods, there is no id or face_up field, and it is Clone+Eq+Hash but not Copy. Pile accessors return Vec<(Card, bool)> where the bool is face-up. Card identity is now the Card value itself (via Eq/Hash), not a numeric u32: - CardEntity stores `card: Card` (was `card_id: u32`); lookups compare cards. - Drag/selection collections and the touch/keyboard selection setters use Vec<Card>; CardFlippedEvent/CardFaceRevealedEvent/HintVisualEvent carry Card. - replay_overlay and feedback/settle/deal animations updated accordingly. solitaire_wasm: CardSnapshot derives its JSON id from suit+rank (matching the desktop engine), and consumes the (Card, bool) pile tuples. test-support: TestPileState tableau overrides now carry a per-card face-up flag so tests can place face-down tableau cards. set_test_tableau_cards keeps its Vec<Card> signature (defaulting to face-up); new set_test_tableau_cards_with_face takes Vec<(Card, bool)>. cargo test --workspace passes (engine lib 897 ok, 0 failed); cargo clippy --workspace --all-targets -- -D warnings is clean. Save/serde format unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
use bevy::ecs::message::MessageReader;
|
||||
use bevy::prelude::*;
|
||||
use solitaire_core::KlondikePile;
|
||||
use solitaire_core::card::Card;
|
||||
|
||||
use crate::card_plugin::CardEntity;
|
||||
use crate::events::StateChangedEvent;
|
||||
@@ -49,8 +50,8 @@ use crate::ui_theme::ACCENT_PRIMARY;
|
||||
/// card ids that will be moved (1 for a single card, multiple for a face-up run).
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub struct TouchSelectionState {
|
||||
/// Currently selected source pile and the card ids to move (bottom-to-top).
|
||||
pub selected: Option<(KlondikePile, Vec<u32>)>,
|
||||
/// Currently selected source pile and the cards to move (bottom-to-top).
|
||||
pub selected: Option<(KlondikePile, Vec<Card>)>,
|
||||
}
|
||||
|
||||
impl TouchSelectionState {
|
||||
@@ -60,12 +61,12 @@ impl TouchSelectionState {
|
||||
}
|
||||
|
||||
/// Takes the current selection, leaving `selected` as `None`.
|
||||
pub fn take(&mut self) -> Option<(KlondikePile, Vec<u32>)> {
|
||||
pub fn take(&mut self) -> Option<(KlondikePile, Vec<Card>)> {
|
||||
self.selected.take()
|
||||
}
|
||||
|
||||
/// Sets the current selection.
|
||||
pub fn set(&mut self, pile: KlondikePile, cards: Vec<u32>) {
|
||||
pub fn set(&mut self, pile: KlondikePile, cards: Vec<Card>) {
|
||||
self.selected = Some((pile, cards));
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ pub(crate) fn update_touch_selection_highlight(
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
|
||||
let Some((_, ref card_ids)) = selection.selected else {
|
||||
let Some((_, ref cards)) = selection.selected else {
|
||||
return;
|
||||
};
|
||||
let Some(layout) = layout else {
|
||||
@@ -154,8 +155,8 @@ pub(crate) fn update_touch_selection_highlight(
|
||||
// but highlighting the whole run gives the player clear confirmation
|
||||
// of how many cards are involved in the move.
|
||||
let card_size = layout.0.card_size;
|
||||
for &card_id in card_ids {
|
||||
spawn_touch_highlight(&mut commands, &card_entities, card_id, card_size);
|
||||
for card in cards {
|
||||
spawn_touch_highlight(&mut commands, &card_entities, card, card_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,11 +164,11 @@ pub(crate) fn update_touch_selection_highlight(
|
||||
fn spawn_touch_highlight(
|
||||
commands: &mut Commands,
|
||||
card_entities: &Query<(Entity, &CardEntity)>,
|
||||
card_id: u32,
|
||||
card: &Card,
|
||||
card_size: Vec2,
|
||||
) {
|
||||
for (entity, card_entity) in card_entities {
|
||||
if card_entity.card_id == card_id {
|
||||
if card_entity.card == *card {
|
||||
commands.entity(entity).with_children(|b| {
|
||||
b.spawn((
|
||||
TouchSelectionHighlight,
|
||||
@@ -193,6 +194,17 @@ fn spawn_touch_highlight(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solitaire_core::Tableau;
|
||||
use solitaire_core::card::{Card, Deck, Rank, Suit};
|
||||
|
||||
/// Three distinct test cards, used in place of the old `vec![1, 2, 3]`
|
||||
/// numeric ids. Identity is now the `Card` value.
|
||||
fn test_cards() -> [Card; 3] {
|
||||
[
|
||||
Card::new(Deck::Deck1, Suit::Clubs, Rank::Ace),
|
||||
Card::new(Deck::Deck1, Suit::Hearts, Rank::Two),
|
||||
Card::new(Deck::Deck1, Suit::Spades, Rank::Three),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_state_default_is_idle() {
|
||||
@@ -204,20 +216,24 @@ mod tests {
|
||||
#[test]
|
||||
fn set_and_take_roundtrip() {
|
||||
let mut state = TouchSelectionState::default();
|
||||
state.set(KlondikePile::Tableau(Tableau::Tableau1), vec![1, 2, 3]);
|
||||
let cards = test_cards().to_vec();
|
||||
state.set(KlondikePile::Tableau(Tableau::Tableau1), cards.clone());
|
||||
assert!(state.has_selection());
|
||||
let taken = state.take();
|
||||
assert!(taken.is_some());
|
||||
let (pile, cards) = taken.unwrap();
|
||||
let (pile, taken_cards) = taken.unwrap();
|
||||
assert_eq!(pile, KlondikePile::Tableau(Tableau::Tableau1));
|
||||
assert_eq!(cards, vec![1, 2, 3]);
|
||||
assert_eq!(taken_cards, cards);
|
||||
assert!(!state.has_selection());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_removes_selection() {
|
||||
let mut state = TouchSelectionState::default();
|
||||
state.set(KlondikePile::Stock, vec![42]);
|
||||
state.set(
|
||||
KlondikePile::Stock,
|
||||
vec![Card::new(Deck::Deck1, Suit::Diamonds, Rank::King)],
|
||||
);
|
||||
state.clear();
|
||||
assert!(!state.has_selection());
|
||||
}
|
||||
@@ -232,10 +248,17 @@ mod tests {
|
||||
#[test]
|
||||
fn set_overwrites_previous_selection() {
|
||||
let mut state = TouchSelectionState::default();
|
||||
state.set(KlondikePile::Tableau(Tableau::Tableau1), vec![1]);
|
||||
state.set(KlondikePile::Tableau(Tableau::Tableau4), vec![7, 8]);
|
||||
state.set(
|
||||
KlondikePile::Tableau(Tableau::Tableau1),
|
||||
vec![Card::new(Deck::Deck1, Suit::Clubs, Rank::Ace)],
|
||||
);
|
||||
let second = vec![
|
||||
Card::new(Deck::Deck1, Suit::Hearts, Rank::Seven),
|
||||
Card::new(Deck::Deck1, Suit::Spades, Rank::Eight),
|
||||
];
|
||||
state.set(KlondikePile::Tableau(Tableau::Tableau4), second.clone());
|
||||
let (pile, cards) = state.take().unwrap();
|
||||
assert_eq!(pile, KlondikePile::Tableau(Tableau::Tableau4));
|
||||
assert_eq!(cards, vec![7, 8]);
|
||||
assert_eq!(cards, second);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user