krz/micro-roguelike
A micro roguelike web game in 1kb and 1mb versions.
clone: git clone https://gitbay.org/krz/micro-roguelike.git
1#!/usr/bin/env node
2// Headless solver harness for the 1MB roguelike.
3// Usage: node tools/validate.js [seedCount] [gameDir]
4//
5// Loads the real, unmodified game: evaluates 1mb/data.js and the inline
6// engine script from 1mb/index.html inside a stubbed browser environment,
7// then sweeps seedCount seeds x floors 1-7 asserting floorSolve() proves
8// each full floor completable (gate -> torch -> bow -> vault -> escape
9// room -> gate) — the same check the in-game validateSeeds() runs.
10"use strict"
11const fs = require("fs")
12const path = require("path")
13const vm = require("vm")
14
15const seedCount = Math.max(1, parseInt(process.argv[2], 10) || 200)
16const gameDir = process.argv[3] || path.join(__dirname, "..", "1mb")
17
18const html = fs.readFileSync(path.join(gameDir, "index.html"), "utf8")
19const dataSrc = fs.readFileSync(path.join(gameDir, "data.js"), "utf8")
20
21// The engine is the inline <script> block that follows the data.js include.
22const m = html.match(/<script src=data\.js><\/script>\s*<script>([\s\S]*?)<\/script>/)
23if (!m) {
24 console.error("validate: could not find the inline engine <script> after the data.js include in index.html")
25 process.exit(2)
26}
27const engineSrc = m[1]
28
29// Minimal inert DOM node — enough surface for the engine's load-time render().
30function el() {
31 return {
32 dataset: {},
33 textContent: "",
34 innerHTML: "",
35 className: "",
36 hidden: false,
37 appendChild() {},
38 onclick: null,
39 }
40}
41
42const sandbox = {
43 document: {
44 getElementById: () => el(),
45 createElement: () => el(),
46 },
47 addEventListener() {},
48 localStorage: {
49 _mem: Object.create(null),
50 getItem(k) { return k in this._mem ? this._mem[k] : null },
51 setItem(k, v) { this._mem[k] = String(v) },
52 removeItem(k) { delete this._mem[k] },
53 },
54 window: { AudioContext: function () { throw new Error("SFX must not run headless") } },
55 console,
56}
57const ctx = vm.createContext(sandbox)
58
59try {
60 vm.runInContext(dataSrc, ctx, { filename: "1mb/data.js" })
61 vm.runInContext(engineSrc, ctx, { filename: "1mb/index.html#engine" })
62} catch (e) {
63 console.error("validate: game code failed to evaluate headless — improve the stubs in this harness, do not touch the game.")
64 console.error(e && e.stack || e)
65 process.exit(2)
66}
67
68// Runs in the same context, so it sees the engine's top-level let/const
69// bindings (S, V) and functions (build, pickMods, escapeSolve) directly.
70const driver = vm.runInContext(`(function (seeds) {
71 const fails = []
72 for (const s0 of seeds) {
73 for (const level of [1, 2, 3, 4, 5, 6, 7]) {
74 S = { level, mods: pickMods(s0) }
75 if (!floorSolve(s0, level)) {
76 S = { level, mods: pickMods(s0) }
77 build(s0)
78 fails.push({ seed: s0, level, escape: V.escape, beast: V.beast, hall: V.hall, pit: V.pit, crack: V.crack, idol: V.idol })
79 }
80 }
81 }
82 return JSON.stringify(fails)
83})`, ctx, { filename: "validate-driver" })
84
85// Deterministic seed sweep using the game's own rng, same range as rollSeed().
86const seedsJson = vm.runInContext(
87 `JSON.stringify((() => { const r = rng(1), out = []; for (let i = 0; i < ${seedCount}; i++) out.push(Math.floor(r() * 1e9)); return out })())`,
88 ctx, { filename: "validate-seeds" })
89const seeds = JSON.parse(seedsJson)
90
91const t0 = Date.now()
92const fails = JSON.parse(driver(seeds))
93const elapsed = ((Date.now() - t0) / 1000).toFixed(1)
94const checks = seedCount * 7
95
96if (fails.length) {
97 console.error(`validate: ${fails.length}/${checks} floors NOT COMPLETABLE (${seedCount} seeds x floors 1-7, ${elapsed}s)`)
98 for (const f of fails.slice(0, 20)) {
99 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})`)
100 }
101 if (fails.length > 20) console.error(` ... and ${fails.length - 20} more`)
102 process.exit(1)
103}
104console.log(`validate: OK — ${checks} full floors solvable (${seedCount} seeds x floors 1-7, ${elapsed}s)`)