// The SpellGrid command line, played from a script (the reference's own check; not a course test). use std::cell::RefCell; use std::io::{Cursor, Write}; use std::rc::Rc; use engine::cli::{run, Outcome}; /// Collects what the CLI prints. #[derive(Clone, Default)] struct Screen(Rc>>); impl Write for Screen { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.0.borrow_mut().extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } fn play(level: &str, input: &[&str]) -> (Outcome, String) { let screen = Screen::default(); let input = Box::new(Cursor::new(input.join("\n") + "\n")); let outcome = run(level, input, Box::new(screen.clone())); let out = String::from_utf8(screen.0.borrow().clone()).unwrap(); (outcome, out) } #[test] fn cli_wins_level_1() { let input = ["w", "w", "c Whirlwind", "s", "s", "a", "s", "s", "d", "d"]; let (outcome, out) = play(include_str!("../levels/l1-double-tap.json"), &input); assert_eq!(outcome, Outcome::Won); assert!(out.contains("DEA IS DOU")); assert!(out.contains("You win!")); } #[test] fn cli_wins_level_2_with_choices_and_play() { let input = [ "c Graft", "Ember Moth", "Rune Scribe", "bottom", "c Ember Moth (Assembled) 4 3", "a", "a", ]; let (outcome, out) = play(include_str!("../levels/l2-spliced-corpse.json"), &input); assert_eq!(outcome, Outcome::Won); assert!(out.contains("Choose the base")); assert!(out.contains("Hand: Ember Moth (Assembled)")); } #[test] fn cli_prints_the_log_tree_of_each_command() { let level = include_str!("../levels/l1-double-tap.json"); let (_, out) = play(level, &["w", "w", "c Whirlwind", "q"]); // The moves run no Sequence, so only the cast prints a log: its tree, Deathrattles nested. assert_eq!(out.matches("Log:\n").count(), 1, "{out}"); let tree = [ "Log:", "SpellCast{card: \"Whirlwind\"}", "Damaged{target: 32, amount: 1}", "Damaged{target: 33, amount: 1}", "Destroyed{id: 32}", "DeathPhase{round: 1}", "Triggered{source: 32, trigger: \"deathrattle\"}", " Damaged{target: 33, amount: 1}", "Triggered{source: 32, trigger: \"deathrattle\"}", " Damaged{target: 33, amount: 1}", "Destroyed{id: 33}", "DeathPhase{round: 2}", "", ]; assert!(out.contains(&tree.join("\n")), "{out}"); } #[test] fn cli_reports_errors_and_quits() { let input = ["c Spark", "x", "q"]; let (outcome, out) = play( include_str!("../levels/l3-simultaneous-paradox.json"), &input, ); assert_eq!(outcome, Outcome::Quit); assert!(out.contains("\"Spark\" is not in the hand")); assert!(out.contains("Commands:")); } // The two reference CLIs print the same text for the same input (tools/diff-tracks.mjs checks it). #[test] fn cli_prints_card_names_as_typed() { let level = include_str!("../levels/l3-simultaneous-paradox.json"); let (_, out) = play(level, &["c a\"b", "q"]); assert!(out.contains("\"a\"b\" is not in the hand"), "{out}"); } #[test] fn cli_numbers_are_plain_digits() { // "-1" is not a coordinate, so it is part of the card's name. let level = include_str!("../levels/l1-double-tap.json"); let (_, out) = play(level, &["c Whirlwind -1 0", "q"]); assert!( out.contains("\"Whirlwind -1 0\" is not in the hand"), "{out}" ); // "+2" is not a number, so the chooser takes the first option. let level = include_str!("../levels/l2-spliced-corpse.json"); let (_, out) = play(level, &["c Graft", "+2", "1", "1", "q"]); assert!(out.contains("Hand: Ember Moth (Assembled)"), "{out}"); }