Public: anyone can read this; the crew can change it.
forked from trevato/cs · proposes
Start the engine package: math and tracing move to src/engine/, and a test keeps the game out #6
Check · Review · Land · Live done — Landed and live in production.
torque/cs-engine:torque/2-start-the-engine-package-math-and-tracin → main 1 commit fork
25 files
changed +193
−24
README.md +6 −1
| @@ -49,7 +49,12 @@ the rest. | ||
| 49 | 49 | |
| 50 | 50 | ## Shape |
| 51 | 51 | |
| 52 | - `src/client/` — the game, in modules named for one thing each: `math`, `map`, | |
| 52 | - `src/engine/` — the part a second game could use, and the rule is that it never | |
| 53 | imports the game: `math` (vectors, matrices, angles, a seeded rng) and `trace` | |
| 54 | (rays against boxes, and the `Box` they are traced against). `boundary.test.ts` | |
| 55 | reads the imports of every file in here and fails if one of them points at | |
| 56 | `src/client/`, at `src/rl/` or at a package. | |
| 57 | - `src/client/` — the game, in modules named for one thing each: `map`, | |
| 53 | 58 | `physics`, `weapons`, `combat`, `match` (the rules), `world` (the simulation), |
| 54 | 59 | `bots`, `observe`, `action`, `policy`, `weights`, `models`, `textures`, |
| 55 | 60 | `renderer`, `hud`, `input`, `audio`, `main`. Everything except the last five is |
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/client/math.ts"; | |
| 18 | import { rng } from "../src/engine/math.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.ts +1 −1
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | * context is only created once the person has clicked something. |
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | import { dist, type Vec3 } from "./math.ts"; | |
| 7 | import { dist, type Vec3 } from "../engine/math.ts"; | |
| 8 | 8 | import { WEAPONS, type WeaponId } from "./weapons.ts"; |
| 9 | 9 | |
| 10 | 10 | export type Audio = { |
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 "./math.ts"; | |
| 15 | import { angleDelta, clamp, dist, rng, type Vec3 } from "../engine/math.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 "./math.ts"; | |
| 4 | import type { Vec3 } from "../engine/math.ts"; | |
| 5 | 5 | import { WEAPONS } from "./weapons.ts"; |
| 6 | 6 | |
| 7 | 7 | const actor = (over: Partial<Actor> = {}): Actor => ({ |
src/client/combat.ts +2 −2
| @@ -5,9 +5,9 @@ | ||
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | 7 | import type { Box } from "./map.ts"; |
| 8 | import { dist, type Vec3 } from "./math.ts"; | |
| 8 | import { dist, type Vec3 } from "../engine/math.ts"; | |
| 9 | 9 | import { HULL } from "./physics.ts"; |
| 10 | import { rayBox, traceWorld } from "./trace.ts"; | |
| 10 | import { rayBox, traceWorld } from "../engine/trace.ts"; | |
| 11 | 11 | import { computeDamage, type Hitgroup, type Weapon } from "./weapons.ts"; |
| 12 | 12 | |
| 13 | 13 | 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 "./math.ts"; | |
| 7 | import { clamp } from "../engine/math.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/input.ts +1 −1
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | * takes from a bot, and hands over accumulated mouse movement once a frame. |
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | import { clamp } from "./math.ts"; | |
| 7 | import { clamp } from "../engine/math.ts"; | |
| 8 | 8 | import { NO_INPUT, type Input } from "./world.ts"; |
| 9 | 9 | |
| 10 | 10 | export type Controls = { |
src/client/main.ts +1 −1
| @@ -8,7 +8,7 @@ | ||
| 8 | 8 | import { createAudio } from "./audio.ts"; |
| 9 | 9 | import { drawHud } from "./hud.ts"; |
| 10 | 10 | import { attachButtons, createControls } from "./input.ts"; |
| 11 | import { clamp } from "./math.ts"; | |
| 11 | import { clamp } from "../engine/math.ts"; | |
| 12 | 12 | import { COMPETITIVE, SHORT, type Rules } from "./match.ts"; |
| 13 | 13 | import { createRenderer, type Camera } from "./renderer.ts"; |
| 14 | 14 | import { buyable } from "./weapons.ts"; |
src/client/map.ts +5 −2
| @@ -8,7 +8,8 @@ | ||
| 8 | 8 | * corridor and a left-hand tunnel, T at the bottom of the grid, CT at the top. |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | import type { Vec3 } from "./math.ts"; | |
| 11 | import type { Vec3 } from "../engine/math.ts"; | |
| 12 | import type { Box } from "../engine/trace.ts"; | |
| 12 | 13 | |
| 13 | 14 | export const CELL = 128; |
| 14 | 15 | export const GRID_W = 44; |
| @@ -85,7 +86,9 @@ export const rowOf = (y: number): number => Math.floor(GRID_H / 2 - y / CELL); | ||
| 85 | 86 | // --- geometry ------------------------------------------------------------- |
| 86 | 87 | |
| 87 | 88 | export const MAT = { FLOOR: 0, WALL: 1, CRATE: 2, SITE: 3, METAL: 4 } as const; |
| 88 | export type Box = { min: Vec3; max: Vec3; mat: number }; | |
| 89 | ||
| 90 | /** The tracer owns the box; the map hands it out, because that is where it is written. */ | |
| 91 | export type { Box } from "../engine/trace.ts"; | |
| 89 | 92 | |
| 90 | 93 | /** |
| 91 | 94 | * Greedy meshing: merge runs of identical solid cells across a row, then merge |
src/client/models.ts +1 −1
| @@ -5,7 +5,7 @@ | ||
| 5 | 5 | */ |
| 6 | 6 | |
| 7 | 7 | import type { Box } from "./map.ts"; |
| 8 | import type { Vec3 } from "./math.ts"; | |
| 8 | import type { Vec3 } from "../engine/math.ts"; | |
| 9 | 9 | |
| 10 | 10 | export type Mesh = { |
| 11 | 11 | position: Float32Array; |
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 "./math.ts"; | |
| 16 | import { angleDelta, clamp, dist, type Vec3 } from "../engine/math.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 "./math.ts"; | |
| 3 | import type { Vec3 } from "../engine/math.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 "./math.ts"; | |
| 12 | import { clamp, dot, normalize, type Vec3 } from "../engine/math.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 "./math.ts"; | |
| 3 | import { rng } from "../engine/math.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 −1
| @@ -5,7 +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 "./math.ts"; | |
| 8 | import { multiply, perspective, viewMatrix, modelMatrix, type Vec3 } from "../engine/math.ts"; | |
| 9 | 9 | import { bombMesh, levelMesh, playerMesh, viewModelMesh, type Mesh } from "./models.ts"; |
| 10 | 10 | import { LAYERS, TEX_SIZE, materialTextures } from "./textures.ts"; |
| 11 | 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 "./math.ts"; | |
| 11 | import { rng } from "../engine/math.ts"; | |
| 12 | 12 | |
| 13 | 13 | export type WeaponId = |
| 14 | 14 | | "knife" | "glock" | "usp" | "deagle" | "mp9" | "ak47" | "m4a4" | "awp"; |
src/client/world.ts +2 −2
| @@ -9,10 +9,10 @@ | ||
| 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 "./math.ts"; | |
| 12 | import { angleVectors, angleDelta, clamp, dist, normalize, rng, sub, type Vec3 } from "../engine/math.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 "./trace.ts"; | |
| 15 | import { visible } from "../engine/trace.ts"; | |
| 16 | 16 | import { |
| 17 | 17 | ARMOR_PRICE, DEFUSE_KIT_PRICE, HELMET_PRICE, WEAPONS, inaccuracy, recoilOffset, |
| 18 | 18 | type WeaponId, type Weapon, |
src/engine/boundary.test.ts added +102
| @@ -0,0 +1,102 @@ | ||
| 1 | /** | |
| 2 | * The boundary. `src/engine/` is the part a second game could use, so a module | |
| 3 | * in here may import the engine and nothing else: not the game in | |
| 4 | * `src/client/`, not the training in `src/rl/`, and no package — the engine has | |
| 5 | * nothing to install. This test reads the sources rather than trusting the | |
| 6 | * convention, because an import is one line and nobody reviews a line that | |
| 7 | * compiles. | |
| 8 | * | |
| 9 | * A test in here may also reach for the runner and for node's builtins, which | |
| 10 | * is how this file reads the sources; neither ships in the bundle. | |
| 11 | */ | |
| 12 | ||
| 13 | import { expect, test } from "bun:test"; | |
| 14 | import { readFileSync, readdirSync } from "node:fs"; | |
| 15 | import { join, relative, resolve } from "node:path"; | |
| 16 | ||
| 17 | const ENGINE = import.meta.dir; | |
| 18 | ||
| 19 | /** Every `.ts` under the engine, recursively, as paths relative to it. */ | |
| 20 | export function sources(dir = ENGINE): string[] { | |
| 21 | const out: string[] = []; | |
| 22 | for (const entry of readdirSync(dir, { withFileTypes: true })) { | |
| 23 | const path = join(dir, entry.name); | |
| 24 | if (entry.isDirectory()) out.push(...sources(path)); | |
| 25 | else if (entry.name.endsWith(".ts")) out.push(relative(ENGINE, path)); | |
| 26 | } | |
| 27 | return out.sort(); | |
| 28 | } | |
| 29 | ||
| 30 | /** | |
| 31 | * The specifiers a file imports. Anything after a `from` keyword, which covers | |
| 32 | * a plain import, a type-only import and a re-export; a side-effect import with | |
| 33 | * no names; and a dynamic import call. A comment that happens to look like one | |
| 34 | * is a false positive we can live with — it fails loudly and is a second to | |
| 35 | * delete, as this docstring found out. | |
| 36 | */ | |
| 37 | export function specifiersOf(source: string): string[] { | |
| 38 | const out: string[] = []; | |
| 39 | const pattern = /(?:\bfrom\s*|\bimport\s*\(?\s*)["']([^"']+)["']/g; | |
| 40 | for (let m = pattern.exec(source); m !== null; m = pattern.exec(source)) | |
| 41 | out.push(m[1] as string); | |
| 42 | return out; | |
| 43 | } | |
| 44 | ||
| 45 | /** Why this file may not import this specifier, or null if it may. */ | |
| 46 | export function leavesEngine(file: string, specifier: string): string | null { | |
| 47 | const isTest = file.endsWith(".test.ts"); | |
| 48 | if (!specifier.startsWith(".")) { | |
| 49 | if (isTest && (specifier === "bun:test" || specifier.startsWith("node:"))) return null; | |
| 50 | return `"${specifier}" is not a path inside the engine`; | |
| 51 | } | |
| 52 | const inside = relative(ENGINE, resolve(ENGINE, join(file, ".."), specifier)); | |
| 53 | if (inside === "" || inside.startsWith("..")) return `"${specifier}" resolves outside src/engine/`; | |
| 54 | return null; | |
| 55 | } | |
| 56 | ||
| 57 | test("the engine has the sources this test is about", () => { | |
| 58 | const files = sources(); | |
| 59 | expect(files).toContain("math.ts"); | |
| 60 | expect(files).toContain("trace.ts"); | |
| 61 | }); | |
| 62 | ||
| 63 | test("no engine module imports the game, the training, or a package", () => { | |
| 64 | const offences: string[] = []; | |
| 65 | for (const file of sources()) { | |
| 66 | const source = readFileSync(join(ENGINE, file), "utf8"); | |
| 67 | for (const specifier of specifiersOf(source)) { | |
| 68 | const why = leavesEngine(file, specifier); | |
| 69 | if (why !== null) offences.push(`${file}: ${why}`); | |
| 70 | } | |
| 71 | } | |
| 72 | expect(offences).toEqual([]); | |
| 73 | }); | |
| 74 | ||
| 75 | test("the specifiers come out of every shape an import takes", () => { | |
| 76 | // The keyword is spelled at run time so this fixture is not itself an import | |
| 77 | // the test above would read out of this file and hold against it. | |
| 78 | const F = "fr" + "om"; | |
| 79 | const source = [ | |
| 80 | `import { a } ${F} "./one.ts";`, | |
| 81 | `import type { B } ${F} './two.ts';`, | |
| 82 | `export type { C } ${F} "../client/map.ts";`, | |
| 83 | `import "./three.ts";`, | |
| 84 | `const d = await import("./four.ts");`, | |
| 85 | ].join("\n"); | |
| 86 | expect(specifiersOf(source)).toEqual([ | |
| 87 | "./one.ts", "./two.ts", "../client/map.ts", "./three.ts", "./four.ts", | |
| 88 | ]); | |
| 89 | }); | |
| 90 | ||
| 91 | test("the rule catches the imports it exists to catch", () => { | |
| 92 | expect(leavesEngine("trace.ts", "../client/map.ts")).toMatch(/outside/); | |
| 93 | expect(leavesEngine("trace.ts", "../rl/arena.ts")).toMatch(/outside/); | |
| 94 | expect(leavesEngine("trace.ts", "gl-matrix")).toMatch(/inside the engine/); | |
| 95 | expect(leavesEngine("trace.ts", "node:fs")).toMatch(/inside the engine/); | |
| 96 | expect(leavesEngine("trace.ts", "bun:test")).toMatch(/inside the engine/); | |
| 97 | expect(leavesEngine("trace.ts", "./math.ts")).toBeNull(); | |
| 98 | expect(leavesEngine("sub/deep.ts", "../math.ts")).toBeNull(); | |
| 99 | expect(leavesEngine("boundary.test.ts", "bun:test")).toBeNull(); | |
| 100 | expect(leavesEngine("boundary.test.ts", "node:path")).toBeNull(); | |
| 101 | expect(leavesEngine("boundary.test.ts", "../client/map.ts")).toMatch(/outside/); | |
| 102 | }); |
src/client/math.test.ts → src/engine/math.test.ts renamed
No content changes.
src/client/math.ts → src/engine/math.ts renamed
No content changes.
src/engine/trace.test.ts added +57
| @@ -0,0 +1,57 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { rayBox, traceWorld, visible, segmentLength, type Box } from "./trace.ts"; | |
| 3 | ||
| 4 | /** A unit-ish crate from (0,0,0) to (64,64,64), the shape everything here uses. */ | |
| 5 | const crate: Box = { min: [0, 0, 0], max: [64, 64, 64], mat: 1 }; | |
| 6 | const box = (min: [number, number, number], max: [number, number, number]): Box => | |
| 7 | ({ min, max, mat: 0 }); | |
| 8 | ||
| 9 | test("a ray straight at a face enters at the fraction of the way it is", () => { | |
| 10 | // From x = -64 to x = 64 is 128 units; the face is at x = 0, so t = 0.5. | |
| 11 | const hit = rayBox([-64, 32, 32], [64, 32, 32], crate.min, crate.max); | |
| 12 | expect(hit).not.toBeNull(); | |
| 13 | expect(hit?.t).toBeCloseTo(0.5, 9); | |
| 14 | expect(hit?.normal).toEqual([-1, 0, 0]); | |
| 15 | }); | |
| 16 | ||
| 17 | test("a ray that stops short of the box does not hit it", () => { | |
| 18 | expect(rayBox([-64, 32, 32], [-1, 32, 32], crate.min, crate.max)).toBeNull(); | |
| 19 | }); | |
| 20 | ||
| 21 | test("a ray beside the box misses on the axis it misses on", () => { | |
| 22 | expect(rayBox([-64, -32, 32], [64, -32, 32], crate.min, crate.max)).toBeNull(); | |
| 23 | expect(rayBox([-64, 32, 128], [64, 32, 128], crate.min, crate.max)).toBeNull(); | |
| 24 | }); | |
| 25 | ||
| 26 | test("a ray from inside the box enters at zero", () => { | |
| 27 | const hit = rayBox([32, 32, 32], [256, 32, 32], crate.min, crate.max); | |
| 28 | expect(hit?.t).toBeCloseTo(0, 9); | |
| 29 | }); | |
| 30 | ||
| 31 | test("a corner-on ray reports the axis it crossed last", () => { | |
| 32 | // Towards (0,0) at 45°: x reaches 0 at t = 0.5, y at t = 0.75, so y is last. | |
| 33 | const hit = rayBox([-64, -96, 32], [64, 32, 32], crate.min, crate.max); | |
| 34 | expect(hit?.t).toBeCloseTo(0.75, 9); | |
| 35 | expect(hit?.normal).toEqual([0, -1, 0]); | |
| 36 | }); | |
| 37 | ||
| 38 | test("traceWorld returns the nearest box of several on the line", () => { | |
| 39 | const near = box([100, -8, -8], [108, 8, 8]); | |
| 40 | const far = box([200, -8, -8], [208, 8, 8]); | |
| 41 | const hit = traceWorld([0, 0, 0], [400, 0, 0], [far, near]); | |
| 42 | expect(hit?.what).toBe(near); | |
| 43 | expect(hit?.t).toBeCloseTo(100 / 400, 9); | |
| 44 | expect(hit?.point[0]).toBeCloseTo(100, 6); | |
| 45 | expect(hit?.normal).toEqual([-1, 0, 0]); | |
| 46 | }); | |
| 47 | ||
| 48 | test("visible is exactly an empty trace", () => { | |
| 49 | const wall = box([100, -64, -64], [108, 64, 64]); | |
| 50 | expect(visible([0, 0, 0], [400, 0, 0], [wall])).toBe(false); | |
| 51 | expect(visible([0, 0, 0], [400, 0, 0], [])).toBe(true); | |
| 52 | expect(visible([0, 0, 0], [99, 0, 0], [wall])).toBe(true); | |
| 53 | }); | |
| 54 | ||
| 55 | test("segmentLength is the distance between the ends", () => { | |
| 56 | expect(segmentLength([0, 0, 0], [3, 4, 12])).toBeCloseTo(13, 9); | |
| 57 | }); |
src/client/trace.ts → src/engine/trace.ts renamed +3 −1
| @@ -3,9 +3,11 @@ | ||
| 3 | 3 | * same question: what is the first thing between here and there. |
| 4 | 4 | */ |
| 5 | 5 | |
| 6 | import type { Box } from "./map.ts"; | |
| 7 | 6 | import { sub, length, type Vec3 } from "./math.ts"; |
| 8 | 7 | |
| 8 | /** An axis-aligned solid. `mat` is whose surface it is, which only a renderer reads. */ | |
| 9 | export type Box = { min: Vec3; max: Vec3; mat: number }; | |
| 10 | ||
| 9 | 11 | export type Hit = { t: number; point: Vec3; normal: Vec3; what: unknown }; |
| 10 | 12 | |
| 11 | 13 | /** Slab test. Returns the entry fraction along `from → to`, or null. */ |
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 "../client/math.ts"; | |
| 18 | import { angleDelta, clamp, dist, rng, type Vec3 } from "../engine/math.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 "../client/math.ts"; | |
| 2 | import { rng } from "../engine/math.ts"; | |
| 3 | 3 | import { |
| 4 | 4 | createPolicy, forward, newActivations, tensorsOf, type Policy, type Shape, |
| 5 | 5 | } from "../client/policy.ts"; |
new above
offline