import { assert, assertEquals } from "@std/assert"; import { GameLoop, LoopClock } from "$/server/game/game_loop.ts"; // a clock tests move by hand. ticks can move it too, to pretend they were slow class FakeClock implements LoopClock { time = 0; #timers: { at: number; fn: () => void; id: number }[] = []; #next_id = 1; now() { return this.time; } schedule(fn: () => void, ms: number) { const id = this.#next_id++; this.#timers.push({ at: this.time + ms, fn, id }); return id; } clear(handle: unknown) { this.#timers = this.#timers.filter((t) => t.id !== handle); } // runs timers in order until `ms` have passed advance(ms: number) { const end = this.time + ms; while (true) { this.#timers.sort((a, b) => a.at - b.at); const timer = this.#timers[0]; if (!timer || timer.at > end) break; this.#timers.shift(); this.time = Math.max(this.time, timer.at); timer.fn(); } this.time = end; } } Deno.test("runs 20 ticks a second", () => { const clock = new FakeClock(); let ticks = 0; const loop = new GameLoop(() => ticks++, 20, clock); loop.start(); clock.advance(10_000); // the first tick runs right away assertEquals(ticks, 201); assertEquals(loop.stats.tps, 20); loop.stop(); clock.advance(1000); assertEquals(ticks, 201); }); Deno.test("a slow tick is made up for, so game time keeps up", () => { const clock = new FakeClock(); let ticks = 0; const loop = new GameLoop( () => { ticks++; // tick 10 takes 200 ms, four ticks' worth if (ticks === 10) clock.time += 200; }, 20, clock, ); loop.start(); clock.advance(2000); assertEquals(ticks, 41); assertEquals(loop.stats.skipped, 0); }); Deno.test("falling too far behind skips ticks and warns", () => { const clock = new FakeClock(); const warnings: string[] = []; const warn = console.warn; console.warn = (message: string) => warnings.push(message); try { let ticks = 0; const loop = new GameLoop( () => { ticks++; // one 2 second hiccup, forty ticks' worth if (ticks === 5) clock.time += 2000; }, 20, clock, ); loop.start(); clock.advance(5000); // it catches up ten, skips the rest and carries on at 20 per second assert(loop.stats.skipped >= 25 && loop.stats.skipped <= 31, `skipped ${loop.stats.skipped}`); assertEquals(loop.stats.tps, 20); assertEquals(warnings.length, 1); assert(warnings[0].startsWith("Can't keep up"), warnings[0]); } finally { console.warn = warn; } }); Deno.test("a tick that throws doesn't stop the loop", () => { const clock = new FakeClock(); let ticks = 0; const error = console.error; console.error = () => {}; try { const loop = new GameLoop( () => { ticks++; if (ticks === 3) throw new Error("oops"); }, 20, clock, ); loop.start(); clock.advance(1000); assertEquals(ticks, 21); } finally { console.error = error; } });