Public: anyone can read this; the crew can change it.
forked from trevato/cs · proposes
The engine has one door: src/engine/index.ts is the surface, and nothing outside reaches past it #28
Check · Review · Land · Live done — Landed and live in production.
torque/cs-engine:torque/25-the-engine-has-one-door-src-engine-index → main 1 commit fork
26 files
changed +288
−88
README.md +17
| @@ -66,6 +66,23 @@ the rest. | ||
| 66 | 66 | them points at `src/client/`, at `src/rl/` or at a package. The last two take |
| 67 | 67 | their clock, their canvas and their window as parameters, so the tests drive |
| 68 | 68 | them with a stub instead of a browser. |
| 69 | - `src/engine/index.ts` — the door, and the rule is that a second game imports | |
| 70 | `src/engine/index.ts` and nothing deeper; inside the engine, modules import | |
| 71 | each other directly. The surface is re-exports and a docstring per module, | |
| 72 | nothing else: from `math` `v3`, `add`, `sub`, `scale`, `mul`, `dot`, `cross`, | |
| 73 | `length`, `length2d`, `dist`, `dist2d`, `normalize`, `lerp`, `clamp`, `rad`, | |
| 74 | `deg`, `angleDelta`, `angleVectors`, `mat4`, `multiply`, `perspective`, | |
| 75 | `viewMatrix`, `modelMatrix`, `rng`, `Vec3` and `Mat4`; from `trace` `rayBox`, | |
| 76 | `traceWorld`, `visible`, `segmentLength`, `Box` and `Hit`; from `grid` | |
| 77 | `inside`, `cellAt`, `worldOf`, `colOf`, `rowOf`, `mergeSolid`, `findPath` and | |
| 78 | `Grid`; from `mesh` `builder`, `addBox`, `finish`, `quadMesh`, `Builder` and | |
| 79 | `Mesh`; from `texture` `hash`, `noise`, `fbm`, `paintAtlas` and `Shader`; from | |
| 80 | `sound` `attenuate`, `createSoundDevice`, `Burst`, `Tone`, `SoundDevice` and | |
| 81 | `Ctor`; from `loop` `createLoop`, `Frames`, `Loop` and `LoopOptions`; and from | |
| 82 | `controls` `createControls`, `attachButtons` and the event and option types | |
| 83 | they are driven with. `boundary.test.ts` fails a deep import from outside, a | |
| 84 | module that is not behind the door, and any change to that list of names that | |
| 85 | is not also a change to the list written out in the test. | |
| 69 | 86 | - `src/client/` — the game, in modules named for one thing each: `map` (de_forge |
| 70 | 87 | itself: its areas, its crates, its cell alphabet and its spawns and sites, laid |
| 71 | 88 | over the engine's grid), `physics`, `weapons`, `combat`, `match` (the rules), |
scripts/train.ts +1 −1
| @@ -15,7 +15,7 @@ import { writeFileSync } from "node:fs"; | ||
| 15 | 15 | import { join } from "node:path"; |
| 16 | 16 | import { HEADS, decode } from "../src/client/action.ts"; |
| 17 | 17 | import { botThink, makeBrain } from "../src/client/bots.ts"; |
| 18 | import { rng } from "../src/engine/math.ts"; | |
| 18 | import { rng } from "../src/engine/index.ts"; | |
| 19 | 19 | import { OBS_SIZE } from "../src/client/observe.ts"; |
| 20 | 20 | import { |
| 21 | 21 | copyPolicy, createPolicy, forward, greedy, newActivations, pack, sample, unpack, |
src/client/audio.test.ts +1 −1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import { createAudio, loudness, shotTone } from "./audio.ts"; |
| 3 | import type { Burst, SoundDevice, Tone } from "../engine/sound.ts"; | |
| 3 | import type { Burst, SoundDevice, Tone } from "../engine/index.ts"; | |
| 4 | 4 | |
| 5 | 5 | /** A device that plays nothing and writes down everything. */ |
| 6 | 6 | function recorder(): SoundDevice & { bursts: Burst[]; tones: Tone[]; resumes: number } { |
src/client/audio.ts +1 −2
| @@ -5,8 +5,7 @@ | ||
| 5 | 5 | * only opens a context once the person has clicked something. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { dist, type Vec3 } from "../engine/math.ts"; | |
| 9 | import { attenuate, createSoundDevice, type SoundDevice } from "../engine/sound.ts"; | |
| 8 | import { attenuate, createSoundDevice, dist, type SoundDevice, type Vec3 } from "../engine/index.ts"; | |
| 10 | 9 | import { WEAPONS, type WeaponId } from "./weapons.ts"; |
| 11 | 10 | |
| 12 | 11 | export type Audio = { |
src/client/binding.ts +1 −1
| @@ -5,7 +5,7 @@ | ||
| 5 | 5 | * hands the simulation, so a person and a bot are the same kind of player. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { clamp } from "../engine/math.ts"; | |
| 8 | import { clamp } from "../engine/index.ts"; | |
| 9 | 9 | import { NO_INPUT, type Input } from "./world.ts"; |
| 10 | 10 | |
| 11 | 11 | /** `KeyboardEvent.code` to action. Today's bindings, moved rather than tuned. */ |
src/client/bots.ts +1 −1
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | */ |
| 13 | 13 | |
| 14 | 14 | import { decode, HEADS, STILL, type Intent } from "./action.ts"; |
| 15 | import { angleDelta, clamp, dist, rng, type Vec3 } from "../engine/math.ts"; | |
| 15 | import { angleDelta, clamp, dist, rng, type Vec3 } from "../engine/index.ts"; | |
| 16 | 16 | import * as map from "./map.ts"; |
| 17 | 17 | import { observe, OBS_SIZE, type Memory } from "./observe.ts"; |
| 18 | 18 | import { |
src/client/combat.test.ts +1 −1
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import { applyHit, hitboxes, traceBullet, type Actor } from "./combat.ts"; |
| 3 | 3 | import type { Box } from "./map.ts"; |
| 4 | import type { Vec3 } from "../engine/math.ts"; | |
| 4 | import type { Vec3 } from "../engine/index.ts"; | |
| 5 | 5 | import { WEAPONS } from "./weapons.ts"; |
| 6 | 6 | |
| 7 | 7 | const actor = (over: Partial<Actor> = {}): Actor => ({ |
src/client/combat.ts +1 −2
| @@ -5,9 +5,8 @@ | ||
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | 7 | import type { Box } from "./map.ts"; |
| 8 | import { dist, type Vec3 } from "../engine/math.ts"; | |
| 8 | import { dist, rayBox, traceWorld, type Vec3 } from "../engine/index.ts"; | |
| 9 | 9 | import { HULL } from "./physics.ts"; |
| 10 | import { rayBox, traceWorld } from "../engine/trace.ts"; | |
| 11 | 10 | import { computeDamage, type Hitgroup, type Weapon } from "./weapons.ts"; |
| 12 | 11 | |
| 13 | 12 | export type Actor = { |
src/client/hud.ts +1 −1
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | * it never changes anything. |
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | import { clamp } from "../engine/math.ts"; | |
| 7 | import { clamp } from "../engine/index.ts"; | |
| 8 | 8 | import { MOVE } from "./physics.ts"; |
| 9 | 9 | import { inaccuracy, WEAPONS } from "./weapons.ts"; |
| 10 | 10 | import { currentWeapon, useProgress, type World } from "./world.ts"; |
src/client/main.ts +1 −3
| @@ -9,9 +9,7 @@ | ||
| 9 | 9 | import { createAudio } from "./audio.ts"; |
| 10 | 10 | import { CS_KEYS, CS_MOUSE, CS_TAP, readIntent } from "./binding.ts"; |
| 11 | 11 | import { drawHud } from "./hud.ts"; |
| 12 | import { attachButtons, createControls } from "../engine/controls.ts"; | |
| 13 | import { createLoop } from "../engine/loop.ts"; | |
| 14 | import { clamp } from "../engine/math.ts"; | |
| 12 | import { attachButtons, clamp, createControls, createLoop } from "../engine/index.ts"; | |
| 15 | 13 | import { COMPETITIVE, SHORT, type Rules } from "./match.ts"; |
| 16 | 14 | import { createRenderer, type Camera } from "./renderer.ts"; |
| 17 | 15 | import { buyable } from "./weapons.ts"; |
src/client/map.ts +3 −4
| @@ -12,9 +12,8 @@ | ||
| 12 | 12 | * about this alphabet: what counts as solid or walkable goes in as a predicate. |
| 13 | 13 | */ |
| 14 | 14 | |
| 15 | import * as g from "../engine/grid.ts"; | |
| 16 | import type { Vec3 } from "../engine/math.ts"; | |
| 17 | import type { Box } from "../engine/trace.ts"; | |
| 15 | import * as g from "../engine/index.ts"; | |
| 16 | import type { Box, Vec3 } from "../engine/index.ts"; | |
| 18 | 17 | |
| 19 | 18 | export const CELL = 128; |
| 20 | 19 | export const GRID_W = 44; |
| @@ -92,7 +91,7 @@ export const rowOf = (y: number): number => g.rowOf(grid, y); | ||
| 92 | 91 | export const MAT = { FLOOR: 0, WALL: 1, CRATE: 2, SITE: 3, METAL: 4 } as const; |
| 93 | 92 | |
| 94 | 93 | /** The tracer owns the box; the map hands it out, because that is where it is written. */ |
| 95 | export type { Box } from "../engine/trace.ts"; | |
| 94 | export type { Box } from "../engine/index.ts"; | |
| 96 | 95 | |
| 97 | 96 | const mergeSolid = (match: (c: Cell) => boolean, z0: number, z1: number, mat: number): Box[] => |
| 98 | 97 | g.mergeSolid(grid, match, z0, z1, mat); |
src/client/materials.ts +1 −1
| @@ -6,7 +6,7 @@ | ||
| 6 | 6 | * painted into. |
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | import { fbm, hash, paintAtlas, type Shader } from "../engine/texture.ts"; | |
| 9 | import { fbm, hash, paintAtlas, type Shader } from "../engine/index.ts"; | |
| 10 | 10 | |
| 11 | 11 | export const TEX_SIZE = 128; |
| 12 | 12 |
src/client/models.ts +1 −1
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | * engine's box builder, which keeps the whole game one download. |
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | import { addBox, builder, finish, type Builder, type Mesh } from "../engine/mesh.ts"; | |
| 7 | import { addBox, builder, finish, type Builder, type Mesh } from "../engine/index.ts"; | |
| 8 | 8 | import type { Box } from "./map.ts"; |
| 9 | 9 | |
| 10 | 10 | /** The level, as one buffer. Fifty-odd boxes, three hundred quads, one draw. */ |
src/client/observe.ts +1 −1
| @@ -13,7 +13,7 @@ | ||
| 13 | 13 | */ |
| 14 | 14 | |
| 15 | 15 | import * as map from "./map.ts"; |
| 16 | import { angleDelta, clamp, dist, type Vec3 } from "../engine/math.ts"; | |
| 16 | import { angleDelta, clamp, dist, type Vec3 } from "../engine/index.ts"; | |
| 17 | 17 | import { HULL } from "./physics.ts"; |
| 18 | 18 | import { inaccuracy, WEAPONS } from "./weapons.ts"; |
| 19 | 19 | import type { Player, World } from "./world.ts"; |
src/client/physics.test.ts +1 −1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import type { Box } from "./map.ts"; |
| 3 | import type { Vec3 } from "../engine/math.ts"; | |
| 3 | import type { Vec3 } from "../engine/index.ts"; | |
| 4 | 4 | import { accelerate, airAccelerate, fallDamage, HULL, MOVE, move, type Body, type Wish } from "./physics.ts"; |
| 5 | 5 | |
| 6 | 6 | const ground: Box = { min: [-1000, -1000, -32], max: [1000, 1000, 0], mat: 0 }; |
src/client/physics.ts +1 −1
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | 11 | import type { Box } from "./map.ts"; |
| 12 | import { clamp, dot, normalize, type Vec3 } from "../engine/math.ts"; | |
| 12 | import { clamp, dot, normalize, type Vec3 } from "../engine/index.ts"; | |
| 13 | 13 | |
| 14 | 14 | export const HULL = { radius: 16, stand: 72, duck: 54, eyeStand: 64, eyeDuck: 46 }; |
| 15 | 15 |
src/client/policy.test.ts +1 −1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import { AXIS_BINS, decode, HEADS, PITCH_BINS, toInput, YAW_BINS } from "./action.ts"; |
| 3 | import { rng } from "../engine/math.ts"; | |
| 3 | import { rng } from "../engine/index.ts"; | |
| 4 | 4 | import { |
| 5 | 5 | createPolicy, forward, greedy, headOffset, logitCount, newActivations, pack, paramCount, |
| 6 | 6 | sample, softmax, unpack, type Shape, |
src/client/renderer.ts +1 −2
| @@ -5,8 +5,7 @@ | ||
| 5 | 5 | * the frame loop except the numbers that change. |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | import { multiply, perspective, viewMatrix, modelMatrix, type Vec3 } from "../engine/math.ts"; | |
| 9 | import type { Mesh } from "../engine/mesh.ts"; | |
| 8 | import { multiply, perspective, viewMatrix, modelMatrix, type Mesh, type Vec3 } from "../engine/index.ts"; | |
| 10 | 9 | import { bombMesh, levelMesh, playerMesh, viewModelMesh } from "./models.ts"; |
| 11 | 10 | import { LAYERS, TEX_SIZE, materialTextures } from "./materials.ts"; |
| 12 | 11 | import type { World } from "./world.ts"; |
src/client/weapons.ts +1 −1
| @@ -8,7 +8,7 @@ | ||
| 8 | 8 | * bullets every round, which is the property that makes spray control a skill. |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | import { rng } from "../engine/math.ts"; | |
| 11 | import { rng } from "../engine/index.ts"; | |
| 12 | 12 | |
| 13 | 13 | export type WeaponId = |
| 14 | 14 | | "knife" | "glock" | "usp" | "deagle" | "mp9" | "ak47" | "m4a4" | "awp"; |
src/client/world.ts +1 −2
| @@ -9,10 +9,9 @@ | ||
| 9 | 9 | |
| 10 | 10 | import { applyHit, traceBullet, type Actor } from "./combat.ts"; |
| 11 | 11 | import * as map from "./map.ts"; |
| 12 | import { angleVectors, angleDelta, clamp, dist, normalize, rng, sub, type Vec3 } from "../engine/math.ts"; | |
| 12 | import { angleVectors, angleDelta, clamp, dist, normalize, rng, sub, visible, type Vec3 } from "../engine/index.ts"; | |
| 13 | 13 | import * as rules from "./match.ts"; |
| 14 | 14 | import { eyeHeight, hullHeight, move, type Body, type Wish, MOVE } from "./physics.ts"; |
| 15 | import { visible } from "../engine/trace.ts"; | |
| 16 | 15 | import { |
| 17 | 16 | ARMOR_PRICE, DEFUSE_KIT_PRICE, HELMET_PRICE, WEAPONS, inaccuracy, recoilOffset, |
| 18 | 17 | type WeaponId, type Weapon, |
src/engine/boundary.test.ts +140
| @@ -8,6 +8,11 @@ | ||
| 8 | 8 | * |
| 9 | 9 | * A test in here may also reach for the runner and for node's builtins, which |
| 10 | 10 | * is how this file reads the sources; neither ships in the bundle. |
| 11 | * | |
| 12 | * The same boundary read from the other side is `index.ts`, the engine's one | |
| 13 | * door: nothing outside may name a deeper path, every module in here is behind | |
| 14 | * the door, and the names the door offers are written out below so that adding | |
| 15 | * or removing one is a line in a review rather than a silent widening. | |
| 11 | 16 | */ |
| 12 | 17 | |
| 13 | 18 | import { expect, test } from "bun:test"; |
| @@ -15,6 +20,10 @@ import { readFileSync, readdirSync } from "node:fs"; | ||
| 15 | 20 | import { join, relative, resolve } from "node:path"; |
| 16 | 21 | |
| 17 | 22 | const ENGINE = import.meta.dir; |
| 23 | const ROOT = resolve(ENGINE, "..", ".."); | |
| 24 | ||
| 25 | /** The directories outside the engine whose imports this test is about. */ | |
| 26 | const OUTSIDE = ["src", "scripts"]; | |
| 18 | 27 | |
| 19 | 28 | /** Every `.ts` under the engine, recursively, as paths relative to it. */ |
| 20 | 29 | export function sources(dir = ENGINE): string[] { |
| @@ -54,6 +63,49 @@ export function leavesEngine(file: string, specifier: string): string | null { | ||
| 54 | 63 | return null; |
| 55 | 64 | } |
| 56 | 65 | |
| 66 | /** Every `.ts` outside the engine, as paths relative to the repository root. */ | |
| 67 | export function outsideSources(dirs = OUTSIDE): string[] { | |
| 68 | const out: string[] = []; | |
| 69 | const walk = (dir: string): void => { | |
| 70 | for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) { | |
| 71 | const path = join(dir, entry.name); | |
| 72 | if (resolve(ROOT, path) === ENGINE) continue; | |
| 73 | if (entry.isDirectory()) walk(path); | |
| 74 | else if (entry.name.endsWith(".ts")) out.push(path); | |
| 75 | } | |
| 76 | }; | |
| 77 | for (const dir of dirs) walk(dir); | |
| 78 | return out.sort(); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Why this file outside the engine may not import this specifier, or null if it | |
| 83 | * may. Only one path into the engine is a path: `index.ts`, the door. | |
| 84 | */ | |
| 85 | export function reachesPastTheDoor(file: string, specifier: string): string | null { | |
| 86 | if (!specifier.startsWith(".")) return null; | |
| 87 | const target = resolve(ROOT, join(file, ".."), specifier); | |
| 88 | const inside = relative(ENGINE, target); | |
| 89 | if (inside === "" || inside.startsWith("..")) return null; | |
| 90 | if (inside === "index.ts") return null; | |
| 91 | return `${file} imports "${specifier}"; the engine's door is src/engine/index.ts`; | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * The names a barrel re-exports: everything inside the braces of an | |
| 96 | * `export { … } from` or `export type { … } from`, under the name it leaves by. | |
| 97 | */ | |
| 98 | export function reExportedNames(source: string): string[] { | |
| 99 | const out: string[] = []; | |
| 100 | const pattern = /\bexport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*["'][^"']+["']/g; | |
| 101 | for (let m = pattern.exec(source); m !== null; m = pattern.exec(source)) | |
| 102 | for (const clause of (m[1] as string).split(",")) { | |
| 103 | const name = clause.trim().replace(/^type\s+/, "").split(/\s+as\s+/).pop()?.trim(); | |
| 104 | if (name !== undefined && name !== "") out.push(name); | |
| 105 | } | |
| 106 | return out; | |
| 107 | } | |
| 108 | ||
| 57 | 109 | test("the engine has the sources this test is about", () => { |
| 58 | 110 | const files = sources(); |
| 59 | 111 | expect(files).toContain("math.ts"); |
| @@ -100,3 +152,91 @@ test("the rule catches the imports it exists to catch", () => { | ||
| 100 | 152 | expect(leavesEngine("boundary.test.ts", "node:path")).toBeNull(); |
| 101 | 153 | expect(leavesEngine("boundary.test.ts", "../client/map.ts")).toMatch(/outside/); |
| 102 | 154 | }); |
| 155 | ||
| 156 | test("nothing outside the engine imports a path past the door", () => { | |
| 157 | const offences: string[] = []; | |
| 158 | for (const file of outsideSources()) { | |
| 159 | const source = readFileSync(join(ROOT, file), "utf8"); | |
| 160 | for (const specifier of specifiersOf(source)) { | |
| 161 | const why = reachesPastTheDoor(file, specifier); | |
| 162 | if (why !== null) offences.push(why); | |
| 163 | } | |
| 164 | } | |
| 165 | expect(offences).toEqual([]); | |
| 166 | }); | |
| 167 | ||
| 168 | test("the door rule is about the door and nothing else", () => { | |
| 169 | expect(reachesPastTheDoor("src/client/world.ts", "../engine/math.ts")).toMatch(/door/); | |
| 170 | expect(reachesPastTheDoor("src/rl/arena.ts", "../engine/sub/deep.ts")).toMatch(/door/); | |
| 171 | expect(reachesPastTheDoor("src/client/world.ts", "../engine/index.ts")).toBeNull(); | |
| 172 | expect(reachesPastTheDoor("src/client/world.ts", "./physics.ts")).toBeNull(); | |
| 173 | expect(reachesPastTheDoor("src/client/world.ts", "bun:test")).toBeNull(); | |
| 174 | expect(reachesPastTheDoor("scripts/build.ts", "../src/engine/trace.ts")).toMatch(/door/); | |
| 175 | }); | |
| 176 | ||
| 177 | test("every module in the engine is behind the door, and the door holds nothing else", () => { | |
| 178 | const door = readFileSync(join(ENGINE, "index.ts"), "utf8"); | |
| 179 | const behind = new Set(specifiersOf(door).map((s) => s.replace(/^\.\//, ""))); | |
| 180 | const modules = sources().filter((f) => !f.endsWith(".test.ts") && f !== "index.ts"); | |
| 181 | expect(modules.length).toBeGreaterThan(0); | |
| 182 | expect([...behind].sort()).toEqual(modules); | |
| 183 | // Nothing of its own: the door is re-exports, a docstring and nothing else. | |
| 184 | const body = door.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); | |
| 185 | expect(body.replace(/\bexport\s+(?:type\s+)?\{[^}]*\}\s*from\s*["'][^"']+["'];/g, "").trim()).toBe(""); | |
| 186 | }); | |
| 187 | ||
| 188 | test("no module in the engine goes through the engine's own door", () => { | |
| 189 | const offences: string[] = []; | |
| 190 | for (const file of sources()) { | |
| 191 | // The door itself, and a test that reaches through it to check what it holds. | |
| 192 | if (file === "index.ts" || file.endsWith(".test.ts")) continue; | |
| 193 | const source = readFileSync(join(ENGINE, file), "utf8"); | |
| 194 | for (const specifier of specifiersOf(source)) | |
| 195 | if (relative(ENGINE, resolve(ENGINE, join(file, ".."), specifier)) === "index.ts") | |
| 196 | offences.push(`${file} imports the door; a package does not go through its own door`); | |
| 197 | } | |
| 198 | expect(offences).toEqual([]); | |
| 199 | }); | |
| 200 | ||
| 201 | /** | |
| 202 | * The surface. This list is the engine's public API written down: a name that | |
| 203 | * arrives, leaves or is renamed fails here and is read in the pull. Sorted the | |
| 204 | * way `Array.prototype.sort` sorts, so capitals come first. | |
| 205 | */ | |
| 206 | const SURFACE = [ | |
| 207 | "Box", "Builder", "Burst", "Button", "ButtonOptions", "Controls", "Ctor", "Frames", "Grid", | |
| 208 | "Hit", "Host", "KeyEventish", "Listens", "LockOwner", "Loop", "LoopOptions", "Mat4", "Mesh", | |
| 209 | "MouseEventish", "Options", "Shader", "SoundDevice", "Stick", "Surface", "Tone", | |
| 210 | "TouchEventish", "Touchish", "Vec3", "add", "addBox", "angleDelta", "angleVectors", | |
| 211 | "attachButtons", "attenuate", "builder", "cellAt", "clamp", "colOf", "createControls", | |
| 212 | "createLoop", "createSoundDevice", "cross", "deg", "dist", "dist2d", "dot", "fbm", | |
| 213 | "findPath", "finish", "hash", "inside", "length", "length2d", "lerp", "mat4", "mergeSolid", | |
| 214 | "modelMatrix", "mul", "multiply", "noise", "normalize", "paintAtlas", "perspective", | |
| 215 | "quadMesh", "rad", "rayBox", "rng", "rowOf", "scale", "segmentLength", "sub", "traceWorld", | |
| 216 | "v3", "viewMatrix", "visible", "worldOf", | |
| 217 | ]; | |
| 218 | ||
| 219 | test("the door offers exactly the names written down here", () => { | |
| 220 | const offered = reExportedNames(readFileSync(join(ENGINE, "index.ts"), "utf8")); | |
| 221 | expect(offered.length).toBe(new Set(offered).size); | |
| 222 | expect(offered.sort()).toEqual(SURFACE); | |
| 223 | }); | |
| 224 | ||
| 225 | test("the names come out of both halves of a re-export, alias and all", () => { | |
| 226 | const F = "fr" + "om"; | |
| 227 | const source = [ | |
| 228 | `export { a, b as c } ${F} "./one.ts";`, | |
| 229 | `export type { D, type E } ${F} "./two.ts";`, | |
| 230 | `export {\n f,\n g,\n} ${F} "./three.ts";`, | |
| 231 | ].join("\n"); | |
| 232 | expect(reExportedNames(source)).toEqual(["a", "c", "D", "E", "f", "g"]); | |
| 233 | }); | |
| 234 | ||
| 235 | test("the values the door re-exports are the ones the modules hold", async () => { | |
| 236 | const door = await import("./index.ts"); | |
| 237 | const math = await import("./math.ts"); | |
| 238 | expect(door.clamp).toBe(math.clamp); | |
| 239 | expect(door.rng).toBe(math.rng); | |
| 240 | const trace = await import("./trace.ts"); | |
| 241 | expect(door.rayBox).toBe(trace.rayBox); | |
| 242 | }); |
src/engine/index.ts added +50
| @@ -0,0 +1,50 @@ | ||
| 1 | /** | |
| 2 | * The engine's door. A package is what it exports, and this file is the whole | |
| 3 | * of it: everything under `src/engine/` reaches the game through here, and | |
| 4 | * nothing outside the engine names a deeper path. `boundary.test.ts` holds both | |
| 5 | * halves of that — the rule against deep imports, and the pinned list of the | |
| 6 | * names below, so a name arrives or leaves in a line a reviewer can see. | |
| 7 | * | |
| 8 | * There is no logic and no constant of its own in here, and no module it | |
| 9 | * re-exports does work at import time: importing the door must stay as cheap as | |
| 10 | * importing the one module you wanted. Inside the engine, modules import each | |
| 11 | * other directly — a package does not go through its own door. | |
| 12 | */ | |
| 13 | ||
| 14 | /** Vectors, matrices, angles and a seeded rng: the arithmetic under everything. */ | |
| 15 | export { | |
| 16 | add, angleDelta, angleVectors, clamp, cross, deg, dist, dist2d, dot, length, length2d, lerp, | |
| 17 | mat4, modelMatrix, mul, multiply, normalize, perspective, rad, rng, scale, sub, v3, viewMatrix, | |
| 18 | } from "./math.ts"; | |
| 19 | export type { Mat4, Vec3 } from "./math.ts"; | |
| 20 | ||
| 21 | /** Rays against axis-aligned boxes, and the `Box` they are traced against. */ | |
| 22 | export { rayBox, segmentLength, traceWorld, visible } from "./trace.ts"; | |
| 23 | export type { Box, Hit } from "./trace.ts"; | |
| 24 | ||
| 25 | /** A grid of cells of any type: cell-to-world arithmetic, greedy meshing, A*. */ | |
| 26 | export { cellAt, colOf, findPath, inside, mergeSolid, rowOf, worldOf } from "./grid.ts"; | |
| 27 | export type { Grid } from "./grid.ts"; | |
| 28 | ||
| 29 | /** A box builder: positions, normals, uvs and a layer, finished into a `Mesh`. */ | |
| 30 | export { addBox, builder, finish, quadMesh } from "./mesh.ts"; | |
| 31 | export type { Builder, Mesh } from "./mesh.ts"; | |
| 32 | ||
| 33 | /** Hashed value noise, fbm, and an atlas painted from a list of shaders. */ | |
| 34 | export { fbm, hash, noise, paintAtlas } from "./texture.ts"; | |
| 35 | export type { Shader } from "./texture.ts"; | |
| 36 | ||
| 37 | /** A lazy `AudioContext` with noise bursts, tones and distance attenuation. */ | |
| 38 | export { attenuate, createSoundDevice } from "./sound.ts"; | |
| 39 | export type { Burst, Ctor, SoundDevice, Tone } from "./sound.ts"; | |
| 40 | ||
| 41 | /** A fixed simulation step with a clamp on a long frame, clock supplied. */ | |
| 42 | export { createLoop } from "./loop.ts"; | |
| 43 | export type { Frames, Loop, LoopOptions } from "./loop.ts"; | |
| 44 | ||
| 45 | /** A bindings table in, held actions out: keyboard, pointer lock and touch. */ | |
| 46 | export { attachButtons, createControls } from "./controls.ts"; | |
| 47 | export type { | |
| 48 | Button, ButtonOptions, Controls, Host, KeyEventish, Listens, LockOwner, MouseEventish, | |
| 49 | Options, Stick, Surface, TouchEventish, Touchish, | |
| 50 | } from "./controls.ts"; |
src/rl/arena.ts +1 −1
| @@ -15,7 +15,7 @@ | ||
| 15 | 15 | import { decode, toInput, type Intent } from "../client/action.ts"; |
| 16 | 16 | import { makeBrain, botThink } from "../client/bots.ts"; |
| 17 | 17 | import * as map from "../client/map.ts"; |
| 18 | import { angleDelta, clamp, dist, rng, type Vec3 } from "../engine/math.ts"; | |
| 18 | import { angleDelta, clamp, dist, rng, type Vec3 } from "../engine/index.ts"; | |
| 19 | 19 | import { observe, OBS_SIZE, type Memory } from "../client/observe.ts"; |
| 20 | 20 | import type { Input, Player, World } from "../client/world.ts"; |
| 21 | 21 | import { aimAt, applyInput, bodyCentre, canSee, createWorld, NO_INPUT } from "../client/world.ts"; |
src/rl/ppo.test.ts +1 −1
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | import { rng } from "../engine/math.ts"; | |
| 2 | import { rng } from "../engine/index.ts"; | |
| 3 | 3 | import { |
| 4 | 4 | createPolicy, forward, newActivations, tensorsOf, type Policy, type Shape, |
| 5 | 5 | } from "../client/policy.ts"; |
static/app.3fa56501a8.js added +58
Diff not shown — a line 57,166 characters wide (limit 2,000). +58
View filestatic/app.90bb5b2a7e.js deleted −58
Diff not shown — a line 57,162 characters wide (limit 2,000). −58
View file
new above
offline