wren/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files wren/38-walkdistance-say-which-argument-is-cache
| 1 | /** |
| 2 | * The property that makes the engine folders copyable at all: nothing under |
| 3 | * `src/engine/` or `static/engine/` imports anything outside them. A module |
| 4 | * that reached for the game's map, agents or match would come along with the |
| 5 | * copy and bring the whole game with it. |
| 6 | * |
| 7 | * Tests count: an engine test that needs the game to run is a test the next |
| 8 | * game cannot take with it. |
| 9 | */ |
| 10 | |
| 11 | import { expect, test } from "bun:test"; |
| 12 | import { existsSync, readdirSync, readFileSync } from "node:fs"; |
| 13 | import { dirname, join, resolve } from "node:path"; |
| 14 | |
| 15 | const ROOT = resolve(import.meta.dir, "..", ".."); |
| 16 | const ENGINE = [join(ROOT, "src", "engine"), join(ROOT, "static", "engine")]; |
| 17 | |
| 18 | /** |
| 19 | * The game's own nouns, read as data rather than imported as code — an |
| 20 | * import from here would itself be a crossing the test below refuses. A |
| 21 | * copy of the engine that lands in a fresh game starts with no such file |
| 22 | * and so no such guard, until that game writes one down. |
| 23 | */ |
| 24 | const WORDS_FILE = join(ROOT, "src", "words.json"); |
| 25 | const gameWords: string[] = existsSync(WORDS_FILE) ? JSON.parse(readFileSync(WORDS_FILE, "utf8")) : []; |
| 26 | |
| 27 | const inEngine = (path: string): boolean => ENGINE.some((dir) => resolve(path).startsWith(dir + "/")); |
| 28 | |
| 29 | /** Every import specifier in a file: static, re-exported, or dynamic. */ |
| 30 | function importsOf(source: string): string[] { |
| 31 | const out: string[] = []; |
| 32 | for (const match of source.matchAll(/^\s*(?:import|export)\b[^;]*?\bfrom\s+["']([^"']+)["']/gm)) out.push(match[1]!); |
| 33 | for (const match of source.matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g)) out.push(match[1]!); |
| 34 | return out; |
| 35 | } |
| 36 | |
| 37 | const files = ENGINE.flatMap((dir) => |
| 38 | readdirSync(dir) |
| 39 | .filter((name) => /\.(ts|js)$/.test(name)) |
| 40 | .map((name) => join(dir, name)), |
| 41 | ); |
| 42 | |
| 43 | test("the engine folders exist and hold the pieces a game stands on", () => { |
| 44 | const names = files.map((f) => f.split("/").slice(-2).join("/")); |
| 45 | for (const need of [ |
| 46 | "engine/glide.ts", |
| 47 | "engine/aim.ts", |
| 48 | "engine/grid.ts", |
| 49 | "engine/arena.ts", |
| 50 | "engine/interp.js", |
| 51 | "engine/predict.js", |
| 52 | "engine/presence.js", |
| 53 | "engine/yaw.js", |
| 54 | "engine/steer.js", |
| 55 | "engine/raycast.js", |
| 56 | "engine/scene.js", |
| 57 | "engine/slices.js", |
| 58 | "engine/resolution.js", |
| 59 | "engine/budget.js", |
| 60 | "engine/topdown.js", |
| 61 | "engine/gun.js", |
| 62 | ]) |
| 63 | expect(names).toContain(need); |
| 64 | }); |
| 65 | |
| 66 | test("no engine module, and no engine test, imports a game module", () => { |
| 67 | const crossings: string[] = []; |
| 68 | for (const file of files) { |
| 69 | for (const spec of importsOf(readFileSync(file, "utf8"))) { |
| 70 | if (/^(bun|node):/.test(spec)) continue; |
| 71 | if (!spec.startsWith(".")) { |
| 72 | crossings.push(`${file}: ${spec} (a package)`); |
| 73 | continue; |
| 74 | } |
| 75 | const target = resolve(dirname(file), spec); |
| 76 | if (!inEngine(target)) crossings.push(`${file.slice(ROOT.length + 1)} -> ${spec}`); |
| 77 | } |
| 78 | } |
| 79 | expect(crossings).toEqual([]); |
| 80 | }); |
| 81 | |
| 82 | test.skipIf(gameWords.length === 0)("the engine does not know what this game calls things", () => { |
| 83 | // The words are the game's own, from src/words.json, never hardcoded here: |
| 84 | // a game that legitimately has a "spike" should not inherit a guard against |
| 85 | // a game it has never played. An engine file that says one of them has a |
| 86 | // hole that should have been a parameter. |
| 87 | const words = new RegExp(`\\b(${gameWords.join("|")})\\b`, "i"); |
| 88 | const said: string[] = []; |
| 89 | for (const file of files) { |
| 90 | if (file.endsWith(".test.ts")) continue; |
| 91 | const source = readFileSync(file, "utf8") |
| 92 | // Comments may explain things by example; the code may not. |
| 93 | .replace(/\/\*[\s\S]*?\*\//g, "") |
| 94 | .replace(/\/\/.*$/gm, ""); |
| 95 | const hit = source.match(words); |
| 96 | if (hit) said.push(`${file.slice(ROOT.length + 1)}: ${hit[0]}`); |
| 97 | } |
| 98 | expect(said).toEqual([]); |
| 99 | }); |