Files
systemic-game-design-course/course/11-conditions/reference/ts/tests/cli.test.ts
T
Lucas WintherandClaude Opus 5.5 34c655c1a4 feat: Module 11, Conditions (NOUN ON NOUN IS ..., checked per object at use; NEAR as the Expansion Drop)
Adds course/11-conditions: lesson, scenarios, nine test_11 tests in both
tracks, the cumulative reference (Module 10's plus conditions) and the NEAR
drop. The engine learns a condition role, rules carry conditions, and
Level.hasProperty and conversion evaluate content's condition checks against
the board whenever a rule is used. Resolution pairs only rules with the same
conditions. Catalog, harness contract, research notes and the diff-tracks
generator follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:00:26 +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)");
});