Files
systemic-game-design-course/course/10-spellgrid/reference/rust/tests/cli.rs
T
Lucas WintherandClaude Opus 5.5 97c64ebee8 feat: log tree from Module 2 (logTree / log_tree); SpellGrid CLIs print Log:
Engine events carry the nesting depth they were recorded at; the harness
prints the log as an indented tree. New tests test_02_log_tree_shows_nesting
and test_03_log_tree_death_phase; both SpellGrid CLIs print the tree of the
last command's Sequences under a Log: line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 10:23:59 +01:00

114 lines
3.7 KiB
Rust

// 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<RefCell<Vec<u8>>>);
impl Write for Screen {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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}");
}