From 0d3f037672ff74c7f025004e4ab820728ab37b3d Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 10 Jun 2026 10:14:20 -0700 Subject: [PATCH] refactor: consolidate card_to_id into solitaire_core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three byte-identical copies of the stable 0..=51 card-id helper (suit_index*13 + rank-1) lived in feedback_anim_plugin, radial_menu, and solitaire_wasm. The WASM copy's own comment notes it MUST match the engine for cross-platform replay parity — exactly the kind of invariant a single source of truth should enforce. Add `solitaire_core::card::card_to_id(&Card) -> u32` and have all three call sites import it. No behaviour change (same formula). cargo test --workspace and cargo clippy --workspace --all-targets -- -D warnings pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- solitaire_core/src/card.rs | 22 ++++++++++++++++++++ solitaire_engine/src/feedback_anim_plugin.rs | 16 +++----------- solitaire_engine/src/radial_menu.rs | 14 +------------ solitaire_wasm/src/lib.rs | 15 +++---------- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/solitaire_core/src/card.rs b/solitaire_core/src/card.rs index f9c3a39..fe6e753 100644 --- a/solitaire_core/src/card.rs +++ b/solitaire_core/src/card.rs @@ -1 +1,23 @@ pub use card_game::{Card, Deck, Rank, Suit}; + +/// Maps a [`Card`] to a stable `0..=51` numeric identity, independent of the +/// upstream `card_game::Card` bit-packing. +/// +/// Encoding: `suit_index * 13 + (rank as u32 - 1)`, where `suit_index` is +/// Clubs=0, Diamonds=1, Hearts=2, Spades=3 and `rank` is 1 (Ace) ..= 13 (King). +/// The deck id is intentionally ignored so the id depends only on the visible +/// face. +/// +/// This is the single source of truth shared by `CardEntity` numeric tracking, +/// deterministic per-card animation jitter, and the WASM replay layer — those +/// must agree byte-for-byte so replay snapshots are identical across the +/// desktop and browser builds. +pub fn card_to_id(card: &Card) -> u32 { + let suit_index: u32 = match card.suit() { + Suit::Clubs => 0, + Suit::Diamonds => 1, + Suit::Hearts => 2, + Suit::Spades => 3, + }; + suit_index * 13 + (card.rank() as u32 - 1) +} diff --git a/solitaire_engine/src/feedback_anim_plugin.rs b/solitaire_engine/src/feedback_anim_plugin.rs index 20fb5f1..452b398 100644 --- a/solitaire_engine/src/feedback_anim_plugin.rs +++ b/solitaire_engine/src/feedback_anim_plugin.rs @@ -188,19 +188,9 @@ pub fn deal_stagger_jitter(card_id: u32) -> f32 { (jitter_norm - 0.5) * 0.2 // ±0.1 == ±10 % } -/// Converts a `Card` to a `u32` seed suitable for deterministic per-card -/// jitter. Uses suit index × 13 + (rank value − 1) to produce a stable 0–51 -/// integer that survives changes to the internal `Card` representation. -fn card_to_id(card: &Card) -> u32 { - use solitaire_core::card::Suit; - let suit_index = match card.suit() { - Suit::Clubs => 0, - Suit::Diamonds => 1, - Suit::Hearts => 2, - Suit::Spades => 3, - }; - suit_index * 13 + (card.rank() as u32 - 1) -} +// Per-card jitter keys off the shared stable card id so it matches the +// numeric identity used elsewhere (and on the WASM replay side). +use solitaire_core::card::card_to_id; // --------------------------------------------------------------------------- // Plugin diff --git a/solitaire_engine/src/radial_menu.rs b/solitaire_engine/src/radial_menu.rs index 131e190..ba02cca 100644 --- a/solitaire_engine/src/radial_menu.rs +++ b/solitaire_engine/src/radial_menu.rs @@ -359,19 +359,7 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> { } } -/// Maps a `card_game::Card` to a stable `u32` identity used by `CardEntity` -/// and systems that still track cards by numeric ID. -/// Encoding: `suit_index * 13 + (rank as u8 - 1)`, range 0..=51. -fn card_to_id(card: &Card) -> u32 { - use solitaire_core::card::Suit; - let suit_index: u32 = match card.suit() { - Suit::Clubs => 0, - Suit::Diamonds => 1, - Suit::Hearts => 2, - Suit::Spades => 3, - }; - suit_index * 13 + (card.rank() as u32 - 1) -} +use solitaire_core::card::card_to_id; const fn foundations() -> [Foundation; 4] { [ diff --git a/solitaire_wasm/src/lib.rs b/solitaire_wasm/src/lib.rs index f6a814e..a06fc4d 100644 --- a/solitaire_wasm/src/lib.rs +++ b/solitaire_wasm/src/lib.rs @@ -87,18 +87,9 @@ pub struct CardSnapshot { pub face_up: bool, } -/// Stable 0..=51 identifier derived from suit and rank. Mirrors the desktop -/// engine's `card_to_id` so replay snapshots are identical across platforms — -/// `Card` itself carries no id field (suit + rank are unique within a deck). -fn card_to_id(card: &solitaire_core::card::Card) -> u32 { - let suit_index = match card.suit() { - Suit::Clubs => 0, - Suit::Diamonds => 1, - Suit::Hearts => 2, - Suit::Spades => 3, - }; - suit_index * 13 + (card.rank() as u32 - 1) -} +// Stable 0..=51 card identity, shared with the desktop engine via +// solitaire_core so replay snapshots are identical across platforms. +use solitaire_core::card::card_to_id; impl From<&(solitaire_core::card::Card, bool)> for CardSnapshot { fn from((card, face_up): &(solitaire_core::card::Card, bool)) -> Self {