//! SpellGrid on the command line: `cargo run --bin spellgrid -- levels/l1-double-tap.json`. //! //! A thin loop over the harness: it draws the level, reads a command, and calls //! `step`, `cast` or `play`. No rules live here. After a command that ran Part I //! Sequences, it prints their events under `Log:` as an indented tree. //! //! ```text //! w a s d one turn: up, left, down, right //! c [x y] cast a spell from your hand (at the minion on (x, y), if given), //! or play a minion from your hand onto the empty tile (x, y) //! q quit //! ``` //! //! Assemble's choices (Graft) are asked for one at a time: answer with a number or a name. use std::cell::RefCell; use std::io::{BufRead, Write}; use std::rc::Rc; use crate::harness::{new_spell_grid, Dir, LevelDef, SpellGrid}; /// How a game ended. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Outcome { Won, Lost, Quit, } /// The player's input and the screen, shared with the chooser. struct Io { input: Box, output: Box, } impl Io { fn read_line(&mut self) -> Option { let mut line = String::new(); match self.input.read_line(&mut line) { Ok(0) | Err(_) => None, Ok(_) => Some(line.trim_end_matches(['\n', '\r']).to_string()), } } fn write(&mut self, s: &str) { let _ = self.output.write_all(s.as_bytes()); let _ = self.output.flush(); } } /// One cell of the drawing: the newest object on the tile, as 3 letters; TEXT in capitals. fn cell(g: &SpellGrid, x: i32, y: i32) -> String { let kinds = g.kinds_at(x, y); // `kinds_at` is sorted; prefer text, which is what the player reads. let Some(kind) = kinds .iter() .find(|k| k.starts_with("text:")) .or(kinds.first()) else { return " . ".to_string(); }; let name = match kind.strip_prefix("text:") { Some(word) => word.to_uppercase(), None => kind.to_lowercase(), }; format!("{:<3}", name.chars().take(3).collect::()) } /// The level as text: the grid, the rules, the minions and the hand. pub fn render(g: &SpellGrid, level: &LevelDef) -> String { let mut lines = Vec::new(); for y in 0..level.height { let row: Vec = (0..level.width).map(|x| cell(g, x, y)).collect(); lines.push(row.join(" ")); } lines.push(format!("Rules: {}", g.active_rules().join(", "))); let mut minions = Vec::new(); for y in 0..level.height { for x in 0..level.width { if let Some(id) = g.minion_at(x, y) { let (name, health) = (g.name_of(id), g.health(id)); minions.push(format!("{name} ({x},{y}) {health} Health")); } } } let or = |list: Vec, separator: &str, none: &str| { if list.is_empty() { none.to_string() } else { list.join(separator) } }; lines.push(format!("Minions: {}", or(minions, "; ", "none"))); lines.push(format!("Hand: {}", or(g.hand(), ", ", "empty"))); lines.join("\n") } /// A coordinate, or a chooser's number: one to nine digits, nothing else (no sign, no point). fn number(word: &str) -> Option { let digits = (1..=9).contains(&word.len()) && word.bytes().all(|b| b.is_ascii_digit()); digits.then(|| word.parse().expect("nine digits fit an i32")) } /// A `c` command: `c [x y]`. fn parse_cast(args: &[&str]) -> (String, Option<(i32, i32)>) { let n = args.len(); if n >= 3 { if let (Some(x), Some(y)) = (number(args[n - 2]), number(args[n - 1])) { return (args[..n - 2].join(" "), Some((x, y))); } } (args.join(" "), None) } /// Play `level_text` (a level in the course's format), reading commands from `input` /// and writing to `output`. pub fn run(level_text: &str, input: Box, output: Box) -> Outcome { let level: LevelDef = serde_json::from_str(level_text).expect("a level in the course's level format"); let mut g = new_spell_grid(level.clone(), vec![]); let io = Rc::new(RefCell::new(Io { input, output })); let chooser_io = Rc::clone(&io); g.set_chooser(move |kind, options| { let mut io = chooser_io.borrow_mut(); let listed: Vec = options .iter() .enumerate() .map(|(i, o)| format!(" {}) {o}", i + 1)) .collect(); io.write(&format!( "Choose the {kind} (a number or a name):\n{}\n? ", listed.join("\n") )); let answer = io.read_line().unwrap_or_default().trim().to_string(); if options.contains(&answer) { return answer; } let picked = number(&answer) .and_then(|n| usize::try_from(n - 1).ok()) .and_then(|i| options.get(i)); picked.unwrap_or(&options[0]).clone() }); play(&mut g, &level, &io) } /// Draw, read a command, do it; until the level is won or lost, or the player quits. fn play(g: &mut SpellGrid, level: &LevelDef, io: &Rc>) -> Outcome { loop { io.borrow_mut().write(&format!("{}\n", render(g, level))); if g.won() { io.borrow_mut().write("You win!\n"); return Outcome::Won; } if g.lost() { io.borrow_mut().write("Nothing is YOU. You lose.\n"); return Outcome::Lost; } io.borrow_mut().write("> "); let Some(line) = io.borrow_mut().read_line() else { return Outcome::Quit; }; let words: Vec<&str> = line.split_whitespace().collect(); // The log holds only what this command does. g.clear_log(); let dir = match words.first() { Some(&"q") => return Outcome::Quit, Some(&"w") => Some(Dir::Up), Some(&"a") => Some(Dir::Left), Some(&"s") => Some(Dir::Down), Some(&"d") => Some(Dir::Right), _ => None, }; // The harness panics on a move the rules don't allow (a card not in the hand, a // full tile). The CLI asks first, and reports the reason instead. let done = match (dir, words.as_slice()) { (Some(dir), _) => Ok(g.step(dir)), (None, ["c", args @ ..]) if !args.is_empty() => match parse_cast(args) { (card, None) => g.check_cast(&card, None).map(|()| g.cast(&card, None)), (card, Some((x, y))) => match g.minion_at(x, y) { Some(target) => g .check_cast(&card, Some(target)) .map(|()| g.cast(&card, Some(target))), None => g.check_play(&card, x, y).map(|()| { g.play(&card, x, y); }), }, }, _ => { Err("Commands: w a s d (move), c [x y] (cast or play), q (quit)".to_string()) } }; if let Err(message) = done { io.borrow_mut().write(&format!("{message}\n")); } let tree = g.log_tree(); if !tree.is_empty() { io.borrow_mut().write(&format!("Log:\n{tree}")); } } }