/** * How often the arena speaks. * * The simulation is 30Hz either way; what is decided here is how much of it * the browser is told about, which is the difference between a body drawn * from fifteen samples a second and one drawn from thirty. */ import { expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; process.env.FORGE_APP_DATA ??= mkdtempSync(join(tmpdir(), "app-")); const { advance, attach, broadcastEvery, match, release, seatCount, snapshot } = await import("./arena.ts"); test("every room this match can hold hears every tick", () => { // The third seat used to halve the rate, which drew every other body 42ms // further in the past for everyone. What it saved is in the comment on the // rule; the ten-seat room is the whole of this game. for (let filled = 0; filled <= 10; filled++) expect(broadcastEvery(filled)).toBe(1); // Past the measured room the old cadence stays, unmeasured rather than right. expect(broadcastEvery(11)).toBe(2); }); /** A seat that only counts what it was sent. */ function listen(session: string) { const got: string[] = []; attach(session, match().players[0]!.id, `test-${session}`, session, (data) => got.push(data)); return got; } // The arena is one match shared by the whole test run, and another file may // be holding a seat of its own while this one runs; what is checked is that // the loop broadcasts at exactly the cadence the room it is in asks for. test("the loop sends at the cadence the seat count asks for", () => { const one = listen("rate-a"); const two = listen("rate-b"); const every = broadcastEvery(seatCount()); advance(1); // Thirty ticks to the second, one snapshot every `every` of them. expect(one.length).toBe(30 / every); expect(two.length).toBe(one.length); release("rate-a", "rate-a"); release("rate-b", "rate-b"); }); test("a third seat no longer halves what everybody is sent", () => { const seats = ["rate-c", "rate-d", "rate-e"].map(listen); expect(seatCount()).toBeGreaterThan(2); advance(1); for (const got of seats) expect(got.length).toBe(30); for (const session of ["rate-c", "rate-d", "rate-e"]) release(session, session); }); test("a snapshot stays small enough to send thirty times a second", () => { // The trade is only worth making if the payload is small. Measured over a // live round it is ~1.6 KiB a seat since the board went sticky and the // vision filter came in — 48 KiB a second at 30Hz, and every seat is sent // that much now. This is the number the cadence's comment quotes, so it // is checked here rather than assumed; the ceiling is loose enough not to // fail on a busy tick and tight enough that a snapshot which doubled in // size would say so. for (let i = 0; i < 40 && match().phase !== "live"; i++) advance(1); const bytes = Buffer.byteLength(snapshot(match().players[0]!.id)); expect(bytes).toBeGreaterThan(200); expect(bytes).toBeLessThan(8000); }); test("the snapshot carries the speed the server will move you at, not the reasons for it", () => { // The browser used to rebuild this rule from four fields and was right // only until a fifth was added. What it gets now is the answer: every // reason the match has to speed a body up, slow it down or hold it still, // resolved here, in units a second, to a tenth like every other number in // the snapshot. for (let i = 0; i < 40 && match().phase !== "live"; i++) advance(1); const state = match(); const me = state.players[0]!; const was = { ...me }; const phase = state.phase; const seen = () => JSON.parse(snapshot(me.id)).me as Record; const speed = () => seen().speed; Object.assign(me, { alive: true, haste: 0, slow: 0, planting: 0, defusing: 0, weapon: "classic" }); expect(speed()).toBe(250); me.weapon = "operator"; expect(speed()).toBe(215); me.weapon = "classic"; me.haste = 2; expect(speed()).toBe(337.5); // exactly what the tick will use me.haste = 0; me.slow = 2; expect(speed()).toBe(137.5); me.slow = 0; // Every way the match holds a body still arrives as a zero, and the client // is told none of them: a fifth one would need no change over there. me.planting = 1; expect(speed()).toBe(0); me.planting = 0; me.defusing = 1; expect(speed()).toBe(0); me.defusing = 0; me.alive = false; expect(speed()).toBe(0); me.alive = true; // Including the phases whose tick returns before it moves anybody. for (const stopped of ["end", "over"] as const) { state.phase = stopped; expect(speed()).toBe(0); } state.phase = phase; expect(speed()).toBe(250); // The three fields that existed only to be re-derived are gone with it. const me2 = seen(); expect(me2).not.toHaveProperty("haste"); expect(me2).not.toHaveProperty("slow"); expect(me2).not.toHaveProperty("defusing"); // What it costs the wire: a name and a number, once a seat a tick. const field = `"speed":${speed()},`; expect(snapshot(me.id)).toContain(field); expect(field.length).toBeLessThanOrEqual(16); Object.assign(me, was); });