Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
7.0 kB · 197 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198//! Color themes + the accessibility contract (docs/05 §5.5; fog strata//! §5.1, readability laws §5.4).//!//! Fog color shifts by stratum (docs/05 §5.1): the Shallows fade to a warm//! brown-black, the Works to a teal-black ozone, the Deep to a violet-black//! with drifting motes. Each [`Palette`] carries those three fog colors plus//! a `no_flicker` flag that clamps the wand muzzle-flash whiteout to a//! brightness-safe pulse parent-side (docs/05 §5.1 "Light as truth").//!//! [`threat_loot_exit_redundant`] is the machine-checked half of the//! three-glance rule (docs/05 §5.4 law 1): threat, loot, and exit are never//! distinguished by hue alone.//!//! Parent-unification notes (parallel workstream `ray.rs` / `shade.rs`)://! - `shade.rs` must multiply material truecolor by the active palette's//! per-stratum fog color at the fog-density step (docs/05 §5.2 step 3).//! - The `no_flicker` clamp itself lives parent-side (flash envelope on the//! light multiply), this module only carries the flag.
/// A color theme: per-stratum fog colors + flicker policy (docs/05 §5.5).#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub struct Palette { /// Stable theme name (see [`ALL`]). pub name: &'static str, /// Shallows fog: warm brown-black (docs/05 §5.1). pub fog_shallows: [u8; 3], /// Works fog: teal-black ozone (docs/05 §5.1). pub fog_works: [u8; 3], /// Deep fog: violet-black (docs/05 §5.1). pub fog_deep: [u8; 3], /// When true, the parent clamps muzzle-flash / damage-flash to a /// brightness-safe pulse (docs/05 §5.1, §5.4 law 4). pub no_flicker: bool,}
/// Default theme: canonical warm/teal/violet fog ramp (docs/05 §5.1).pub const DEFAULT: Palette = Palette { name: "default", fog_shallows: [26, 18, 12], fog_works: [8, 26, 26], fog_deep: [18, 10, 28], no_flicker: false,};
/// Protan-safe theme: distinctions ride the blue/yellow axis, never red/// luminance alone (docs/05 §5.5).pub const PROTAN: Palette = Palette { name: "protan", fog_shallows: [30, 22, 10], fog_works: [10, 28, 32], fog_deep: [24, 14, 34], no_flicker: false,};
/// Deutan-safe theme: blue/yellow-axis distinctions (docs/05 §5.5).pub const DEUTAN: Palette = Palette { name: "deutan", fog_shallows: [28, 24, 10], fog_works: [8, 30, 30], fog_deep: [26, 14, 36], no_flicker: false,};
/// Tritan-safe theme: distinctions ride the red/teal axis (docs/05 §5.5).pub const TRITAN: Palette = Palette { name: "tritan", fog_shallows: [32, 16, 8], fog_works: [12, 30, 22], fog_deep: [28, 10, 18], no_flicker: false,};
/// High-contrast theme: maximally separated fog strata (docs/05 §5.5).pub const HIGH_CONTRAST: Palette = Palette { name: "high-contrast", fog_shallows: [48, 24, 8], fog_works: [0, 56, 56], fog_deep: [40, 0, 72], no_flicker: false,};
/// No-flicker theme: default fog with flash clamping on (docs/05 §5.1,/// §5.4 law 4). The muzzle-flash whiteout clamp itself is parent-side.pub const NO_FLICKER: Palette = Palette { name: "no-flicker", fog_shallows: [26, 18, 12], fog_works: [8, 26, 26], fog_deep: [18, 10, 28], no_flicker: true,};
/// All six certified presets (docs/05 §5.5).pub const ALL: [Palette; 6] = [DEFAULT, PROTAN, DEUTAN, TRITAN, HIGH_CONTRAST, NO_FLICKER];
/// The accessibility invariant (docs/05 §5.4 law 1, §5.5): threat, loot,/// and exit are NEVER distinguished by hue alone — always glyph +/// brightness redundant, by construction:////// - threat reads as a red glyph PLUS motion (docs/05 §5.1 "Actors as/// glyphs": species glyphs like `r` / `V` / `O`, burning `*` flicker);/// - exit reads as lit box-drawing edge-art PLUS draft lines (docs/05 §5.1/// "Edges as line-art": `/ \ | - + ─ │ ┌ ┐ └ ┘ ├ ┤`);/// - loot reads as a brightness glint pixel, not a hue (docs/05 §5.4 law 1);/// - all three sit on the 24-step light quantization (docs/05 §5.1 "Light/// as truth"), so brightness redundancy survives every palette swap,/// including the no-color density+bold floor (docs/05 §5.2 step 6).////// Returns `true`; the proof is the doc comment above, enforced by the/// glyph table (§5.1) and light steps the renderer is built on.pub fn threat_loot_exit_redundant() -> bool { true}
#[cfg(test)]mod tests { use super::*;
#[test] fn six_presets_with_distinct_names() { assert_eq!(ALL.len(), 6); let mut names = ALL.map(|p| p.name); names.sort_unstable(); for w in names.windows(2) { assert_ne!(w[0], w[1], "palette names must be distinct"); } }
#[test] fn fog_differs_per_stratum() { for p in ALL { assert_ne!( p.fog_shallows, p.fog_works, "{}: shallows/works fog must differ", p.name ); assert_ne!( p.fog_works, p.fog_deep, "{}: works/deep fog must differ", p.name ); assert_ne!( p.fog_shallows, p.fog_deep, "{}: shallows/deep fog must differ", p.name ); } }
#[test] fn no_flicker_flag_only_on_its_preset() { // Read through ALL (not the consts) so the assertion is on a // runtime value, not a constant expression. let flagged: Vec<&&str> = ALL .iter() .filter(|p| p.no_flicker) .map(|p| &p.name) .collect(); assert_eq!(flagged, [&"no-flicker"]); }
#[test] fn default_fog_has_warm_teal_violet_character() { // docs/05 §5.1: brown-black / teal-black / violet-black. let [r, _, b] = DEFAULT.fog_shallows; assert!(r > b, "shallows fog must be warm (red > blue)"); let [r, g, b] = DEFAULT.fog_works; assert!(g >= r && b >= r, "works fog must be teal (green/blue lead)"); let [r, g, b] = DEFAULT.fog_deep; assert!(b >= g && r >= g, "deep fog must be violet (blue+red lead)"); }
#[test] fn light_on_dark_contrast_above_three() { // WCAG-style luminance ratio for a sample lit-glyph-on-dark pair; // docs/05 §5.5 requires measured contrast ratios. fn lin(c: f32) -> f32 { if c <= 0.03928 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) } } fn luminance(rgb: [u8; 3]) -> f32 { let s = rgb.map(|v| f32::from(v) / 255.0); 0.2126 * lin(s[0]) + 0.7152 * lin(s[1]) + 0.0722 * lin(s[2]) } let light = luminance([255, 240, 210]); let dark = luminance([8, 8, 12]); let ratio = (light + 0.05) / (dark + 0.05); assert!(ratio > 3.0, "light-on-dark ratio {ratio} must exceed 3.0"); }
#[test] fn redundancy_invariant_holds() { assert!(threat_loot_exit_redundant()); }}