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.9 kB · 251 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252//! Pure terminal-bell synthesis (docs/03 §3.10, Phase 12).//!//! This crate never prints, flushes, reads environment variables, or opens an//! audio device. [`Synth::pattern`] returns a bounded string for the terminal//! backend to append to its next flush. Patterns contain ASCII BEL (`0x07`)//! bytes only: sound identity is a count, never a delay or terminal control//! sequence that could collide with prompt input.//!//! SSH state is an input, not a hidden environment probe. An SSH session always//! suppresses output, while the user's mutable mute preference is preserved so//! leaving SSH restores that preference. Rich sampled synthesis (including any//! optional audio backend) is an honest Phase 13+ hook; Phase 12 has no `rodio`//! dependency and no pretending that repeated terminal bells have pitch.
/// ASCII terminal bell, the only byte emitted in a Phase 12 pattern.pub const TERMINAL_BELL: char = '\x07';/// Hard safety cap on bell bytes returned for one event.pub const MAX_BELLS_PER_PATTERN: usize = 4;/// Since every pattern byte is one ASCII BEL, this is also the byte-length cap.pub const MAX_PATTERN_LEN: usize = MAX_BELLS_PER_PATTERN;
/// A semantic sound event.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum Sound { BellToll, Clack, Hum, Silence,}
impl Sound { /// Canonical count-only terminal pattern before caller/config caps. pub const fn bell_count(self) -> usize { match self { Self::BellToll => 3, Self::Clack => 1, Self::Hum => 2, Self::Silence => 0, } }}
/// Pure startup inputs supplied by CLI/config and terminal capability probing.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub struct SynthConfig { /// User/config mute preference. pub muted: bool, /// Whether terminal probing identified an SSH session. pub ssh: bool, /// Per-event preference, clamped to [`MAX_BELLS_PER_PATTERN`]. pub max_bells: usize,}
impl Default for SynthConfig { fn default() -> Self { Self { muted: false, ssh: false, max_bells: MAX_BELLS_PER_PATTERN, } }}
/// Build a deterministic, bounded count-only bell pattern.////// `max_bells` is untrusted configuration and is always clamped to the global/// safety cap. [`Sound::Silence`] always yields an empty string.#[allow(clippy::manual_repeat_n)] // repeat_n stabilized after the Rust 1.75 MSRV.pub fn bell_pattern(sound: Sound, max_bells: usize) -> String { let count = sound.bell_count().min(max_bells).min(MAX_BELLS_PER_PATTERN); std::iter::repeat(TERMINAL_BELL).take(count).collect()}
/// Mutable pure synth state. The caller owns all actual terminal I/O.#[derive(Clone, Debug, PartialEq, Eq)]pub struct Synth { muted: bool, ssh: bool, max_bells: usize,}
impl Default for Synth { fn default() -> Self { Self::new(SynthConfig::default()) }}
impl Synth { /// Construct from explicit config/probe inputs. pub fn new(config: SynthConfig) -> Self { Self { muted: config.muted, ssh: config.ssh, max_bells: config.max_bells.min(MAX_BELLS_PER_PATTERN), } }
/// Effective mute state. SSH cannot be overridden by toggling user mute. pub const fn is_muted(&self) -> bool { self.muted || self.ssh }
/// User/config mute preference (separate from mandatory SSH suppression). pub const fn user_muted(&self) -> bool { self.muted }
/// Current SSH-probe input. pub const fn is_ssh(&self) -> bool { self.ssh }
/// Effective bounded per-event bell cap. pub const fn max_bells(&self) -> usize { self.max_bells }
/// Change the user's mute preference without bypassing SSH suppression. pub fn set_muted(&mut self, muted: bool) { self.muted = muted; }
/// Update the terminal probe result. Leaving SSH restores user preference. pub fn set_ssh(&mut self, ssh: bool) { self.ssh = ssh; }
/// Change the preference, clamped to the global safety cap. pub fn set_max_bells(&mut self, max_bells: usize) { self.max_bells = max_bells.min(MAX_BELLS_PER_PATTERN); }
/// Return the next pure output pattern; never performs I/O. pub fn pattern(&self, sound: Sound) -> String { if self.is_muted() { String::new() } else { bell_pattern(sound, self.max_bells) } }}
#[cfg(test)]mod tests { use super::*;
#[test] fn canonical_patterns_are_count_only_and_distinct() { assert_eq!(bell_pattern(Sound::BellToll, usize::MAX), "\x07\x07\x07"); assert_eq!(bell_pattern(Sound::Clack, usize::MAX), "\x07"); assert_eq!(bell_pattern(Sound::Hum, usize::MAX), "\x07\x07"); assert_eq!(bell_pattern(Sound::Silence, usize::MAX), ""); }
#[test] fn pattern_builder_is_deterministic() { for sound in [Sound::BellToll, Sound::Clack, Sound::Hum, Sound::Silence] { assert_eq!(bell_pattern(sound, 4), bell_pattern(sound, 4)); } }
#[test] fn untrusted_cap_is_globally_bounded() { let pattern = bell_pattern(Sound::BellToll, usize::MAX); assert!(pattern.len() <= MAX_PATTERN_LEN); assert!(pattern.chars().count() <= MAX_BELLS_PER_PATTERN); }
#[test] fn caller_cap_truncates_pattern() { assert_eq!(bell_pattern(Sound::BellToll, 2), "\x07\x07"); assert_eq!(bell_pattern(Sound::BellToll, 0), ""); }
#[test] fn patterns_contain_no_terminal_control_language_except_bell() { for sound in [Sound::BellToll, Sound::Clack, Sound::Hum] { assert!(bell_pattern(sound, 4).bytes().all(|byte| byte == 0x07)); } }
#[test] fn default_synth_is_local_and_audible() { let synth = Synth::default(); assert!(!synth.is_muted()); assert!(!synth.pattern(Sound::Clack).is_empty()); }
#[test] fn explicit_mute_suppresses_every_sound() { let mut synth = Synth::default(); synth.set_muted(true); for sound in [Sound::BellToll, Sound::Clack, Sound::Hum, Sound::Silence] { assert_eq!(synth.pattern(sound), ""); } }
#[test] fn unmuting_is_mutable() { let mut synth = Synth::new(SynthConfig { muted: true, ..SynthConfig::default() }); synth.set_muted(false); assert_eq!(synth.pattern(Sound::Hum), "\x07\x07"); }
#[test] fn ssh_forces_sound_off_even_after_unmute() { let mut synth = Synth::new(SynthConfig { ssh: true, ..SynthConfig::default() }); synth.set_muted(false); assert!(synth.is_muted()); assert_eq!(synth.pattern(Sound::BellToll), ""); }
#[test] fn leaving_ssh_restores_user_preference() { let mut synth = Synth::new(SynthConfig { muted: false, ssh: true, max_bells: 4, }); synth.set_ssh(false); assert!(!synth.is_muted()); assert_eq!(synth.pattern(Sound::Clack), "\x07"); }
#[test] fn leaving_ssh_does_not_clear_user_mute() { let mut synth = Synth::new(SynthConfig { muted: true, ssh: true, max_bells: 4, }); synth.set_ssh(false); assert!(synth.is_muted()); }
#[test] fn mutable_cap_remains_globally_bounded() { let mut synth = Synth::default(); synth.set_max_bells(1); assert_eq!(synth.pattern(Sound::BellToll), "\x07"); synth.set_max_bells(usize::MAX); assert_eq!(synth.max_bells(), MAX_BELLS_PER_PATTERN); assert!(synth.pattern(Sound::BellToll).len() <= MAX_PATTERN_LEN); }}