feat: module 10 spellgrid
SpellGrid capstone: Part I's card engine and Part II's grid engine joined at three seams (rules -> aura data, hazard victims -> one destroy Sequence, Sequence end -> write text, remove the dead, reparse). Three levels with scripted solutions, a naive-timing test via a harness-only hook that reverses the END_OF_PHASE list, CLIs in both tracks, the BATTLECRY IS DOUBLE Expansion Drop, SpellGrid cards and words in course/cards and the catalog, level schema cards/hand, and tools staging/copying of module levels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f3359cfe2
commit
a0cf9be214
96 files changed
+10454
-8
No files matched your search
@@ -40,6 +40,13 @@ if (existsSync(cards)) {
|
||||
console.log(`Copied the course card data into ${join(ws, "cards")}`);
|
||||
}
|
||||
|
||||
// The module's puzzle levels (Module 10's tests load them from levels/).
|
||||
const levels = join(mod.path, "levels");
|
||||
if (existsSync(levels)) {
|
||||
cpSync(levels, join(ws, "levels"), { recursive: true });
|
||||
console.log(`Copied the module's levels into ${join(ws, "levels")}`);
|
||||
}
|
||||
|
||||
// Harness additions: module-local note, else the "- M<N>:" line of course/harness.md.
|
||||
const note = join(mod.path, "harness-adds.md");
|
||||
let adds = existsSync(note) ? readFileSync(note, "utf8").trim() : "";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "width": 1, "height": 1, "objects": [] }
|
||||
@@ -27,11 +27,14 @@ test("catalog_valid", () => {
|
||||
assert.ok(Array.isArray(cat) && cat.length > 0);
|
||||
const names = new Set();
|
||||
for (const e of cat) {
|
||||
assert.deepEqual(validate({ ...entry, required: e.fictional ? ["name", "kind", "text"] : entry.required }, e), [], e.name);
|
||||
// Test cards and SpellGrid's own cards and words (Module 10) have no outside source.
|
||||
const sourced = !e.fictional && e.game !== "spellgrid";
|
||||
assert.deepEqual(validate({ ...entry, required: sourced ? entry.required : ["name", "kind", "text"] }, e), [], e.name);
|
||||
assert.deepEqual(validate(e.kind === "card" ? cardEntry : wordEntry, e), [], e.name);
|
||||
assert.ok(!names.has(e.name), `duplicate ${e.name}`);
|
||||
names.add(e.name);
|
||||
if (e.kind === "card" && !e.fictional) assert.match(e.source, /^https:\/\/hearthstone\.wiki\.gg\//, e.name);
|
||||
if (e.kind === "card" && sourced) assert.match(e.source, /^https:\/\/hearthstone\.wiki\.gg\//, e.name);
|
||||
if (e.game === "spellgrid") assert.ok(!e.fictional && !e.name.startsWith("Test "), `${e.name}: a SpellGrid card is not a test card`);
|
||||
if (e.kind === "card" && e.name.startsWith("Test ")) assert.equal(e.fictional, true, e.name);
|
||||
if (e.fictional) assert.ok(e.name.startsWith("Test "), e.name);
|
||||
}
|
||||
@@ -84,8 +87,8 @@ test("course_cards_valid_and_match_catalog", () => {
|
||||
);
|
||||
assert.ok(c.split <= c.text.length, `${f}: split within text`);
|
||||
}
|
||||
// Every Hearthstone (and fictional) card in the catalog has its data.
|
||||
for (const e of cat.values()) if (e.kind === "card" && !e.game) assert.ok(seen.has(e.name), `no card file for ${e.name}`);
|
||||
// Every card in the catalog (Hearthstone, fictional and SpellGrid's own) has its data.
|
||||
for (const e of cat.values()) if (e.kind === "card") assert.ok(seen.has(e.name), `no card file for ${e.name}`);
|
||||
});
|
||||
|
||||
test("card_schema_assemble_effect", () => {
|
||||
@@ -108,3 +111,33 @@ test("level_schema", () => {
|
||||
assert.deepEqual(validate(levelSchema, { width: 3, height: 1, objects: [{ kind: "baba", x: 0, y: 0 }] }), []);
|
||||
assert.notDeepEqual(validate(levelSchema, { width: 3, height: 1, objects: [{ kind: "baba", x: 0 }] }), []);
|
||||
});
|
||||
|
||||
test("level_schema_spellgrid_fields", () => {
|
||||
const level = { width: 2, height: 1, objects: [], cards: [{ card: "Powder Keg", x: 1, y: 0 }], hand: ["Spark"] };
|
||||
assert.deepEqual(validate(levelSchema, level), []);
|
||||
assert.notDeepEqual(validate(levelSchema, { ...level, hand: [{ card: "Spark" }] }), []);
|
||||
assert.notDeepEqual(validate(levelSchema, { ...level, cards: [{ card: "Powder Keg", x: 1 }] }), []);
|
||||
});
|
||||
|
||||
// Puzzle levels the course ships: course/NN-*/levels/*.json. Every one is valid, stays
|
||||
// on its grid, and names only cards that have data, minions with a noun on the grid.
|
||||
test("course_levels_valid", () => {
|
||||
const courseDir = join(here, "../../course");
|
||||
const cards = new Map(courseCardFiles().map((f) => json(f)).map((c) => [c.name, c]));
|
||||
const files = readdirSync(courseDir)
|
||||
.filter((d) => /^\d\d-/.test(d) && existsSync(join(courseDir, d, "levels")))
|
||||
.flatMap((d) => readdirSync(join(courseDir, d, "levels")).filter((f) => f.endsWith(".json")).map((f) => join(courseDir, d, "levels", f)));
|
||||
assert.ok(files.length >= 3, `found ${files.length} level files`);
|
||||
for (const f of files) {
|
||||
const level = json(f);
|
||||
assert.deepEqual(validate(levelSchema, level), [], f);
|
||||
for (const o of [...level.objects, ...(level.cards ?? [])]) {
|
||||
assert.ok(o.x < level.width && o.y < level.height, `${f}: (${o.x}, ${o.y}) is off the grid`);
|
||||
}
|
||||
for (const { card } of level.cards ?? []) {
|
||||
const c = cards.get(card);
|
||||
assert.ok(c && c.type === "minion" && c.noun, `${f}: ${card} is a minion with a noun`);
|
||||
}
|
||||
for (const card of level.hand ?? []) assert.ok(cards.has(card), `${f}: no card data for ${card}`);
|
||||
}
|
||||
});
|
||||
@@ -21,6 +21,15 @@ test("start_module_copies_course_cards", () => {
|
||||
assert.ok(existsSync(join(ws, "cards/test-fixture.json")));
|
||||
});
|
||||
|
||||
test("start_module_copies_module_levels", () => {
|
||||
const ws = copyFixture("ws-start");
|
||||
mkdirSync(join(ws, "tests"), { recursive: true });
|
||||
writeFileSync(join(ws, "tests/m02.rs"), "");
|
||||
const r = run("start-module.mjs", ["03", ws], env);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.ok(existsSync(join(ws, "levels/fixture-level.json")));
|
||||
});
|
||||
|
||||
test("start_module_uses_track", () => {
|
||||
const ws = copyFixture("ws-start-ts");
|
||||
assert.equal(run("start-module.mjs", ["01", ws], env).code, 0);
|
||||
|
||||
@@ -66,3 +66,13 @@ test("verify_ts_default_installs_dependencies_first", async () => {
|
||||
assert.equal(commandFor("ts", { VERIFY_CMD_TS: "true" }), "true");
|
||||
assert.equal(commandFor("rust", {}), "cargo test");
|
||||
});
|
||||
|
||||
test("verify_stage_copies_module_levels", async () => {
|
||||
// From Module 10 the tests load puzzle levels from <crate or package root>/levels.
|
||||
process.env.COURSE_ROOT = root;
|
||||
const { planVerification, stage } = await import("../verify-references.mjs");
|
||||
const { existsSync } = await import("node:fs");
|
||||
const plans = planVerification(moduleDirs(), 3);
|
||||
assert.ok(existsSync(join(stage(plans[2], "ts"), "levels", "fixture-level.json")));
|
||||
assert.ok(!existsSync(join(stage(plans[1], "ts"), "levels")));
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
// For each module <= N with a reference/: copy reference + tests of modules 01..module into a temp dir, run tests.
|
||||
// course/cards (the card data, loaded by the references from Module 5) is staged as <dir>/cards.
|
||||
// course/cards (the card data, loaded by the references from Module 5) is staged as <dir>/cards,
|
||||
// and each tested module's levels/ (the puzzle levels its tests load, from Module 10) as <dir>/levels.
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -35,6 +36,7 @@ export function stage(plan, lang) {
|
||||
mkdirSync(join(dir, "tests"), { recursive: true });
|
||||
for (const t of plan.testModules) {
|
||||
for (const f of testFiles(t.path, lang)) cpSync(f, join(dir, "tests", basename(f)));
|
||||
if (existsSync(join(t.path, "levels"))) cpSync(join(t.path, "levels"), join(dir, "levels"), { recursive: true });
|
||||
}
|
||||
const cards = join(courseRoot(), "cards");
|
||||
if (existsSync(cards)) cpSync(cards, join(dir, "cards"), { recursive: true });
|
||||
|
||||
Reference in new issue
Block a user