//! Cause-link recorder: every death, injury, and major event stores the //! rule chain that produced it (docs/04 §4.2 phase 12, docs/03 §3.9). //! Powers epitaphs, death replays, and tutorial subtitles. /// One link in a causal chain: `rule` names the law (e.g. "rooms breathe"), /// `detail` is the NetHack-voice line shown to the player. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CauseLink { pub tick: u64, pub rule: &'static str, pub detail: String, } impl CauseLink { pub fn new(tick: u64, rule: &'static str, detail: impl Into) -> Self { Self { tick, rule, detail: detail.into(), } } } /// Ordered record of what happened, oldest first. #[derive(Clone, Debug, Default)] pub struct CauseChain { links: Vec, } impl CauseChain { pub fn new() -> Self { Self::default() } pub fn push(&mut self, link: CauseLink) { self.links.push(link); } /// Reserve cause capacity before an atomic multi-link transaction mutates. pub(crate) fn try_reserve( &mut self, additional: usize, ) -> Result<(), std::collections::TryReserveError> { self.links.try_reserve(additional) } /// Links in canonical occurrence order (oldest first). pub fn links(&self) -> &[CauseLink] { &self.links } pub fn is_empty(&self) -> bool { self.links.is_empty() } pub fn len(&self) -> usize { self.links.len() } /// NetHack-voice epitaph fragment, e.g. /// "Slain by own cleverness, via falling boulder, while overburdened". pub fn epitaph(&self) -> String { if self.links.is_empty() { return "Died without witnesses, which is also a story.".to_string(); } self.links .iter() .map(|l| l.detail.clone()) .collect::>() .join(", via ") } } #[cfg(test)] mod tests { use super::*; #[test] fn epitaph_joins_chain() { let mut c = CauseChain::new(); c.push(CauseLink::new(1, "gravity", "own cleverness")); c.push(CauseLink::new(2, "gravity", "falling boulder")); assert!(c.epitaph().contains("falling boulder")); } }