Server actually ticks

This commit is contained in:
2026-09-25 13:25:25 -03:00
parent 2a2ccae9ce
commit dfabe40e7a
18 changed files with 763 additions and 79 deletions
+116
View File
@@ -0,0 +1,116 @@
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;
}
});