#!/usr/bin/env node // Play the SpellGrid capstone (Module 10) on the command line, using the course's reference engine. // // node tools/play-spellgrid.mjs [level] [--dir ] [--stage-only] // npm run play:spellgrid -- rust l1 // // Stages course/10-spellgrid/reference/ with its card data (course/cards plus // course/10-spellgrid/cards) and course/10-spellgrid/levels into (default: a new temp // directory), installs the TypeScript dependencies if needed, then runs the CLI in that // directory. `level` is a level of course/10-spellgrid/levels by name or prefix (`l1`, // `l2-spliced-corpse`), or a path to a level file; default `l1`. With --stage-only it prints // the command instead of running it. Commands are read from stdin, so a script can be piped in. import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; import { cardDirs, fail, isMain, moduleDirs } from "./lib.mjs"; const MODULE = "10"; const MARKER = ".play-spellgrid"; const COMMANDS = { rust: (level) => ["cargo", ["run", "--quiet", "--bin", "spellgrid", "--", level]], ts: (level) => ["npx", ["tsx", "src/cli.ts", level]], }; export function parseArgs(argv) { const opts = { lang: null, level: "l1", dir: null, stageOnly: false }; const rest = []; for (let i = 0; i < argv.length; i++) { if (argv[i] === "--dir") opts.dir = argv[++i]; else if (argv[i] === "--stage-only") opts.stageOnly = true; else rest.push(argv[i]); } [opts.lang, opts.level = "l1"] = rest; return opts; } /** The level file to play: a path that exists, else a level of the module by name or prefix. */ export function resolveLevel(levelsDir, level) { if (existsSync(level) && level.endsWith(".json")) return { path: resolve(level), staged: false }; const files = existsSync(levelsDir) ? readdirSync(levelsDir).filter((f) => f.endsWith(".json")).sort() : []; const match = files.find((f) => f === level || f === `${level}.json`) ?? files.find((f) => f.startsWith(`${level}-`)); if (!match) return null; return { path: join("levels", match), staged: true }; } /** Copy the reference, its cards and the levels into `dir`. Returns `dir`. */ export function stagePlay(lang, dir) { const mod = moduleDirs().find((m) => m.num === MODULE); if (!mod) fail(`no module ${MODULE} in the course`); const ref = join(mod.path, "reference", lang); if (!existsSync(ref)) fail(`no ${lang} reference in ${mod.path}`); if (dir) { if (existsSync(dir) && readdirSync(dir).length && !existsSync(join(dir, MARKER))) { fail(`${dir} is not empty and was not staged by play-spellgrid; give an empty or new directory`); } mkdirSync(dir, { recursive: true }); } else { dir = mkdtempSync(join(tmpdir(), `spellgrid-${lang}-`)); } cpSync(ref, dir, { recursive: true }); for (const cards of cardDirs(MODULE)) cpSync(cards, join(dir, "cards"), { recursive: true }); if (existsSync(join(mod.path, "levels"))) cpSync(join(mod.path, "levels"), join(dir, "levels"), { recursive: true }); writeFileSync(join(dir, MARKER), `${lang}\n`); return dir; } function main() { const opts = parseArgs(process.argv.slice(2)); if (!COMMANDS[opts.lang]) fail("usage: play-spellgrid.mjs [level] [--dir ] [--stage-only]"); const dir = resolve(stagePlay(opts.lang, opts.dir)); const level = resolveLevel(join(dir, "levels"), opts.level); if (!level) { const names = readdirSync(join(dir, "levels")).filter((f) => f.endsWith(".json")).map((f) => basename(f, ".json")); fail(`no level "${opts.level}". Levels: ${names.join(", ")}`); } if (opts.lang === "ts" && !existsSync(join(dir, "node_modules"))) { console.error(`Installing the TypeScript reference's dependencies in ${dir} ...`); const r = spawnSync("npm", ["install", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, stdio: ["ignore", "inherit", "inherit"] }); if (r.status !== 0) fail("npm install failed"); } const [cmd, args] = COMMANDS[opts.lang](level.path); const shown = `cd ${dir} && ${cmd} ${args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")}`; if (opts.stageOnly) { console.log(`Staged the Module ${MODULE} ${opts.lang} reference in ${dir}. Play with:\n ${shown}`); return; } console.error(`Staged in ${dir} (re-run there with: ${shown})`); console.error("w a s d: one turn; c [x y]: cast or play a card; q: quit.\n"); const r = spawnSync(cmd, args, { cwd: dir, stdio: "inherit" }); process.exit(r.status ?? 1); } if (isMain(import.meta.url)) main();