rook/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 470af835242d7c193b45544c677099a00451ce7b
| 1 | /** |
| 2 | * How often the arena speaks. |
| 3 | * |
| 4 | * The simulation is 30Hz either way; what is decided here is how much of it |
| 5 | * the browser is told about, which is the difference between a body drawn |
| 6 | * from fifteen samples a second and one drawn from thirty. |
| 7 | */ |
| 8 | |
| 9 | import { expect, test } from "bun:test"; |
| 10 | import { mkdtempSync } from "node:fs"; |
| 11 | import { tmpdir } from "node:os"; |
| 12 | import { join } from "node:path"; |
| 13 | |
| 14 | process.env.FORGE_APP_DATA ??= mkdtempSync(join(tmpdir(), "app-")); |
| 15 | const { advance, attach, broadcastEvery, match, release, seatCount, snapshot } = await import("./arena.ts"); |
| 16 | |
| 17 | test("every room this match can hold hears every tick", () => { |
| 18 | // The third seat used to halve the rate, which drew every other body 42ms |
| 19 | // further in the past for everyone. What it saved is in the comment on the |
| 20 | // rule; the ten-seat room is the whole of this game. |
| 21 | for (let filled = 0; filled <= 10; filled++) expect(broadcastEvery(filled)).toBe(1); |
| 22 | // Past the measured room the old cadence stays, unmeasured rather than right. |
| 23 | expect(broadcastEvery(11)).toBe(2); |
| 24 | }); |
| 25 | |
| 26 | /** A seat that only counts what it was sent. */ |
| 27 | function listen(session: string) { |
| 28 | const got: string[] = []; |
| 29 | attach(session, match().players[0]!.id, `test-${session}`, session, (data) => got.push(data)); |
| 30 | return got; |
| 31 | } |
| 32 | |
| 33 | // The arena is one match shared by the whole test run, and another file may |
| 34 | // be holding a seat of its own while this one runs; what is checked is that |
| 35 | // the loop broadcasts at exactly the cadence the room it is in asks for. |
| 36 | test("the loop sends at the cadence the seat count asks for", () => { |
| 37 | const one = listen("rate-a"); |
| 38 | const two = listen("rate-b"); |
| 39 | const every = broadcastEvery(seatCount()); |
| 40 | advance(1); |
| 41 | // Thirty ticks to the second, one snapshot every `every` of them. |
| 42 | expect(one.length).toBe(30 / every); |
| 43 | expect(two.length).toBe(one.length); |
| 44 | release("rate-a", "rate-a"); |
| 45 | release("rate-b", "rate-b"); |
| 46 | }); |
| 47 | |
| 48 | test("a third seat no longer halves what everybody is sent", () => { |
| 49 | const seats = ["rate-c", "rate-d", "rate-e"].map(listen); |
| 50 | expect(seatCount()).toBeGreaterThan(2); |
| 51 | advance(1); |
| 52 | for (const got of seats) expect(got.length).toBe(30); |
| 53 | for (const session of ["rate-c", "rate-d", "rate-e"]) release(session, session); |
| 54 | }); |
| 55 | |
| 56 | test("a snapshot stays small enough to send thirty times a second", () => { |
| 57 | // The trade is only worth making if the payload is small. Measured over a |
| 58 | // live round it is ~1.6 KiB a seat since the board went sticky and the |
| 59 | // vision filter came in — 48 KiB a second at 30Hz, and every seat is sent |
| 60 | // that much now. This is the number the cadence's comment quotes, so it |
| 61 | // is checked here rather than assumed; the ceiling is loose enough not to |
| 62 | // fail on a busy tick and tight enough that a snapshot which doubled in |
| 63 | // size would say so. |
| 64 | for (let i = 0; i < 40 && match().phase !== "live"; i++) advance(1); |
| 65 | const bytes = Buffer.byteLength(snapshot(match().players[0]!.id)); |
| 66 | expect(bytes).toBeGreaterThan(200); |
| 67 | expect(bytes).toBeLessThan(8000); |
| 68 | }); |
| 69 | |
| 70 | test("the snapshot carries the speed the server will move you at, not the reasons for it", () => { |
| 71 | // The browser used to rebuild this rule from four fields and was right |
| 72 | // only until a fifth was added. What it gets now is the answer: every |
| 73 | // reason the match has to speed a body up, slow it down or hold it still, |
| 74 | // resolved here, in units a second, to a tenth like every other number in |
| 75 | // the snapshot. |
| 76 | for (let i = 0; i < 40 && match().phase !== "live"; i++) advance(1); |
| 77 | const state = match(); |
| 78 | const me = state.players[0]!; |
| 79 | const was = { ...me }; |
| 80 | const phase = state.phase; |
| 81 | const seen = () => JSON.parse(snapshot(me.id)).me as Record<string, unknown>; |
| 82 | const speed = () => seen().speed; |
| 83 | |
| 84 | Object.assign(me, { alive: true, haste: 0, slow: 0, planting: 0, defusing: 0, weapon: "classic" }); |
| 85 | expect(speed()).toBe(250); |
| 86 | me.weapon = "operator"; |
| 87 | expect(speed()).toBe(215); |
| 88 | me.weapon = "classic"; |
| 89 | me.haste = 2; |
| 90 | expect(speed()).toBe(337.5); // exactly what the tick will use |
| 91 | me.haste = 0; |
| 92 | me.slow = 2; |
| 93 | expect(speed()).toBe(137.5); |
| 94 | me.slow = 0; |
| 95 | |
| 96 | // Every way the match holds a body still arrives as a zero, and the client |
| 97 | // is told none of them: a fifth one would need no change over there. |
| 98 | me.planting = 1; |
| 99 | expect(speed()).toBe(0); |
| 100 | me.planting = 0; |
| 101 | me.defusing = 1; |
| 102 | expect(speed()).toBe(0); |
| 103 | me.defusing = 0; |
| 104 | me.alive = false; |
| 105 | expect(speed()).toBe(0); |
| 106 | me.alive = true; |
| 107 | // Including the phases whose tick returns before it moves anybody. |
| 108 | for (const stopped of ["end", "over"] as const) { |
| 109 | state.phase = stopped; |
| 110 | expect(speed()).toBe(0); |
| 111 | } |
| 112 | state.phase = phase; |
| 113 | expect(speed()).toBe(250); |
| 114 | |
| 115 | // The three fields that existed only to be re-derived are gone with it. |
| 116 | const me2 = seen(); |
| 117 | expect(me2).not.toHaveProperty("haste"); |
| 118 | expect(me2).not.toHaveProperty("slow"); |
| 119 | expect(me2).not.toHaveProperty("defusing"); |
| 120 | // What it costs the wire: a name and a number, once a seat a tick. |
| 121 | const field = `"speed":${speed()},`; |
| 122 | expect(snapshot(me.id)).toContain(field); |
| 123 | expect(field.length).toBeLessThanOrEqual(16); |
| 124 | |
| 125 | Object.assign(me, was); |
| 126 | }); |