refactor: remove card.rs / card_to_id; use card_game::Card directly (#83)

card_to_id was a frankenstein 0..=51 id shim. Replace it with card_game::Card:
- feedback_anim deal jitter now seeds off a hash of the Card itself
- radial_menu RightClickRadialState.cards: Vec<u32> -> Vec<Card>
- wasm CardSnapshot.id: u32 -> Card (serialises transparently as a plain JS
  number, the same opaque key the renderer already used; new test asserts the
  JSON id field is a number)
- wasm DebugInvariantReport deck-completeness check reworked from a [bool;52]
  index into a HashSet<Card> + Card::new reference deck; the out-of-range check
  is dropped since a Card is always valid

Delete card.rs entirely: the Card/Deck/Rank/Suit re-exports move to the crate
root and the 69 `solitaire_core::card::` import paths flatten to `solitaire_core::`.

The JS card.id is purely an opaque identity key (Map key / dataset.cardId, no
arithmetic, card faces render from rank+suit), so the value change is safe.
cargo test --workspace and clippy --workspace --all-targets -- -D warnings green.

Closes #83

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-11 19:33:47 -07:00
parent 5c992cbdca
commit e0a858d4e8
23 changed files with 132 additions and 137 deletions
+19 -19
View File
@@ -27,7 +27,7 @@ use bevy::window::PrimaryWindow;
#[cfg(not(target_os = "android"))]
use bevy::window::{MonitorSelection, WindowMode};
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
use solitaire_core::card::{Card, Suit};
use solitaire_core::{Card, Suit};
use solitaire_core::game_state::GameState;
use crate::auto_complete_plugin::AutoCompleteState;
@@ -1953,8 +1953,8 @@ mod tests {
fn find_draggable_returns_run_when_picking_mid_stack() {
// Manually construct a tableau with three face-up cards all stacked.
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let king = Card::new(D::Deck1, Suit::Spades, Rank::King);
let queen = Card::new(D::Deck1, Suit::Hearts, Rank::Queen);
let jack = Card::new(D::Deck1, Suit::Clubs, Rank::Jack);
@@ -1980,8 +1980,8 @@ mod tests {
#[test]
fn find_draggable_skips_non_top_waste_card() {
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let two_spades = Card::new(D::Deck1, Suit::Spades, Rank::Two);
let three_hearts = Card::new(D::Deck1, Suit::Hearts, Rank::Three);
game.set_test_waste_cards(vec![two_spades, three_hearts.clone()]);
@@ -2044,8 +2044,8 @@ mod tests {
#[test]
fn find_draggable_draw_three_waste_top_card_hit_at_fanned_position() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
use solitaire_core::{DrawStockConfig, game_state::GameMode};
let mut game = GameState::new_with_mode(1, DrawStockConfig::DrawThree, GameMode::Classic);
// Three waste cards; top (four_clubs) is rightmost in the fan.
@@ -2103,8 +2103,8 @@ mod tests {
#[test]
fn best_destination_returns_none_when_no_legal_move() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Clear everything except one card that has nowhere to go.
@@ -2121,8 +2121,8 @@ mod tests {
#[test]
fn best_tableau_destination_for_stack_skips_source_pile() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
clear_test_piles(&mut game);
@@ -2147,8 +2147,8 @@ mod tests {
#[test]
fn best_tableau_destination_for_stack_returns_none_when_no_legal_move() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
clear_test_piles(&mut game);
@@ -2176,8 +2176,8 @@ mod tests {
#[test]
fn find_hint_finds_ace_to_foundation() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Place Ace of Clubs on top of tableau 0.
@@ -2220,8 +2220,8 @@ mod tests {
/// are no other moves and the stock is non-empty.
#[test]
fn all_hints_suggests_draw_when_no_moves_and_stock_nonempty() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
let mut game = GameState::new(1, DrawStockConfig::DrawOne);
// Remove all foundation, tableau, and waste cards so no pile-to-pile
@@ -2273,8 +2273,8 @@ mod tests {
/// gets a CardAnimation" — same coverage, new component.
#[test]
fn rejected_drag_inserts_card_animation_on_each_dragged_card() {
use solitaire_core::card::Deck as D;
use solitaire_core::card::{Card, Rank, Suit};
use solitaire_core::Deck as D;
use solitaire_core::{Card, Rank, Suit};
// Simulate a stack drag of two cards.
let dragged_cards: Vec<Card> = vec![
Card::new(D::Deck1, Suit::Hearts, Rank::King),