summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/check-size.sh45
-rwxr-xr-xtools/check.sh13
-rwxr-xr-xtools/pre-commit7
-rwxr-xr-xtools/validate.js104
4 files changed, 169 insertions, 0 deletions
diff --git a/tools/check-size.sh b/tools/check-size.sh
new file mode 100755
index 0000000..ed28157
--- /dev/null
+++ b/tools/check-size.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# Byte-budget gate for the 1MB roguelike.
+# Usage: tools/check-size.sh [dir] (dir defaults to the repo's 1mb/)
+# Prints a per-file ledger, the total, and percent of budget.
+# Exit 1 if the total exceeds 1,048,576 bytes; loud warning above 95%.
+set -eu
+
+dir=${1:-$(cd "$(dirname "$0")/.." && pwd)/1mb}
+budget=1048576
+
+if [ ! -d "$dir" ]; then
+ echo "check-size: no such directory: $dir" >&2
+ exit 2
+fi
+
+total=0
+echo "byte ledger for $dir"
+echo "----------------------------------------"
+# Newline-separated iteration so paths with spaces survive.
+oldifs=$IFS
+IFS='
+'
+for f in $(find "$dir" -type f | sort); do
+ bytes=$(wc -c < "$f")
+ bytes=$((bytes + 0))
+ total=$((total + bytes))
+ printf '%10d %s\n' "$bytes" "${f#"$dir"/}"
+done
+IFS=$oldifs
+
+# Percent to one decimal place using integer math only.
+pct10=$((total * 1000 / budget))
+echo "----------------------------------------"
+printf '%10d total (%d.%d%% of %d)\n' "$total" $((pct10 / 10)) $((pct10 % 10)) "$budget"
+
+if [ "$total" -gt "$budget" ]; then
+ echo "FAIL: over budget by $((total - budget)) bytes" >&2
+ exit 1
+fi
+if [ "$pct10" -ge 950 ]; then
+ echo ""
+ echo "!!! WARNING: past 95% of the 1 MiB budget ($((budget - total)) bytes left) !!!"
+ echo ""
+fi
+echo "OK: $((budget - total)) bytes remaining"
diff --git a/tools/check.sh b/tools/check.sh
new file mode 100755
index 0000000..0fa90f1
--- /dev/null
+++ b/tools/check.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# Single verification entry point for the 1MB roguelike:
+# size budget gate + headless solver sweep.
+# Usage: tools/check.sh [seedCount] (seedCount defaults to 200)
+set -eu
+
+root=$(cd "$(dirname "$0")/.." && pwd)
+
+"$root/tools/check-size.sh"
+echo ""
+node "$root/tools/validate.js" "${1:-200}"
+echo ""
+echo "check: all gates passed"
diff --git a/tools/pre-commit b/tools/pre-commit
new file mode 100755
index 0000000..1cf06b6
--- /dev/null
+++ b/tools/pre-commit
@@ -0,0 +1,7 @@
+#!/bin/sh
+# Optional git pre-commit hook: run the 1MB budget + solver gate.
+# Install (from the repo root):
+# ln -s ../../tools/pre-commit .git/hooks/pre-commit
+set -eu
+root=$(git rev-parse --show-toplevel)
+exec "$root/tools/check.sh"
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)`)