Files
systemic-game-design-course/course/12-combat/reference/ts/tests/cli.test.ts
T
Lucas WintherandClaude Opus 5.5 fb9d0a4eb4 feat: Module 12, Combat (attack as one Sequence, Taunt, defender-first as a queue priority, end of turn; Bonechewer Brawler as the Expansion Drop)
Adds course/12-combat: lesson, scenarios, nine test_12 tests in both tracks,
the cumulative reference (Module 11's plus combat) and the drop. The Damage
action carries per-target hits with a priority; react stable-sorts its Queue by
priority before order of play, so the triggers answering the defender's Damage
Event go first. orderAttack/canAttack/endTurn join the harness (orderAttack
because attack(id) already reads Attack). endOfTurn triggers fire, so Drakkari
Enchanter's data is live. Test Bell is scoped to Module 12's cards. Harness
contract, catalog, research notes and earlier lessons' forward pointers follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:34:20 +01:00

71 lines
2.8 KiB
TypeScript

// The SpellGrid command line, played from a script (the reference's own check; not a course test).
import { readFileSync } from "node:fs";
import { expect, test } from "vitest";
import { run } from "../src/cli";
function play(level: string, input: string[]): { outcome: string; out: string } {
let out = "";
const outcome = run(readFileSync(`levels/${level}.json`, "utf8"), () => input.shift(), (s) => (out += s));
return { outcome, out };
}
test("cli_wins_level_1", () => {
const { outcome, out } = play("l1-double-tap", ["w", "w", "c Whirlwind", "s", "s", "a", "s", "s", "d", "d"]);
expect(outcome).toBe("won");
expect(out).toContain("DEA IS DOU");
expect(out).toContain("You win!");
});
test("cli_wins_level_2_with_choices_and_play", () => {
const input = ["c Graft", "Ember Moth", "Rune Scribe", "bottom", "c Ember Moth (Assembled) 4 3", "a", "a"];
const { outcome, out } = play("l2-spliced-corpse", input);
expect(outcome).toBe("won");
expect(out).toContain("Choose the base");
expect(out).toContain("Hand: Ember Moth (Assembled)");
});
test("cli_prints_the_log_tree_of_each_command", () => {
const { out } = play("l1-double-tap", ["w", "w", "c Whirlwind", "q"]);
// The moves run no Sequence, so only the cast prints a log: its tree, Deathrattles nested.
expect(out.split("Log:\n")).toHaveLength(2);
expect(out).toContain(
[
"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}",
"",
].join("\n"),
);
});
test("cli_reports_errors_and_quits", () => {
const { outcome, out } = play("l3-simultaneous-paradox", ["c Spark", "x", "q"]);
expect(outcome).toBe("quit");
expect(out).toContain('"Spark" is not in the hand');
expect(out).toContain("Commands:");
});
// The two reference CLIs print the same text for the same input (tools/diff-tracks.mjs checks it).
test("cli_prints_card_names_as_typed", () => {
const { out } = play("l3-simultaneous-paradox", ['c a"b', "q"]);
expect(out).toContain('"a"b" is not in the hand');
});
test("cli_numbers_are_plain_digits", () => {
// "-1" is not a coordinate, so it is part of the card's name.
const cast = play("l1-double-tap", ["c Whirlwind -1 0", "q"]);
expect(cast.out).toContain('"Whirlwind -1 0" is not in the hand');
// "+2" is not a number, so the chooser takes the first option.
const graft = play("l2-spliced-corpse", ["c Graft", "+2", "1", "1", "q"]);
expect(graft.out).toContain("Hand: Clay Golem (Assembled)");
});