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

forked from benjivers/val

workbenchthis machine

walkDistance: say which argument is cached, and guard the slow order (#24 ite… (#39)

Requested-by: benjivers <[email protected]>
Co-authored-by: benjivers <[email protected]>
Wren committed 674c40d Browse files
2 files changed +66 −2
src/engine/grid.test.ts added +43
@@ -0,0 +1,43 @@
1 import { expect, test } from "bun:test";
2 import { makeGrid } from "./grid.ts";
3
4 const LINES = ["#########", "#.......#", "#.##.##.#", "#.......#", "#########"];
5 const cells = (g: ReturnType<typeof makeGrid>) => g.cellsOf(".");
6
7 test("many distances to one fixed target build one field", () => {
8 const g = makeGrid(LINES, 10);
9 const target = cells(g)[0]!;
10 for (const from of cells(g)) g.walkDistance(from, target);
11 expect(g.fieldsBuilt()).toBe(1);
12 });
13
14 test("varying the target builds a field per target", () => {
15 const g = makeGrid(LINES, 10);
16 const from = cells(g)[0]!;
17 for (const to of cells(g)) g.walkDistance(from, to);
18 expect(g.fieldsBuilt()).toBe(cells(g).length);
19 });
20
21 test("pathStep shares the field walkDistance built for the goal", () => {
22 const g = makeGrid(LINES, 10);
23 const goal = cells(g).at(-1)!;
24 for (const from of cells(g)) {
25 g.walkDistance(from, goal);
26 g.pathStep(from, goal);
27 }
28 expect(g.fieldsBuilt()).toBe(1);
29 });
30
31 test("walk distance is symmetric, so swapping the arguments is safe", () => {
32 const g = makeGrid(LINES, 10);
33 for (const a of cells(g)) for (const b of cells(g)) expect(g.walkDistance(a, b)).toBe(g.walkDistance(b, a));
34 });
35
36 test("clearPaths forgets the fields, so the next ask builds again", () => {
37 const g = makeGrid(LINES, 10);
38 const [a, b] = cells(g);
39 g.walkDistance(a!, b!);
40 g.clearPaths();
41 g.walkDistance(a!, b!);
42 expect(g.fieldsBuilt()).toBe(2);
43 });
src/engine/grid.ts +23 −2
@@ -10,6 +10,10 @@
1010 * `makeGrid` binds the maths to one set of rows and one tile size. The
1111 * collision itself is `glide.ts`, which the browser runs too; the walk
1212 * distances are cached per goal for the round and cleared by the caller.
13 *
14 * Argument order matters for speed: a walk field is built for `to` and read
15 * at `from`. Keep `to` fixed and vary `from`; walking is symmetric on this
16 * grid, so swapping the two never changes the answer.
1317 */
1418
1519 import { circleBlocked, slideCircle } from "./glide.ts";
@@ -39,9 +43,16 @@ export type Grid = {
3943 blockedCircle: (x: number, y: number, r: number) => boolean;
4044 wallBetween: (a: Point, b: Point) => boolean;
4145 rayHitWall: (from: Point, angle: number, range: number) => Point;
46 /**
47 * Tiles walked between two points, or -1. Cached by `to`: hold `to` fixed
48 * and vary `from`, never the other way (symmetric, so swapping is safe).
49 */
4250 walkDistance: (from: Point, to: Point) => number;
51 /** The next point toward `to`; reads the same field, cached by `to`. */
4352 pathStep: (from: Point, to: Point) => Point;
4453 clearPaths: () => void;
54 /** How many walk fields have been built so far — for tests of the cache. */
55 fieldsBuilt: () => number;
4556 };
4657
4758 /** Closest approach of a segment to a point, as distance and travel along it. */
@@ -132,11 +143,14 @@ export function makeGrid(lines: string[], tile: number): Grid {
132143 // every few ticks; the field for a goal is built once and kept for the round.
133144 const fields = new Map<number, Int32Array>();
134145 const clearPaths = (): void => void fields.clear();
146 let built = 0;
147 const fieldsBuilt = (): number => built;
135148
136149 function field(goalCol: number, goalRow: number): Int32Array {
137150 const key = goalRow * cols + goalCol;
138151 const cached = fields.get(key);
139152 if (cached) return cached;
153 built++;
140154 const dist = new Int32Array(cols * rows).fill(-1);
141155 if (!solidTile(goalCol, goalRow)) {
142156 dist[key] = 0;
@@ -162,7 +176,12 @@ export function makeGrid(lines: string[], tile: number): Grid {
162176 return dist;
163177 }
164178
165 /** Walk distance in tiles between two points, or -1 when unreachable. */
179 /**
180 * Walk distance in tiles between two points, or -1 when unreachable. The
181 * field is built for `to` and read at `from`, so a caller asking about many
182 * cells should pass them as `from` against one fixed `to`: one field, not
183 * one per call. Distance is symmetric, so the swap is always safe.
184 */
166185 function walkDistance(from: Point, to: Point): number {
167186 const f = tileOf(from.x, from.y);
168187 const g = tileOf(to.x, to.y);
@@ -172,7 +191,8 @@ export function makeGrid(lines: string[], tile: number): Grid {
172191 /**
173192 * The next point to walk toward on the way to a goal: the centre of the
174193 * neighbouring tile that is one step closer, so a walker rounds corners
175 * instead of grinding into them.
194 * instead of grinding into them. Uses the field cached for `to`, like
195 * `walkDistance`: many walkers to one goal cost one field.
176196 */
177197 function pathStep(from: Point, to: Point): Point {
178198 const f = tileOf(from.x, from.y);
@@ -220,5 +240,6 @@ export function makeGrid(lines: string[], tile: number): Grid {
220240 walkDistance,
221241 pathStep,
222242 clearPaths,
243 fieldsBuilt,
223244 };
224245 }
↑↓ navigate ↵ open esc close

Keyboard shortcuts