117 lines
2.8 KiB
TypeScript
117 lines
2.8 KiB
TypeScript
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;
|
|
}
|
|
});
|