diff options
Diffstat (limited to 'tools')
| -rwxr-xr-x | tools/check-size.sh | 45 | ||||
| -rwxr-xr-x | tools/check.sh | 13 | ||||
| -rwxr-xr-x | tools/pre-commit | 7 | ||||
| -rwxr-xr-x | tools/validate.js | 104 |
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)`) |
