// Every card the course ships is well-formed, by the rule the reference engines apply to // Assemble (Module 5: `wellFormed` in card.ts, `well_formed` in card.rs), restated here // against the JSON so that a new card file is checked without building an engine. import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { courseRoot, moduleDirs } from "../lib.mjs"; /** * For a minion, every effect clause belongs to a trigger (none before the first trigger, * or after an aura or keyword), and every trigger has at least one effect before the next * trigger, aura, keyword or the end. A spell passes. */ function wellFormed(text, type) { if (type !== "minion") return true; let open = "none"; // the trigger being listed: "none", "empty" (no effect yet) or "filled" for (const clause of text) { if (clause.kind === "effect") { if (open === "none") return false; open = "filled"; } else { if (open === "empty") return false; open = clause.kind === "trigger" ? "empty" : "none"; } } return open !== "empty"; } /** Every card file the course ships: course/cards, each module's cards/, each Expansion Drop solution's cards/. */ function shippedCards() { const dirs = [ join(courseRoot(), "cards"), ...moduleDirs().flatMap((m) => [join(m.path, "cards"), join(m.path, "expansion", "solution", "cards")]), ].filter((d) => existsSync(d)); return dirs.flatMap((d) => readdirSync(d) .filter((f) => f.endsWith(".json")) .map((f) => ({ path: join(d, f), def: JSON.parse(readFileSync(join(d, f), "utf8")) })), ); } const trigger = { kind: "trigger", on: "deathrattle" }; const effect = { kind: "effect", effect: { type: "draw", count: 1 } }; const aura = { kind: "aura", modifier: { type: "damageRedirect", from: "friendlyHero", to: "self" } }; const keyword = { kind: "keyword", keyword: "taunt" }; test("well_formed_rule", () => { for (const text of [[], [aura], [keyword, trigger, effect], [trigger, effect, effect, trigger, effect, aura]]) { assert.equal(wellFormed(text, "minion"), true, JSON.stringify(text)); } // Loot Hoarder with Baron Rivendare's top half, then his bottom half (Module 5). for (const text of [[effect], [trigger, aura], [trigger], [trigger, trigger, effect], [trigger, effect, keyword, effect]]) { assert.equal(wellFormed(text, "minion"), false, JSON.stringify(text)); } assert.equal(wellFormed([effect], "spell"), true); }); test("shipped_cards_are_well_formed", () => { const cards = shippedCards(); assert.ok(cards.some((c) => c.def.name === "Loot Hoarder"), "course/cards was not read"); assert.ok(cards.some((c) => c.def.name === "Graft"), "course/10-spellgrid/cards was not read"); for (const { path, def } of cards) assert.ok(wellFormed(def.text, def.type), `${path}: ill-formed text`); });