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

forked from benjivers/val

workbenchthis machine

Merge pull request #33 from client-test-stands-still

Stop the frame test measuring a view nobody chose
benjivers committed 19b0f58 merge Browse files
1 file changed +45 −10
src/client.test.ts +45 −10
@@ -9,6 +9,7 @@
99 */
1010
1111 import { expect, test } from "bun:test";
12
1213 import { mkdtempSync } from "node:fs";
1314 import { tmpdir } from "node:os";
1415 import { join } from "node:path";
@@ -21,6 +22,15 @@ const base = `http://${server.hostname}:${server.port}`;
2122 // ------------------------------------------------------------ the stub DOM
2223
2324 const drawn: string[] = [];
25 /**
26 * Every canvas the client asked the document for. The widest is its offscreen
27 * world buffer, whose width is however many columns the frame budget settled
28 * on; the other is the minimap's tile layer. Keeping them is what lets a test
29 * bound the frame's cost as a fraction of the columns actually used, instead
30 * of against a number somebody picked once.
31 */
32 const canvases: Record<string, unknown>[] = [];
33 const worldColumns = (): number => Math.max(0, ...canvases.map((c) => Number(c.width) || 0));
2434 // Where the canvas was moved to: on the top-down view your own body is an
2535 // arrow drawn about the origin, so the last translate of a frame is where
2636 // this client thinks you are.
@@ -131,7 +141,11 @@ Object.assign(globalThis, {
131141 getElementById: (id: string) => nodes.get(id) ?? null,
132142 querySelector: (sel: string) => (sel.includes("red") ? scoreRed : scoreBlue),
133143 querySelectorAll: (sel: string) => (sel.includes("data-buy") ? buys : agentButtons),
134 createElement: (tag: string) => element(tag),
144 createElement: (tag: string) => {
145 const node = element(tag);
146 if (tag === "canvas") canvases.push(node);
147 return node;
148 },
135149 addEventListener: noop,
136150 pointerLockElement: null,
137151 exitPointerLock: noop,
@@ -248,13 +262,26 @@ test("the bundled client connects, paints the HUD and draws a frame", async () =
248262 expect(row).not.toMatch(/\d/);
249263
250264 // A frame of the first-person view draws the world into its own buffer and
251 // blits it, with the minimap's tiles blitted too. It is a picture, but it
252 // is not one fill a screen column any more, nor one a map tile: the map has
253 // 570 tiles and the canvas 900 columns, and the frame used to pay for both.
254 // The minimap's tiles are the blit; the walls are a run a fill, and how
255 // many runs a view has depends on where the seat happens to be standing —
256 // a long corridor is many depths — so the bound is the columns, which no
257 // view can reach, rather than the tile count, which a corridor can.
265 // blits it, with the minimap's tiles blitted too. It is a picture, but it is
266 // NOT one fill a screen column, nor one a map tile: the map has 570 tiles and
267 // the buffer up to 960 columns, and the frame used to pay for both. The
268 // minimap's tiles are the blit; the walls are a run a fill.
269 //
270 // That sentence is the whole assertion, and it is now written as one. It
271 // used to demand more than 40 fills and fewer than 900, which failed about
272 // one full-suite run in twenty-five: the loop above advances the SHARED
273 // match until somebody dies — five to two hundred seconds of play — so the
274 // view being counted was wherever that left the seat. Alone it draws 154 to
275 // 236 fills; the run that failed drew 31, and a preview drew 525. Every one
276 // of those is a correct frame of a different view. A bound on the count of
277 // an unfixed view can only ever be luck.
278 //
279 // So the bounds are the two things true of EVERY view: it drew something,
280 // and it cost fewer fills than the frame had columns. The column count is
281 // read off the client's own offscreen buffer rather than hardcoded, because
282 // the frame budget moves the world between 960 and 448 columns and a
283 // constant cannot tell "a run a wall" at one rung from "a fill a column" at
284 // another.
258285 const tally = () => ({
259286 fills: drawn.filter((op) => op.startsWith("fillRect")).length,
260287 blits: drawn.filter((op) => op.startsWith("drawImage")).length,
@@ -263,9 +290,17 @@ test("the bundled client connects, paints the HUD and draws a frame", async () =
263290 drawn.length = 0;
264291 runFrame();
265292 const fp = tally();
293 drawn.length = 0;
294 runFrame();
295 const again = tally();
296
266297 expect(fp.blits).toBeGreaterThanOrEqual(2); // the world, and the map's tiles
267 expect(fp.fills).toBeGreaterThan(40);
268 expect(fp.fills).toBeLessThan(900);
298 expect(fp.fills).toBeGreaterThan(0); // it drew a world, not an empty buffer
299 expect(fp.fills).toBeLessThan(worldColumns()); // run-batched, not per column
300 // The same state drawn twice costs the same. A frame whose cost wanders
301 // while nothing moves is a renderer bug, and it is the shape of thing the
302 // old bound was mistaking for flakiness.
303 expect(again.fills).toBe(fp.fills);
269304
270305 // Tab swaps that frame for the whole map. Its tiles are drawn once into a
271306 // canvas of their own and blitted from then on: the first such frame pays
↑↓ navigate ↵ open esc close

Keyboard shortcuts