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

forked from benjivers/val

workbenchthis machine
Files 0da7ffcb37d59bc652a0ef6ea8768f5a4ee103ff
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 { 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
const inEngine = (path: string): boolean => ENGINE.some((dir) => resolve(path).startsWith(dir + "/"));
19
20
/** Every import specifier in a file: static, re-exported, or dynamic. */
21
function importsOf(source: string): string[] {
22
  const out: string[] = [];
23
  for (const match of source.matchAll(/^\s*(?:import|export)\b[^;]*?\bfrom\s+["']([^"']+)["']/gm)) out.push(match[1]!);
24
  for (const match of source.matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g)) out.push(match[1]!);
25
  return out;
26
}
27
28
const files = ENGINE.flatMap((dir) =>
29
  readdirSync(dir)
30
    .filter((name) => /\.(ts|js)$/.test(name))
31
    .map((name) => join(dir, name)),
32
);
33
34
test("the engine folders exist and hold the pieces a game stands on", () => {
35
  const names = files.map((f) => f.split("/").slice(-2).join("/"));
36
  for (const need of [
37
    "engine/glide.ts",
38
    "engine/aim.ts",
39
    "engine/grid.ts",
40
    "engine/arena.ts",
41
    "engine/interp.js",
42
    "engine/predict.js",
43
    "engine/presence.js",
44
    "engine/yaw.js",
45
    "engine/steer.js",
46
    "engine/raycast.js",
47
    "engine/scene.js",
48
    "engine/slices.js",
49
    "engine/resolution.js",
50
    "engine/budget.js",
51
    "engine/topdown.js",
52
    "engine/gun.js",
53
  ])
54
    expect(names).toContain(need);
55
});
56
57
test("no engine module, and no engine test, imports a game module", () => {
58
  const crossings: string[] = [];
59
  for (const file of files) {
60
    for (const spec of importsOf(readFileSync(file, "utf8"))) {
61
      if (/^(bun|node):/.test(spec)) continue;
62
      if (!spec.startsWith(".")) {
63
        crossings.push(`${file}: ${spec} (a package)`);
64
        continue;
65
      }
66
      const target = resolve(dirname(file), spec);
67
      if (!inEngine(target)) crossings.push(`${file.slice(ROOT.length + 1)} -> ${spec}`);
68
    }
69
  }
70
  expect(crossings).toEqual([]);
71
});
72
73
test("the engine does not know what a spike is", () => {
74
  // The words of this game. An engine file that says one of them has a hole
75
  // that should have been a parameter.
76
  const words = /\b(spike|agent|credits|molly|defus\w*|plant\w*|orb|valorant)\b/i;
77
  const said: string[] = [];
78
  for (const file of files) {
79
    if (file.endsWith(".test.ts")) continue;
80
    const source = readFileSync(file, "utf8")
81
      // Comments may explain things by example; the code may not.
82
      .replace(/\/\*[\s\S]*?\*\//g, "")
83
      .replace(/\/\/.*$/gm, "");
84
    const hit = source.match(words);
85
    if (hit) said.push(`${file.slice(ROOT.length + 1)}: ${hit[0]}`);
86
  }
87
  expect(said).toEqual([]);
88
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts