Files
Ferrous-Solitaire/solitaire_engine/tests/card_face_svg_pin.rs
T
funman300 a292a7ead0 feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242
Project-wide palette shift at user request. Replaces the cyan
primary accent everywhere it surfaces — splash boot screen,
home menu glyphs, action chevrons, replay overlay banner +
scrub fill + chip border, achievement checkmarks, leaderboard
#1 indicator, radial menu fill, focus ring, card-back canonical
badge, etc. — with `#a54242` from the same base16-eighties
family as the existing pink suit colour.

Knock-on changes that all land in this commit per the
lockstep rule:

- ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER
  (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85
  alpha). Module-level palette comment + STOCK_BADGE_FG +
  CARD_SHADOW_ALPHA_DRAG doc strings updated to match.
- card_plugin.rs: card_back_colour(0) now returns the brick-red
  ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from
  cyan to lime #acc267 — the CBM alternative needs to stay
  hue-distinct from the new red-family primary, lime is the
  next-best non-red base16-eighties accent. text_colour doc
  + CBM tests renamed cyan→lime in lockstep
  (text_colour_color_blind_mode_swaps_red_suits_to_lime).
- card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical
  Terminal back).
- splash_plugin.rs / ui_modal.rs / replay_overlay.rs /
  selection_plugin.rs: descriptive "cyan" comments swapped to
  "accent" / "primary-accent" wording so the doc strings stay
  decoupled from any specific hue. Future palette tweaks won't
  require comment churn.
- design-system.md: YAML token frontmatter updated (primary,
  surface-tint, suit-red-cb, primary-container,
  on-primary-container, inverse-primary). Palette table gains
  a project-specific `base08` slot for the new red. CTA /
  Selection / Card-back badge / Primary button / Bottom-bar
  active-icon / glow / CBM swap text all retuned. Historical
  references preserved (e.g. "Was cyan #6fc2ef before the
  2026-05-08 swap") so the audit trail stays in the spec.
- card_face_svg_pin.rs: rebaselined. Exactly one hash drift
  (back_0 — the canonical Terminal back's badge changed
  colour). Other 56 hashes identical (face SVGs don't
  reference the accent; back_1..4 use unchanged accents). The
  one-hash-drift signal confirms the change scope was
  surgical.

Workspace clippy + cargo test --workspace clean, 1184 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:30:35 -07:00

176 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Pinning test for the Terminal card-face SVG builders.
//!
//! Hashes the raw RGBA8 pixel bytes produced by rasterising every
//! `face_svg` × `back_svg` output through `assets::rasterize_svg`,
//! and compares each hash to an embedded constant. Catches silent
//! rendering drift if `usvg`, `resvg`, `tiny_skia`, or the bundled
//! `FiraMono` font ever change in a way that perturbs the rendered
//! pixels.
//!
//! When the SVG builders intentionally change (or a dependency
//! upgrade legitimately changes rendering), update `EXPECTED` by
//! emptying it (`&[]`) and re-running this test once — the test
//! will panic with the new hashes formatted as Rust source ready
//! to paste back in.
//!
//! Hashing is FNV-1a 64-bit on the raw RGBA byte buffer. PNG
//! compression is intentionally not in the loop — we only want
//! the test to fire on actual pixel changes, not zlib-level
//! shifts that don't affect what a player sees.
use solitaire_engine::assets::card_face_svg::{
back_svg, face_svg, rank_filename, suit_filename, ALL_RANKS, ALL_SUITS, BACK_ACCENTS, TARGET,
};
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_AH", 0x2c8e05964b5e3a5f),
("face_2H", 0xb44e68b79bb3842e),
("face_3H", 0x15226ed29769e1c4),
("face_4H", 0xe28c86ba92a3aee9),
("face_5H", 0x18276e48b28d0f6b),
("face_6H", 0xcca5e60e65724eaa),
("face_7H", 0x7f3eee634137f13a),
("face_8H", 0x8974515a8904d6c4),
("face_9H", 0x2f8155cd7690d4b9),
("face_10H", 0x78142f898fd66578),
("face_JH", 0x5e6df78654a1de73),
("face_QH", 0xc231ae8c25d877a9),
("face_KH", 0x55a0a772baf3e97f),
("face_AS", 0xc90e798aebdc1c5f),
("face_2S", 0x4178c699a726ea70),
("face_3S", 0xdfcd34480bb06f4c),
("face_4S", 0xdbd4938042afb02e),
("face_5S", 0x8741456ab1ec58ab),
("face_6S", 0x6d2632f648f1c34d),
("face_7S", 0x3c05c70ff3d93ea6),
("face_8S", 0x12d7f456efbaffe0),
("face_9S", 0x11b6ade208b8fa12),
("face_10S", 0x475d4110834b6b2a),
("face_JS", 0x52525a2200c07246),
("face_QS", 0xb4f0251a2757cbb1),
("face_KS", 0x1e1975919bb9a029),
("back_0", 0xfd1742ebe330481a),
("back_1", 0x446fdc0a3c83a03a),
("back_2", 0xcf188fdec9f5819a),
("back_3", 0xcaffd02af141743a),
("back_4", 0xcee8a700bbaaf71a),
];
#[test]
fn rasterised_card_bytes_match_pinned_hashes() {
let actual = compute_actual_hashes();
if EXPECTED.is_empty() {
panic_with_hashes_to_paste(&actual);
}
assert_eq!(
actual.len(),
EXPECTED.len(),
"card-output count drifted (actual {} vs expected {})",
actual.len(),
EXPECTED.len(),
);
let mut mismatches: Vec<String> = Vec::new();
for ((actual_name, actual_hash), (expected_name, expected_hash)) in
actual.iter().zip(EXPECTED.iter())
{
assert_eq!(
actual_name, expected_name,
"card-output naming/order drifted",
);
if actual_hash != expected_hash {
mismatches.push(format!(
" {actual_name}: actual 0x{actual_hash:016x} expected 0x{expected_hash:016x}",
));
}
}
if !mismatches.is_empty() {
let mut msg = String::from(
"rasterised card bytes drifted from EXPECTED — usvg/resvg/tiny_skia/font upgrade?\n",
);
for m in &mismatches {
msg.push_str(m);
msg.push('\n');
}
msg.push_str(
"\nIf this drift is intentional, replace EXPECTED with `&[]` and re-run\nthis test to print fresh hashes.\n",
);
panic!("{msg}");
}
}
fn compute_actual_hashes() -> Vec<(String, u64)> {
let mut out = Vec::with_capacity(ALL_RANKS.len() * ALL_SUITS.len() + BACK_ACCENTS.len());
for suit in ALL_SUITS {
for rank in ALL_RANKS {
let name = format!("face_{}{}", rank_filename(rank), suit_filename(suit));
out.push((name, hash_rasterised(&face_svg(rank, suit))));
}
}
for (idx, accent) in BACK_ACCENTS.iter().enumerate() {
out.push((format!("back_{idx}"), hash_rasterised(&back_svg(accent))));
}
out
}
fn hash_rasterised(svg: &str) -> u64 {
let image = rasterize_svg(svg.as_bytes(), TARGET).expect("rasterise card SVG");
let bytes = image.data.expect("rasterised image carries RGBA pixel data");
fnv1a(&bytes)
}
/// FNV-1a 64-bit. Inline rather than a dependency — adding `sha2`
/// or `blake3` for ~5 lines of code would burn a CLAUDE.md §8
/// "ask before adding deps" round-trip for no real benefit.
/// Cryptographic strength isn't load-bearing here — we just need
/// stable byte fingerprints.
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn panic_with_hashes_to_paste(actual: &[(String, u64)]) -> ! {
let mut out = String::from(
"\nEXPECTED is empty — paste the following into the const literal:\n\nconst EXPECTED: &[(&str, u64)] = &[\n",
);
for (name, hash) in actual {
out.push_str(&format!(" (\"{name}\", 0x{hash:016x}),\n"));
}
out.push_str("];\n");
panic!("{out}");
}