| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 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 |
|
| 20 |
|
| 21 |
|
| 22 |
|
| 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 |
|
| 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 |
"engine/sides.js", |
| 63 |
]) |
| 64 |
expect(names).toContain(need); |
| 65 |
}); |
| 66 |
|
| 67 |
test("no engine module, and no engine test, imports a game module", () => { |
| 68 |
const crossings: string[] = []; |
| 69 |
for (const file of files) { |
| 70 |
for (const spec of importsOf(readFileSync(file, "utf8"))) { |
| 71 |
if (/^(bun|node):/.test(spec)) continue; |
| 72 |
if (!spec.startsWith(".")) { |
| 73 |
crossings.push(`${file}: ${spec} (a package)`); |
| 74 |
continue; |
| 75 |
} |
| 76 |
const target = resolve(dirname(file), spec); |
| 77 |
if (!inEngine(target)) crossings.push(`${file.slice(ROOT.length + 1)} -> ${spec}`); |
| 78 |
} |
| 79 |
} |
| 80 |
expect(crossings).toEqual([]); |
| 81 |
}); |
| 82 |
|
| 83 |
test.skipIf(gameWords.length === 0)("the engine does not know what this game calls things", () => { |
| 84 |
|
| 85 |
|
| 86 |
|
| 87 |
|
| 88 |
const words = new RegExp(`\\b(${gameWords.join("|")})\\b`, "i"); |
| 89 |
const said: string[] = []; |
| 90 |
for (const file of files) { |
| 91 |
if (file.endsWith(".test.ts")) continue; |
| 92 |
const source = readFileSync(file, "utf8") |
| 93 |
|
| 94 |
.replace(/\/\*[\s\S]*?\*\//g, "") |
| 95 |
.replace(/\/\/.*$/gm, ""); |
| 96 |
const hit = source.match(words); |
| 97 |
if (hit) said.push(`${file.slice(ROOT.length + 1)}: ${hit[0]}`); |
| 98 |
} |
| 99 |
expect(said).toEqual([]); |
| 100 |
}); |