diff --git a/assets/cards/faces/10C.png b/assets/cards/faces/10C.png index 65d5a31..1114dca 100644 Binary files a/assets/cards/faces/10C.png and b/assets/cards/faces/10C.png differ diff --git a/assets/cards/faces/10D.png b/assets/cards/faces/10D.png index 67df1b6..a10e9ac 100644 Binary files a/assets/cards/faces/10D.png and b/assets/cards/faces/10D.png differ diff --git a/assets/cards/faces/2C.png b/assets/cards/faces/2C.png index 69d9c0f..5b1e29c 100644 Binary files a/assets/cards/faces/2C.png and b/assets/cards/faces/2C.png differ diff --git a/assets/cards/faces/2D.png b/assets/cards/faces/2D.png index 1484651..c6e8409 100644 Binary files a/assets/cards/faces/2D.png and b/assets/cards/faces/2D.png differ diff --git a/assets/cards/faces/3C.png b/assets/cards/faces/3C.png index b8b39cd..3872ae6 100644 Binary files a/assets/cards/faces/3C.png and b/assets/cards/faces/3C.png differ diff --git a/assets/cards/faces/3D.png b/assets/cards/faces/3D.png index 387cf26..e31d47e 100644 Binary files a/assets/cards/faces/3D.png and b/assets/cards/faces/3D.png differ diff --git a/assets/cards/faces/4C.png b/assets/cards/faces/4C.png index 1cfd385..1986982 100644 Binary files a/assets/cards/faces/4C.png and b/assets/cards/faces/4C.png differ diff --git a/assets/cards/faces/4D.png b/assets/cards/faces/4D.png index 5ee8134..fe2431d 100644 Binary files a/assets/cards/faces/4D.png and b/assets/cards/faces/4D.png differ diff --git a/assets/cards/faces/5C.png b/assets/cards/faces/5C.png index e2127c9..77d048c 100644 Binary files a/assets/cards/faces/5C.png and b/assets/cards/faces/5C.png differ diff --git a/assets/cards/faces/5D.png b/assets/cards/faces/5D.png index fb974fb..69e6de7 100644 Binary files a/assets/cards/faces/5D.png and b/assets/cards/faces/5D.png differ diff --git a/assets/cards/faces/6C.png b/assets/cards/faces/6C.png index 36ac9a7..3b44b1a 100644 Binary files a/assets/cards/faces/6C.png and b/assets/cards/faces/6C.png differ diff --git a/assets/cards/faces/6D.png b/assets/cards/faces/6D.png index c57052c..9bd8f42 100644 Binary files a/assets/cards/faces/6D.png and b/assets/cards/faces/6D.png differ diff --git a/assets/cards/faces/7C.png b/assets/cards/faces/7C.png index fee30a8..16a9ee7 100644 Binary files a/assets/cards/faces/7C.png and b/assets/cards/faces/7C.png differ diff --git a/assets/cards/faces/7D.png b/assets/cards/faces/7D.png index 375f117..3941053 100644 Binary files a/assets/cards/faces/7D.png and b/assets/cards/faces/7D.png differ diff --git a/assets/cards/faces/8C.png b/assets/cards/faces/8C.png index 9def589..443f594 100644 Binary files a/assets/cards/faces/8C.png and b/assets/cards/faces/8C.png differ diff --git a/assets/cards/faces/8D.png b/assets/cards/faces/8D.png index 97592d2..0b44ae9 100644 Binary files a/assets/cards/faces/8D.png and b/assets/cards/faces/8D.png differ diff --git a/assets/cards/faces/9C.png b/assets/cards/faces/9C.png index d6fcd88..2e73df0 100644 Binary files a/assets/cards/faces/9C.png and b/assets/cards/faces/9C.png differ diff --git a/assets/cards/faces/9D.png b/assets/cards/faces/9D.png index 8822f3a..992b867 100644 Binary files a/assets/cards/faces/9D.png and b/assets/cards/faces/9D.png differ diff --git a/assets/cards/faces/AC.png b/assets/cards/faces/AC.png index 21db1d9..6f53fdc 100644 Binary files a/assets/cards/faces/AC.png and b/assets/cards/faces/AC.png differ diff --git a/assets/cards/faces/AD.png b/assets/cards/faces/AD.png index 41e1eac..b627ea7 100644 Binary files a/assets/cards/faces/AD.png and b/assets/cards/faces/AD.png differ diff --git a/assets/cards/faces/JC.png b/assets/cards/faces/JC.png index aee45c7..dea4920 100644 Binary files a/assets/cards/faces/JC.png and b/assets/cards/faces/JC.png differ diff --git a/assets/cards/faces/JD.png b/assets/cards/faces/JD.png index 7ada36a..c78d22c 100644 Binary files a/assets/cards/faces/JD.png and b/assets/cards/faces/JD.png differ diff --git a/assets/cards/faces/KC.png b/assets/cards/faces/KC.png index 5096c5d..3a0f4f8 100644 Binary files a/assets/cards/faces/KC.png and b/assets/cards/faces/KC.png differ diff --git a/assets/cards/faces/KD.png b/assets/cards/faces/KD.png index 68ba147..ac69212 100644 Binary files a/assets/cards/faces/KD.png and b/assets/cards/faces/KD.png differ diff --git a/assets/cards/faces/QC.png b/assets/cards/faces/QC.png index 9f06cfa..5a96614 100644 Binary files a/assets/cards/faces/QC.png and b/assets/cards/faces/QC.png differ diff --git a/assets/cards/faces/QD.png b/assets/cards/faces/QD.png index f076dea..e2e7366 100644 Binary files a/assets/cards/faces/QD.png and b/assets/cards/faces/QD.png differ diff --git a/docs/ui-mockups/design-system.md b/docs/ui-mockups/design-system.md index af58be3..e02e697 100644 --- a/docs/ui-mockups/design-system.md +++ b/docs/ui-mockups/design-system.md @@ -137,18 +137,22 @@ The palette is base16-eighties — a 16-slot terminal palette where indices 00 ## Suit Colors -**Two-color traditional mapping**, with mandatory color-blind support: +**Four-color deck**, with mandatory color-blind support. Each suit +picks up its own base16-eighties accent so a player scanning the +table can identify the suit by hue alone (faster than the +traditional 2-color red/black mapping; common in poker decks and +online card games): | Suit | Default | Color-blind mode | Glyph differentiation | |---|---|---|---| -| Hearts | `#fb9fb1` (pink) | `#acc267` (lime) | Solid filled glyph | -| Diamonds | `#fb9fb1` (pink) | `#acc267` (lime) | **Outlined glyph (1.5px stroke)** | -| Spades | `#d0d0d0` (foreground) | `#d0d0d0` | Solid filled glyph | -| Clubs | `#d0d0d0` (foreground) | `#d0d0d0` | **Outlined glyph (1.5px stroke)** | +| Hearts | `#fb9fb1` (pink, base08) | `#acc267` (lime) | Solid filled glyph | +| Diamonds | `#ddb26f` (gold, base09) | `#ddb26f` (unchanged) | **Outlined glyph (1.5px stroke)** | +| Spades | `#d0d0d0` (foreground, base05) | `#d0d0d0` (unchanged) | Solid filled glyph | +| Clubs | `#acc267` (lime, base0A) | `#acc267` (unchanged) | **Outlined glyph (1.5px stroke)** | The outlined-glyph treatment is the **primary** differentiation mechanism. Color is supplementary. This means a player viewing the game on a monochrome display, or with severe red-green deficiency, can still distinguish all four suits without context. This is a hard requirement, not an optional setting. -The "color-blind mode" toggle in Settings only swaps red→lime; it does not turn the outlined glyphs on or off, because outlined glyphs are always on. (Was red→cyan before the 2026-05-08 primary-accent swap; CBM moved to lime to stay hue-distinct from the new red-family primary.) +The "color-blind mode" toggle in Settings only swaps the heart suit colour from pink to lime; the other three suits (diamonds gold, clubs lime, spades gray) are already hue-distinct from pink and stay unchanged. The toggle does not turn the outlined glyphs on or off, because outlined glyphs are always on. Note: under CBM with the 4-colour deck, hearts and clubs share the lime hue — the always-on filled-vs-outlined glyph differentiation (♥ filled, ♣ outlined) keeps them readable. (Was red→cyan before the 2026-05-08 primary-accent swap; CBM moved to lime to stay hue-distinct from the new red-family primary.) ## Typography @@ -272,7 +276,7 @@ Top-right corner of the HUD: a 6px circular dot. ## Accessibility -1. **Color-blind mode** (Settings → Gameplay): swaps red suits' default `#fb9fb1` for `#acc267` (lime). Outlined-glyph differentiation remains active in *all* modes. +1. **Color-blind mode** (Settings → Gameplay): swaps the heart suit colour from `#fb9fb1` (pink) to `#acc267` (lime). The other three suits in the 4-colour deck (diamonds gold, clubs lime, spades gray) are already hue-distinct and stay unchanged. Outlined-glyph differentiation remains active in *all* modes. 2. **High-contrast mode** (Settings → Gameplay): boosts on-surface from `#d0d0d0` to `#f5f5f5`, outline from `#505050` to `#a0a0a0`, suit-red from `#fb9fb1` to `#ff8aa0`. 3. **Reduce-motion mode** (Settings → Gameplay): disables card-lift transition (instant z-lift), disables CRT scanline effect, disables the warning-chip pulse animation. 4. **Tabular figures** are mandatory for any number that updates live (timer, score, moves) so they don't reflow. diff --git a/solitaire_engine/assets/themes/default/clubs_10.svg b/solitaire_engine/assets/themes/default/clubs_10.svg index fc8f6d3..0875f0b 100644 --- a/solitaire_engine/assets/themes/default/clubs_10.svg +++ b/solitaire_engine/assets/themes/default/clubs_10.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 10 + fill="#acc267">10 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_2.svg b/solitaire_engine/assets/themes/default/clubs_2.svg index 4feaba0..19ba63d 100644 --- a/solitaire_engine/assets/themes/default/clubs_2.svg +++ b/solitaire_engine/assets/themes/default/clubs_2.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 2 + fill="#acc267">2 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_3.svg b/solitaire_engine/assets/themes/default/clubs_3.svg index 5d1bc87..bd88816 100644 --- a/solitaire_engine/assets/themes/default/clubs_3.svg +++ b/solitaire_engine/assets/themes/default/clubs_3.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 3 + fill="#acc267">3 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_4.svg b/solitaire_engine/assets/themes/default/clubs_4.svg index 8667f81..a8a2e68 100644 --- a/solitaire_engine/assets/themes/default/clubs_4.svg +++ b/solitaire_engine/assets/themes/default/clubs_4.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 4 + fill="#acc267">4 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_5.svg b/solitaire_engine/assets/themes/default/clubs_5.svg index 386785a..70c887f 100644 --- a/solitaire_engine/assets/themes/default/clubs_5.svg +++ b/solitaire_engine/assets/themes/default/clubs_5.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 5 + fill="#acc267">5 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_6.svg b/solitaire_engine/assets/themes/default/clubs_6.svg index 2f9f911..1d62ad4 100644 --- a/solitaire_engine/assets/themes/default/clubs_6.svg +++ b/solitaire_engine/assets/themes/default/clubs_6.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 6 + fill="#acc267">6 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_7.svg b/solitaire_engine/assets/themes/default/clubs_7.svg index 54319f3..95581a4 100644 --- a/solitaire_engine/assets/themes/default/clubs_7.svg +++ b/solitaire_engine/assets/themes/default/clubs_7.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 7 + fill="#acc267">7 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_8.svg b/solitaire_engine/assets/themes/default/clubs_8.svg index 55d1faf..b27d0da 100644 --- a/solitaire_engine/assets/themes/default/clubs_8.svg +++ b/solitaire_engine/assets/themes/default/clubs_8.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 8 + fill="#acc267">8 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_9.svg b/solitaire_engine/assets/themes/default/clubs_9.svg index c5129c6..d5826c0 100644 --- a/solitaire_engine/assets/themes/default/clubs_9.svg +++ b/solitaire_engine/assets/themes/default/clubs_9.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> 9 + fill="#acc267">9 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_ace.svg b/solitaire_engine/assets/themes/default/clubs_ace.svg index cf2383c..72f1c58 100644 --- a/solitaire_engine/assets/themes/default/clubs_ace.svg +++ b/solitaire_engine/assets/themes/default/clubs_ace.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> A + fill="#acc267">A - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_jack.svg b/solitaire_engine/assets/themes/default/clubs_jack.svg index a656b90..d55b92d 100644 --- a/solitaire_engine/assets/themes/default/clubs_jack.svg +++ b/solitaire_engine/assets/themes/default/clubs_jack.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> J + fill="#acc267">J - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_king.svg b/solitaire_engine/assets/themes/default/clubs_king.svg index 6f979d8..b654e92 100644 --- a/solitaire_engine/assets/themes/default/clubs_king.svg +++ b/solitaire_engine/assets/themes/default/clubs_king.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> K + fill="#acc267">K - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/clubs_queen.svg b/solitaire_engine/assets/themes/default/clubs_queen.svg index 8437177..ba540fc 100644 --- a/solitaire_engine/assets/themes/default/clubs_queen.svg +++ b/solitaire_engine/assets/themes/default/clubs_queen.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#acc267" stroke-width="2"/> Q + fill="#acc267">Q - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_10.svg b/solitaire_engine/assets/themes/default/diamonds_10.svg index a88bc5a..60ab29c 100644 --- a/solitaire_engine/assets/themes/default/diamonds_10.svg +++ b/solitaire_engine/assets/themes/default/diamonds_10.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 10 + fill="#ddb26f">10 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_2.svg b/solitaire_engine/assets/themes/default/diamonds_2.svg index c7ee305..371b467 100644 --- a/solitaire_engine/assets/themes/default/diamonds_2.svg +++ b/solitaire_engine/assets/themes/default/diamonds_2.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 2 + fill="#ddb26f">2 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_3.svg b/solitaire_engine/assets/themes/default/diamonds_3.svg index 39dfdf5..6a1999b 100644 --- a/solitaire_engine/assets/themes/default/diamonds_3.svg +++ b/solitaire_engine/assets/themes/default/diamonds_3.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 3 + fill="#ddb26f">3 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_4.svg b/solitaire_engine/assets/themes/default/diamonds_4.svg index ce5b94e..2c48b73 100644 --- a/solitaire_engine/assets/themes/default/diamonds_4.svg +++ b/solitaire_engine/assets/themes/default/diamonds_4.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 4 + fill="#ddb26f">4 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_5.svg b/solitaire_engine/assets/themes/default/diamonds_5.svg index c05ad81..7b07f42 100644 --- a/solitaire_engine/assets/themes/default/diamonds_5.svg +++ b/solitaire_engine/assets/themes/default/diamonds_5.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 5 + fill="#ddb26f">5 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_6.svg b/solitaire_engine/assets/themes/default/diamonds_6.svg index de3af82..ddce01a 100644 --- a/solitaire_engine/assets/themes/default/diamonds_6.svg +++ b/solitaire_engine/assets/themes/default/diamonds_6.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 6 + fill="#ddb26f">6 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_7.svg b/solitaire_engine/assets/themes/default/diamonds_7.svg index eee758f..cac2f21 100644 --- a/solitaire_engine/assets/themes/default/diamonds_7.svg +++ b/solitaire_engine/assets/themes/default/diamonds_7.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 7 + fill="#ddb26f">7 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_8.svg b/solitaire_engine/assets/themes/default/diamonds_8.svg index 3fecfaa..25abc8f 100644 --- a/solitaire_engine/assets/themes/default/diamonds_8.svg +++ b/solitaire_engine/assets/themes/default/diamonds_8.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 8 + fill="#ddb26f">8 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_9.svg b/solitaire_engine/assets/themes/default/diamonds_9.svg index a1ea29b..c658ff9 100644 --- a/solitaire_engine/assets/themes/default/diamonds_9.svg +++ b/solitaire_engine/assets/themes/default/diamonds_9.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> 9 + fill="#ddb26f">9 - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_ace.svg b/solitaire_engine/assets/themes/default/diamonds_ace.svg index 969cefa..686a878 100644 --- a/solitaire_engine/assets/themes/default/diamonds_ace.svg +++ b/solitaire_engine/assets/themes/default/diamonds_ace.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> A + fill="#ddb26f">A - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_jack.svg b/solitaire_engine/assets/themes/default/diamonds_jack.svg index ef7bba1..b96c4ce 100644 --- a/solitaire_engine/assets/themes/default/diamonds_jack.svg +++ b/solitaire_engine/assets/themes/default/diamonds_jack.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> J + fill="#ddb26f">J - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_king.svg b/solitaire_engine/assets/themes/default/diamonds_king.svg index 6bb9fad..39f96a2 100644 --- a/solitaire_engine/assets/themes/default/diamonds_king.svg +++ b/solitaire_engine/assets/themes/default/diamonds_king.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> K + fill="#ddb26f">K - + - + \ No newline at end of file diff --git a/solitaire_engine/assets/themes/default/diamonds_queen.svg b/solitaire_engine/assets/themes/default/diamonds_queen.svg index e3a1bf9..358021a 100644 --- a/solitaire_engine/assets/themes/default/diamonds_queen.svg +++ b/solitaire_engine/assets/themes/default/diamonds_queen.svg @@ -1,18 +1,18 @@ + fill="#1a1a1a" stroke="#ddb26f" stroke-width="2"/> Q + fill="#ddb26f">Q - + - + \ No newline at end of file diff --git a/solitaire_engine/src/assets/card_face_svg.rs b/solitaire_engine/src/assets/card_face_svg.rs index 41d7e07..3133437 100644 --- a/solitaire_engine/src/assets/card_face_svg.rs +++ b/solitaire_engine/src/assets/card_face_svg.rs @@ -29,8 +29,19 @@ use solitaire_core::card::{Rank, Suit}; pub const TARGET: UVec2 = UVec2::new(256, 384); const BG_FACE: &str = "#1a1a1a"; // BG_ELEVATED — face background -const SUIT_RED: &str = "#fb9fb1"; // hearts + diamonds -const SUIT_DARK: &str = "#d0d0d0"; // spades + clubs (also TEXT_PRIMARY) + +// Four-colour deck: each suit picks up its own base16-eighties +// accent so a player scanning the table can distinguish the suit +// by hue alone. Faster recognition than the 2-colour traditional +// red/black scheme; common in poker-room decks and online card +// games. The outlined-glyph differentiation (♦ ♣ outlined, ♥ ♠ +// filled) is preserved on top of the colour split as the +// always-on colour-blind fallback per `design-system.md` +// §Accessibility. +const SUIT_HEART: &str = "#fb9fb1"; // pink (base08 / RED_SUIT_COLOUR) +const SUIT_DIAMOND: &str = "#ddb26f"; // gold (base09 / STATE_WARNING) +const SUIT_CLUB: &str = "#acc267"; // lime (base0A / STATE_SUCCESS) +const SUIT_SPADE: &str = "#d0d0d0"; // foreground gray (base05 / TEXT_PRIMARY) const BACK_BG: &str = "#151515"; const BACK_SCANLINE: &str = "#1a1a1a"; @@ -145,10 +156,10 @@ enum GlyphPaint { fn suit_paint(suit: Suit) -> (&'static str, GlyphPaint) { match suit { - Suit::Hearts => (SUIT_RED, GlyphPaint::Filled), - Suit::Diamonds => (SUIT_RED, GlyphPaint::Outlined), - Suit::Spades => (SUIT_DARK, GlyphPaint::Filled), - Suit::Clubs => (SUIT_DARK, GlyphPaint::Outlined), + Suit::Hearts => (SUIT_HEART, GlyphPaint::Filled), + Suit::Diamonds => (SUIT_DIAMOND, GlyphPaint::Outlined), + Suit::Spades => (SUIT_SPADE, GlyphPaint::Filled), + Suit::Clubs => (SUIT_CLUB, GlyphPaint::Outlined), } } diff --git a/solitaire_engine/src/card_plugin.rs b/solitaire_engine/src/card_plugin.rs index a30e109..cdb1506 100644 --- a/solitaire_engine/src/card_plugin.rs +++ b/solitaire_engine/src/card_plugin.rs @@ -63,7 +63,13 @@ const FONT_SIZE_FRAC: f32 = 0.28; /// Card-face background — Terminal `#1a1a1a` (BG_ELEVATED). pub const CARD_FACE_COLOUR: Color = Color::srgb(0.102, 0.102, 0.102); -/// Suit colour for hearts + diamonds — Terminal `#fb9fb1` (suit-pink). +/// Suit colour for hearts — Terminal `#fb9fb1` (suit-pink). Per +/// the 4-colour deck convention, hearts and diamonds no longer +/// share a hue: hearts stay pink (the most strongly "red" of the +/// four base16 accents), diamonds pick up gold so a player +/// scanning the table can distinguish the suit by colour alone. +/// Kept as `RED_SUIT_COLOUR` for back-compat; semantically this +/// is now "the heart suit colour". pub const RED_SUIT_COLOUR: Color = Color::srgb(0.984, 0.624, 0.694); /// High-contrast variant of [`RED_SUIT_COLOUR`] — `#ff8aa0`. Lifted /// chroma + luminance for the Settings → Accessibility → High- @@ -76,7 +82,19 @@ pub const RED_SUIT_COLOUR: Color = Color::srgb(0.984, 0.624, 0.694); /// alternative. The two modes can stack; CBM wins when both are on /// because the CBM lime is itself a high-contrast colour. pub const RED_SUIT_COLOUR_HC: Color = Color::srgb(1.000, 0.541, 0.627); -/// Suit colour for spades + clubs — Terminal `#d0d0d0` (TEXT_PRIMARY). +/// Suit colour for diamonds — Terminal `#ddb26f` (gold, base09). +/// In the 4-colour deck split, diamonds break away from sharing +/// the "red" hue with hearts and pick up gold so the two former +/// red suits are visually distinguishable. +pub const DIAMOND_SUIT_COLOUR: Color = Color::srgb(0.867, 0.698, 0.435); +/// Suit colour for clubs — Terminal `#acc267` (lime, base0A). +/// In the 4-colour deck split, clubs break away from sharing the +/// "black" hue with spades and pick up lime so the two former +/// black suits are visually distinguishable. +pub const CLUB_SUIT_COLOUR: Color = Color::srgb(0.675, 0.761, 0.404); +/// Suit colour for spades — Terminal `#d0d0d0` (TEXT_PRIMARY). +/// Kept as `BLACK_SUIT_COLOUR` for back-compat; semantically this +/// is now "the spade suit colour" in the 4-colour deck split. pub const BLACK_SUIT_COLOUR: Color = Color::srgb(0.816, 0.816, 0.816); /// Pre-loaded [`Handle`]s for card face and back PNG textures. @@ -813,37 +831,58 @@ fn label_for(card: &Card) -> String { /// Suit colour for the rank/suit overlay rendered atop the constant /// fallback sprite (only fires under `MinimalPlugins` — production -/// renders the suit glyph baked into the PNG). Two independent -/// accessibility flags compose: +/// renders the suit glyph baked into the PNG). The 4-colour deck +/// split assigns each suit a distinct base16-eighties accent so +/// players can identify the suit by hue alone: /// -/// - `color_blind`: red-suit cards swap to `RED_SUIT_COLOUR_CBM` -/// (lime) — the "Settings toggle swaps red→lime" half of the -/// design system's colour-blind support. CBM is a hue-replacement -/// for red, so HC has no further effect on red when CBM is on -/// (the lime is itself a high-contrast colour). -/// - `high_contrast`: when CBM is off, red suits boost to -/// `RED_SUIT_COLOUR_HC` (`#ff8aa0` from the spec); black suits -/// boost from `#d0d0d0` to `#f5f5f5` (`TEXT_PRIMARY_HC`). +/// | Suit | Default colour | CBM swap | HC boost | +/// | -------- | ----------------------- | ------------- | ---------------- | +/// | Hearts | `RED_SUIT_COLOUR` pink | lime | `RED_SUIT_COLOUR_HC` | +/// | Diamonds | `DIAMOND_SUIT_COLOUR` gold | (no swap — already non-red-family) | (no boost — already mid-luminance) | +/// | Clubs | `CLUB_SUIT_COLOUR` lime | (no swap) | (no boost) | +/// | Spades | `BLACK_SUIT_COLOUR` gray | (no swap) | `TEXT_PRIMARY_HC` | +/// +/// Two independent accessibility flags compose: +/// +/// - `color_blind`: hearts swap from pink to `RED_SUIT_COLOUR_CBM` +/// (lime). The other three suits are already hue-distinct from +/// pink so they don't change. Note that CBM lime collides with +/// the club suit colour — players running CBM on the 4-colour +/// deck rely on the always-on filled-vs-outlined differentiation +/// (♥ filled, ♣ outlined) to distinguish hearts and clubs. +/// - `high_contrast`: hearts boost to `RED_SUIT_COLOUR_HC` +/// (`#ff8aa0`); spades boost from `#d0d0d0` to `#f5f5f5` +/// (`TEXT_PRIMARY_HC`). Diamonds (gold) and clubs (lime) are +/// already mid-luminance accents so no HC boost is defined for +/// them. /// /// The other half of CBM support (always-on filled-vs-outlined /// glyph differentiation for ♥♠ vs ♦♣) is baked into the PNG art /// and has no constant-fallback equivalent. fn text_colour(card: &Card, color_blind: bool, high_contrast: bool) -> Color { - if card.suit.is_red() { - if color_blind { - // CBM lime wins — the colour-blind swap replaces the - // red hue entirely, and the lime is already high- - // luminance, so an HC boost on top has nothing to do. - RED_SUIT_COLOUR_CBM - } else if high_contrast { - RED_SUIT_COLOUR_HC - } else { - RED_SUIT_COLOUR + match card.suit { + Suit::Hearts => { + if color_blind { + // CBM lime replaces the pink for red-deficient + // readers; the always-on filled-vs-outlined split + // keeps hearts visually distinct from clubs (which + // are also lime in the 4-colour scheme). + RED_SUIT_COLOUR_CBM + } else if high_contrast { + RED_SUIT_COLOUR_HC + } else { + RED_SUIT_COLOUR + } + } + Suit::Diamonds => DIAMOND_SUIT_COLOUR, + Suit::Clubs => CLUB_SUIT_COLOUR, + Suit::Spades => { + if high_contrast { + TEXT_PRIMARY_HC + } else { + BLACK_SUIT_COLOUR + } } - } else if high_contrast { - TEXT_PRIMARY_HC - } else { - BLACK_SUIT_COLOUR } } @@ -1784,39 +1823,41 @@ mod tests { } #[test] - fn text_colour_is_red_for_hearts_and_diamonds() { - let h = Card { - id: 0, - suit: Suit::Hearts, - rank: Rank::Ace, - face_up: true, - }; - let d = Card { - id: 0, - suit: Suit::Diamonds, - rank: Rank::Ace, - face_up: true, - }; - assert_eq!(text_colour(&h, false, false), RED_SUIT_COLOUR); - assert_eq!(text_colour(&d, false, false), RED_SUIT_COLOUR); - } + fn text_colour_4_colour_deck_assigns_each_suit_its_own_hue() { + // Pre-4-colour-deck this was two tests asserting hearts + + // diamonds shared `RED_SUIT_COLOUR` and clubs + spades + // shared `BLACK_SUIT_COLOUR`. The 4-colour split breaks + // both pairings: hearts pink, diamonds gold, clubs lime, + // spades gray. All four colours must be distinct so a + // player scanning the table can identify the suit by hue + // alone. + let h = Card { id: 0, suit: Suit::Hearts, rank: Rank::Ace, face_up: true }; + let d = Card { id: 0, suit: Suit::Diamonds, rank: Rank::Ace, face_up: true }; + let c = Card { id: 0, suit: Suit::Clubs, rank: Rank::Ace, face_up: true }; + let s = Card { id: 0, suit: Suit::Spades, rank: Rank::Ace, face_up: true }; - #[test] - fn text_colour_is_black_for_clubs_and_spades() { - let c = Card { - id: 0, - suit: Suit::Clubs, - rank: Rank::Ace, - face_up: true, - }; - let s = Card { - id: 0, - suit: Suit::Spades, - rank: Rank::Ace, - face_up: true, - }; - assert_eq!(text_colour(&c, false, false), BLACK_SUIT_COLOUR); + assert_eq!(text_colour(&h, false, false), RED_SUIT_COLOUR); + assert_eq!(text_colour(&d, false, false), DIAMOND_SUIT_COLOUR); + assert_eq!(text_colour(&c, false, false), CLUB_SUIT_COLOUR); assert_eq!(text_colour(&s, false, false), BLACK_SUIT_COLOUR); + + // All four hues must be pairwise distinct — the visual + // identification gain of a 4-colour deck depends on hue + // separation, so this is the load-bearing invariant. + let colours = [ + text_colour(&h, false, false), + text_colour(&d, false, false), + text_colour(&c, false, false), + text_colour(&s, false, false), + ]; + for i in 0..colours.len() { + for j in (i + 1)..colours.len() { + assert_ne!( + colours[i], colours[j], + "4-colour deck requires every suit to have a distinct colour", + ); + } + } } #[test] @@ -2108,27 +2149,35 @@ mod tests { // ----------------------------------------------------------------------- #[test] - fn text_colour_color_blind_mode_swaps_red_suits_to_lime() { - let red_card = Card { id: 0, suit: Suit::Diamonds, rank: Rank::Queen, face_up: true }; - let cbm_colour = text_colour(&red_card, true, false); + fn text_colour_color_blind_mode_swaps_hearts_to_lime() { + // Pre-4-colour-deck this test asserted both red suits + // (hearts + diamonds) swapped to lime under CBM. With + // hearts the only "red-family" suit now, CBM only + // affects hearts; diamonds is gold and stays gold. + let hearts = Card { id: 0, suit: Suit::Hearts, rank: Rank::Queen, face_up: true }; + let cbm_colour = text_colour(&hearts, true, false); assert_eq!( cbm_colour, RED_SUIT_COLOUR_CBM, - "color-blind mode must replace the red suit colour with the CBM lime", + "color-blind mode must replace the heart suit colour with the CBM lime", ); assert_ne!( cbm_colour, RED_SUIT_COLOUR, - "CBM lime must be visibly distinct from the default red suit colour", + "CBM lime must be visibly distinct from the default heart suit colour", ); } #[test] - fn text_colour_color_blind_mode_does_not_change_black_suits() { - let black_card = Card { id: 0, suit: Suit::Clubs, rank: Rank::Jack, face_up: true }; - assert_eq!( - text_colour(&black_card, true, false), - BLACK_SUIT_COLOUR, - "color-blind mode must not alter black-suit text colour", - ); + fn text_colour_color_blind_mode_does_not_change_non_heart_suits() { + // CBM only affects hearts in the 4-colour deck. Diamonds + // (gold), clubs (lime), and spades (gray) are already + // hue-distinct from the original heart pink and stay + // unchanged. + let d = Card { id: 0, suit: Suit::Diamonds, rank: Rank::Jack, face_up: true }; + let c = Card { id: 0, suit: Suit::Clubs, rank: Rank::Jack, face_up: true }; + let s = Card { id: 0, suit: Suit::Spades, rank: Rank::Jack, face_up: true }; + assert_eq!(text_colour(&d, true, false), DIAMOND_SUIT_COLOUR); + assert_eq!(text_colour(&c, true, false), CLUB_SUIT_COLOUR); + assert_eq!(text_colour(&s, true, false), BLACK_SUIT_COLOUR); } // ----------------------------------------------------------------------- @@ -2168,31 +2217,48 @@ mod tests { } #[test] - fn text_colour_color_blind_wins_over_high_contrast_on_red_suits() { - // When both modes are enabled, red→lime (CBM) wins because - // the CBM lime is itself a high-luminance accent and the HC - // boost would pick a different hue, defeating the purpose of - // the colour-blind swap. - let red_card = Card { id: 0, suit: Suit::Diamonds, rank: Rank::Ace, face_up: true }; + fn text_colour_color_blind_wins_over_high_contrast_on_hearts() { + // When both modes are enabled on hearts, CBM lime wins over + // HC red because the CBM lime is itself a high-luminance + // accent and the HC boost would pick a different hue, + // defeating the purpose of the colour-blind swap. + let hearts = Card { id: 0, suit: Suit::Hearts, rank: Rank::Ace, face_up: true }; assert_eq!( - text_colour(&red_card, true, true), + text_colour(&hearts, true, true), RED_SUIT_COLOUR_CBM, "CBM lime must win over HC red when both modes are on", ); } #[test] - fn text_colour_high_contrast_alone_does_not_change_black_suits_under_cbm() { - // CBM doesn't touch black suits, so HC remains the only - // source of variation for the black row when both are on. - let black_card = Card { id: 0, suit: Suit::Clubs, rank: Rank::King, face_up: true }; + fn text_colour_high_contrast_alone_boosts_spades_under_cbm() { + // CBM doesn't touch spades, so HC remains the only source + // of variation for the spade row when both are on. (Pre- + // 4-colour-deck this test used Clubs; in the 4-colour + // scheme clubs is lime not gray, so the assertion shifted + // to the only suit that's still gray-family.) + let spades = Card { id: 0, suit: Suit::Spades, rank: Rank::King, face_up: true }; assert_eq!( - text_colour(&black_card, true, true), + text_colour(&spades, true, true), TEXT_PRIMARY_HC, - "with CBM + HC both on, black suits still pick up the HC boost", + "with CBM + HC both on, spades still pick up the HC boost", ); } + #[test] + fn text_colour_diamonds_and_clubs_are_immune_to_accessibility_flags() { + // Diamonds (gold) and clubs (lime) are already mid-luminance + // hue-distinct accents, so neither CBM nor HC has a defined + // boost for them. Verify all four flag combinations leave + // them at their default suit colour. + let d = Card { id: 0, suit: Suit::Diamonds, rank: Rank::King, face_up: true }; + let c = Card { id: 0, suit: Suit::Clubs, rank: Rank::King, face_up: true }; + for (cbm, hc) in [(false, false), (false, true), (true, false), (true, true)] { + assert_eq!(text_colour(&d, cbm, hc), DIAMOND_SUIT_COLOUR); + assert_eq!(text_colour(&c, cbm, hc), CLUB_SUIT_COLOUR); + } + } + // ----------------------------------------------------------------------- // label_visibility (pure) // ----------------------------------------------------------------------- diff --git a/solitaire_engine/tests/card_face_svg_pin.rs b/solitaire_engine/tests/card_face_svg_pin.rs index 3768e3b..377f326 100644 --- a/solitaire_engine/tests/card_face_svg_pin.rs +++ b/solitaire_engine/tests/card_face_svg_pin.rs @@ -24,32 +24,32 @@ use solitaire_engine::assets::card_face_svg::{ use solitaire_engine::assets::rasterize_svg; const EXPECTED: &[(&str, u64)] = &[ - ("face_AC", 0xdac8c6f869cea53c), - ("face_2C", 0x8976454d1919bfdb), - ("face_3C", 0x0eda320371ca2d3f), - ("face_4C", 0x2e921081296553c9), - ("face_5C", 0xdb574a322d615af0), - ("face_6C", 0xad93daa160b5e7fa), - ("face_7C", 0xa3cdae097cb23271), - ("face_8C", 0x7b652bc9f0a5940b), - ("face_9C", 0xb5b274c80f319b85), - ("face_10C", 0x2ed8324f84c443cd), - ("face_JC", 0x3d9bc380e83d7611), - ("face_QC", 0xacad01ad4053a396), - ("face_KC", 0xba575aa772fc2e3e), - ("face_AD", 0xe1049b5a7d2c110c), - ("face_2D", 0x58f2a7e60a5cfff9), - ("face_3D", 0x89aeece03e7afe0b), - ("face_4D", 0xb97dd2633958d6ba), - ("face_5D", 0x32b57300e16c5b30), - ("face_6D", 0xd617e851d97f4a7d), - ("face_7D", 0xdd2da9b2457bfded), - ("face_8D", 0xfe00cf683015f30b), - ("face_9D", 0x7188b0fade3d086a), - ("face_10D", 0x53d0db517868e1f7), - ("face_JD", 0xeb2c6a0192146258), - ("face_QD", 0x36edafbbc3d34f0a), - ("face_KD", 0x1bbfa8b1176ee3ac), + ("face_AC", 0x287e3293f95990a5), + ("face_2C", 0x01c66d8e461fb0c4), + ("face_3C", 0xfdae6be53af8b7c8), + ("face_4C", 0x4b2a7aef966c6cc2), + ("face_5C", 0xa4ca0ce3759b5cc9), + ("face_6C", 0xe1a730d1ce810314), + ("face_7C", 0x9c8de5c7d014eca3), + ("face_8C", 0x39e09f90c957b192), + ("face_9C", 0xd6627707fb2d5079), + ("face_10C", 0xbe8411c60411195c), + ("face_JC", 0x7c33abf5619477ac), + ("face_QC", 0xe75657d63c99a892), + ("face_KC", 0xf4a445b771026496), + ("face_AD", 0xad8820c694c464d7), + ("face_2D", 0xef771dbb39ae4f5a), + ("face_3D", 0xe955ec9a96e1256a), + ("face_4D", 0x6bb5979ef6004957), + ("face_5D", 0x55715fd2353b2126), + ("face_6D", 0x87fbd6efce1b1f9f), + ("face_7D", 0xabb2d52d363e93ab), + ("face_8D", 0xde78161ee9093b05), + ("face_9D", 0x1475987ba1e66036), + ("face_10D", 0x3a52d7fda7158aeb), + ("face_JD", 0xc9078d8a7b2e6372), + ("face_QD", 0x84c9011b916fdbe8), + ("face_KD", 0xbcd20dbb6b1c8cdf), ("face_AH", 0x2c8e05964b5e3a5f), ("face_2H", 0xb44e68b79bb3842e), ("face_3H", 0x15226ed29769e1c4),