summaryrefslogtreecommitdiff
path: root/tools/validate.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/validate.js')
-rwxr-xr-xtools/validate.js104
1 files changed, 104 insertions, 0 deletions
diff --git a/tools/validate.js b/tools/validate.js
new file mode 100755
index 0000000..b0f4532
--- /dev/null
+++ b/tools/validate.js
@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+// Headless solver harness for the 1MB roguelike.
+// Usage: node tools/validate.js [seedCount] [gameDir]
+//
+// Loads the real, unmodified game: evaluates 1mb/data.js and the inline
+// engine script from 1mb/index.html inside a stubbed browser environment,
+// then sweeps seedCount seeds x floors 1-7 asserting floorSolve() proves
+// each full floor completable (gate -> torch -> bow -> vault -> escape
+// room -> gate) — the same check the in-game validateSeeds() runs.
+"use strict"
+const fs = require("fs")
+const path = require("path")
+const vm = require("vm")
+
+const seedCount = Math.max(1, parseInt(process.argv[2], 10) || 200)
+const gameDir = process.argv[3] || path.join(__dirname, "..", "1mb")
+
+const html = fs.readFileSync(path.join(gameDir, "index.html"), "utf8")
+const dataSrc = fs.readFileSync(path.join(gameDir, "data.js"), "utf8")
+
+// The engine is the inline <script> block that follows the data.js include.
+const m = html.match(/<script src=data\.js><\/script>\s*<script>([\s\S]*?)<\/script>/)
+if (!m) {
+ console.error("validate: could not find the inline engine <script> after the data.js include in index.html")
+ process.exit(2)
+}
+const engineSrc = m[1]
+
+// Minimal inert DOM node — enough surface for the engine's load-time render().
+function el() {
+ return {
+ dataset: {},
+ textContent: "",
+ innerHTML: "",
+ className: "",
+ hidden: false,
+ appendChild() {},
+ onclick: null,
+ }
+}
+
+const sandbox = {
+ document: {
+ getElementById: () => el(),
+ createElement: () => el(),
+ },
+ addEventListener() {},
+ localStorage: {
+ _mem: Object.create(null),
+ getItem(k) { return k in this._mem ? this._mem[k] : null },
+ setItem(k, v) { this._mem[k] = String(v) },
+ removeItem(k) { delete this._mem[k] },
+ },
+ window: { AudioContext: function () { throw new Error("SFX must not run headless") } },
+ console,
+}
+const ctx = vm.createContext(sandbox)
+
+try {
+ vm.runInContext(dataSrc, ctx, { filename: "1mb/data.js" })
+ vm.runInContext(engineSrc, ctx, { filename: "1mb/index.html#engine" })
+} catch (e) {
+ console.error("validate: game code failed to evaluate headless — improve the stubs in this harness, do not touch the game.")
+ console.error(e && e.stack || e)
+ process.exit(2)
+}
+
+// Runs in the same context, so it sees the engine's top-level let/const
+// bindings (S, V) and functions (build, pickMods, escapeSolve) directly.
+const driver = vm.runInContext(`(function (seeds) {
+ const fails = []
+ for (const s0 of seeds) {
+ for (const level of [1, 2, 3, 4, 5, 6, 7]) {
+ S = { level, mods: pickMods(s0) }
+ if (!floorSolve(s0, level)) {
+ S = { level, mods: pickMods(s0) }
+ build(s0)
+ fails.push({ seed: s0, level, escape: V.escape, beast: V.beast, hall: V.hall, pit: V.pit, crack: V.crack, idol: V.idol })
+ }
+ }
+ }
+ return JSON.stringify(fails)
+})`, ctx, { filename: "validate-driver" })
+
+// Deterministic seed sweep using the game's own rng, same range as rollSeed().
+const seedsJson = vm.runInContext(
+ `JSON.stringify((() => { const r = rng(1), out = []; for (let i = 0; i < ${seedCount}; i++) out.push(Math.floor(r() * 1e9)); return out })())`,
+ ctx, { filename: "validate-seeds" })
+const seeds = JSON.parse(seedsJson)
+
+const t0 = Date.now()
+const fails = JSON.parse(driver(seeds))
+const elapsed = ((Date.now() - t0) / 1000).toFixed(1)
+const checks = seedCount * 7
+
+if (fails.length) {
+ console.error(`validate: ${fails.length}/${checks} floors NOT COMPLETABLE (${seedCount} seeds x floors 1-7, ${elapsed}s)`)
+ for (const f of fails.slice(0, 20)) {
+ console.error(` seed ${f.seed.toString(36)} floor ${f.level}: escape variant ${f.escape}, beast variant ${f.beast} (hall ${f.hall}, pit ${f.pit}, crack ${f.crack}, idol ${f.idol})`)
+ }
+ if (fails.length > 20) console.error(` ... and ${fails.length - 20} more`)
+ process.exit(1)
+}
+console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`)