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.
4.8 kB · 80 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081use super::*;use hexwork_game::composed_frame::Style;use unicode_width::UnicodeWidthChar;
#[derive(Clone, Debug, Default, PartialEq, Eq)]struct Pixel { text: String, continuation: bool, style: Style }struct Oracle { cells: Vec<Pixel>, width: usize, x: usize, y: usize, style: Style, lead: Option<usize> }impl Oracle { fn new(width: usize, height: usize) -> Self { Self { cells: vec![Pixel::default(); width*height], width, x:0, y:0, style:Style::default(), lead:None } } fn apply(&mut self, ansi: &str) { let mut chars = ansi.chars(); while let Some(c) = chars.next() { if c == '\x1b' { assert_eq!(chars.next(), Some('[')); let mut args = String::new(); let command = loop { let c = chars.next().unwrap(); if c.is_ascii_alphabetic() { break c; } args.push(c); }; let nums: Vec<usize> = if args.is_empty() { vec![] } else { args.split(';').map(|v| v.parse().unwrap()).collect() }; match command { 'H' => { self.y = nums.first().copied().unwrap_or(1)-1; self.x = nums.get(1).copied().unwrap_or(1)-1; self.lead = None; } 'J' => { assert_eq!(nums, [2]); self.cells.fill(Pixel::default()); self.lead = None; } 'm' => match nums.as_slice() { [0] => self.style = Style::default(), [1] => self.style.bold = true, [38,2,r,g,b] => self.style.foreground = Some([*r as u8,*g as u8,*b as u8]), [48,2,r,g,b] => self.style.background = Some([*r as u8,*g as u8,*b as u8]), _ => panic!("unsupported test SGR {nums:?}"), }, _ => panic!("unsupported test CSI {command}"), } continue; } let width = c.width().unwrap_or(0); if width == 0 { if let Some(i) = self.lead { self.cells[i].text.push(c); } continue; } let i = self.y*self.width+self.x; // Terminal overwrites either half of an old wide glyph as one unit. for offset in 0..width { let target = i + offset; if self.cells[target].continuation && target % self.width > 0 { self.cells[target-1] = Pixel::default(); } if target % self.width + 1 < self.width && self.cells[target+1].continuation { self.cells[target+1] = Pixel::default(); } self.cells[target] = Pixel::default(); } self.cells[i] = Pixel { text:c.to_string(), continuation:false, style:self.style }; for offset in 1..width { self.cells[i+offset] = Pixel { text:String::new(), continuation:true, style:self.style }; } self.lead = Some(i); self.x += width; } }}fn frame(text: &str, style: Style) -> ComposedFrame { let mut frame = ComposedFrame::new(8,1).unwrap(); frame.text_row(0,text,style); frame}#[test]fn terminal_oracle_wide_narrow_shift_combining_and_style() { let red = Style { foreground:Some([255,0,0]), background:Some([1,2,3]), bold:true }; let mut cache = TerminalCache::default(); let mut actual = Oracle::new(8,1); for (text, style) in [("深x",red),("abx",red),("a深x",red),("深ax",red),("e\u{301}x",red),("ex",red),("ex",Style::default()),("深",Style::default()),("a",Style::default())] { let next = frame(text,style); actual.apply(&cache.encode(next.clone(), ColorMode::TrueColor)); let mut expected = Oracle::new(8,1); expected.apply(&encode(&next,None,ColorMode::TrueColor)); assert_eq!(actual.cells, expected.cells, "transition to {text:?}"); }}#[test]fn terminal_oracle_physical_clear_and_mode_change_reset_cache() { let mut cache = TerminalCache::default(); let next = frame("@ deep",Style::default()); let mut actual = Oracle::new(8,1); actual.apply(&cache.encode(next.clone(),ColorMode::TrueColor)); assert!(!cache.encode(next.clone(),ColorMode::TrueColor).contains('H')); // Same-size resize and rejected-size excursion both clear without composition. for _ in 0..2 { actual.apply(cache.physical_clear()); actual.apply(&cache.encode(next.clone(),ColorMode::TrueColor)); let mut expected = Oracle::new(8,1); expected.apply(&encode(&next,None,ColorMode::TrueColor)); assert_eq!(actual.cells, expected.cells); } let colored = frame("@",Style { foreground:Some([255,0,0]), ..Style::default() }); assert!(cache.encode(colored.clone(),ColorMode::TrueColor).contains("38;2;255;0;0m")); assert!(cache.encode(colored.clone(),ColorMode::Ansi256).contains("38;5;196m")); let plain = cache.encode(colored,ColorMode::NoColor); assert!(plain.contains('H')); assert!(!plain.contains("38;")); assert!(plain.contains("[1m"));}