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.
5.5 kB · 180 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181//! Undo for INTERFACE mistakes, never sim mistakes (docs/03 §3.10).//!//! Kindness removes *friction*, never *consequence* (docs/01 §7): a delver//! who mis-clicks a loot filter or mistypes a chalk mark gets it back; a//! delver who drinks a mystery phial does not. This stack is generic over//! interface state (`T`) and knows nothing about the sim. Snapshots are moved//! into and out of the stack, so even non-`Clone` interface state is supported.//!//! Hook example (parent-side, NOT code — the CLI owns chalk): before applying//! a chalk-mark edit on the tactical planner (`M`, docs/05 §5.1 camera 3),//! the CLI pushes `undo.push("chalk: bridge?", marks.clone())`; on `u` it//! pops with [`UndoStack::undo`] and restores the marks. Same hook fits loot//! filters and menu selections. The sim tick, inventory, and world state must//! NEVER flow through this stack.
/// Bounded LIFO stack of labeled interface snapshots.#[derive(Clone, Debug)]pub struct UndoStack<T> { stack: Vec<(String, T)>, cap: usize,}
impl<T> UndoStack<T> { /// New stack holding at most `cap` snapshots. `cap == 0` stores nothing /// (pushes are no-ops, [`UndoStack::undo`] always returns `None`). pub fn new(cap: usize) -> Self { Self { stack: Vec::new(), cap, } }
/// Default capacity for casual interface state (16 snapshots). pub const DEFAULT_CAP: usize = 16;
/// Push a labeled snapshot; drops the oldest beyond capacity. pub fn push(&mut self, label: &str, state: T) { if self.cap == 0 { return; } if self.stack.len() >= self.cap { self.stack.remove(0); } self.stack.push((label.to_owned(), state)); }
/// Pop the most recent snapshot (LIFO), with its label. pub fn undo(&mut self) -> Option<(String, T)> { self.stack.pop() }
/// Number of snapshots currently held. pub fn len(&self) -> usize { self.stack.len() }
/// True when there is nothing to undo. pub fn is_empty(&self) -> bool { self.stack.is_empty() }
/// Maximum snapshots held. pub fn cap(&self) -> usize { self.cap }
/// Discard all snapshots (e.g. on screen change). pub fn clear(&mut self) { self.stack.clear(); }}
impl<T> Default for UndoStack<T> { fn default() -> Self { Self::new(Self::DEFAULT_CAP) }}
#[cfg(test)]mod tests { use super::*;
#[test] fn lifo_order() { let mut u: UndoStack<Vec<String>> = UndoStack::new(8); u.push("first", vec!["a".to_owned()]); u.push("second", vec!["a".to_owned(), "b".to_owned()]); let (label, state) = u.undo().expect("second undo target"); assert_eq!(label, "second"); assert_eq!(state, vec!["a".to_owned(), "b".to_owned()]); let (label, state) = u.undo().expect("first undo target"); assert_eq!(label, "first"); assert_eq!(state, vec!["a".to_owned()]); }
#[test] fn cap_eviction_drops_oldest() { let mut u: UndoStack<i32> = UndoStack::new(2); u.push("one", 1); u.push("two", 2); u.push("three", 3); assert_eq!(u.len(), 2); assert_eq!(u.undo(), Some(("three".to_owned(), 3))); assert_eq!(u.undo(), Some(("two".to_owned(), 2))); assert_eq!(u.undo(), None, "evicted 'one' is gone"); }
#[test] fn empty_undo_is_none() { let mut u: UndoStack<String> = UndoStack::new(4); assert!(u.is_empty()); assert_eq!(u.undo(), None); }
#[test] fn labels_preserved() { let mut u: UndoStack<char> = UndoStack::new(4); u.push("chalk: bridge?", 'b'); u.push("filter: wire only", 'w'); assert_eq!(u.undo(), Some(("filter: wire only".to_owned(), 'w'))); }
#[test] fn len_and_is_empty_track() { let mut u: UndoStack<u8> = UndoStack::new(4); assert!(u.is_empty()); u.push("a", 1); assert_eq!(u.len(), 1); assert!(!u.is_empty()); u.clear(); assert!(u.is_empty()); }
#[test] fn zero_cap_stores_nothing() { let mut u: UndoStack<u8> = UndoStack::new(0); u.push("x", 1); assert!(u.is_empty()); assert_eq!(u.undo(), None); }
#[test] fn undo_returns_owned_snapshot() { let mut u: UndoStack<Vec<u8>> = UndoStack::new(4); let marks = vec![1, 2, 3]; u.push("chalk", marks.clone()); let (_, back) = u.undo().expect("snapshot"); assert_eq!(back, marks); }
#[test] fn supports_non_clone_interface_state() { #[derive(Debug, PartialEq, Eq)] struct CursorSelection(usize);
let mut u = UndoStack::new(1); u.push("cursor", CursorSelection(7)); assert_eq!(u.undo(), Some(("cursor".to_owned(), CursorSelection(7)))); }
#[test] fn default_capacity_is_bounded() { let mut u = UndoStack::default(); for state in 0..(UndoStack::<usize>::DEFAULT_CAP + 3) { u.push("selection", state); } assert_eq!(u.cap(), UndoStack::<usize>::DEFAULT_CAP); assert_eq!(u.len(), UndoStack::<usize>::DEFAULT_CAP); }
#[test] fn clear_preserves_capacity_for_reuse() { let mut u = UndoStack::new(2); u.push("one", 1); u.clear(); assert_eq!(u.cap(), 2); u.push("two", 2); assert_eq!(u.undo(), Some(("two".to_owned(), 2))); }}