pike/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine
Files 984794ed22fb89fd183fee6ad0e3c1184486a759
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
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts