deadhead-space-freight-tycoon/packages/engine/test/loop.test.ts

114 lines
3.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { GameEngine, ResolutionLoop, SaveStorage, type LoopHost } from "../src/index";
import {
insertDispatch,
newTestApp,
readSave,
signUpOwner,
TEST_NOW,
type TestApp,
} from "./support";
/** A timer and resume signal the test drives by hand. */
class FakeHost implements LoopHost {
ticks: (() => void)[] = [];
resumes: (() => void)[] = [];
setInterval(fn: () => void) {
this.ticks.push(fn);
return this.ticks.length;
}
clearInterval() {
this.ticks = [];
}
onResume(fn: () => void) {
this.resumes.push(fn);
return () => (this.resumes = this.resumes.filter((f) => f !== fn));
}
}
/** The same storage opened by a new engine: the app launched again. */
function relaunch(test: TestApp): GameEngine {
return new GameEngine({
storage: new SaveStorage(test.backend),
clock: { now: () => test.clock.now },
devTools: true,
});
}
describe("resolution loop (Step 14.6)", () => {
it("resolves an arrival that passed while the app was closed, exactly once, on load", async () => {
const test = newTestApp();
const owner = await signUpOwner(test.app);
const record = await insertDispatch(test.app, {
shipId: owner.shipId,
pilotId: owner.avatarId,
departAt: TEST_NOW,
});
// The app closes; the trip lands; the app opens again.
test.clock.now = record.arrivalAt + 60_000;
const engine = relaunch(test);
let changes = 0;
const loop = new ResolutionLoop(engine, new FakeHost(), () => changes++);
await loop.start();
expect(changes).toBe(1);
const save = await readSave(engine);
expect(save.dispatches.find((d) => d.id === record.id)?.status).toBe("resolved");
expect(save.company.credits).toBeGreaterThan(0);
// "While you were away" shows it as before.
const state = await engine.call("getPlayerState", {});
expect(state.summaries.map((s) => s.dispatchId)).toEqual([record.id]);
});
it("changes nothing when it runs twice in a row", async () => {
const test = newTestApp();
const owner = await signUpOwner(test.app);
const record = await insertDispatch(test.app, {
shipId: owner.shipId,
pilotId: owner.avatarId,
departAt: TEST_NOW,
});
test.clock.now = record.arrivalAt;
const first = await test.app.resolveDue(null);
const afterFirst = test.backend.slots.get("main");
const second = await test.app.resolveDue(first.now);
expect([first.resolved, second.resolved]).toEqual([1, 0]);
expect(test.backend.slots.get("main")).toBe(afterFirst);
});
it("ticks on the interval and on resume, and reports a rest ending", async () => {
const test = newTestApp();
const owner = await signUpOwner(test.app);
await test.app.call("restPilot", { pilotId: owner.avatarId });
const host = new FakeHost();
let changes = 0;
const loop = new ResolutionLoop(test.app, host, () => changes++);
await loop.start();
expect(changes).toBe(0);
expect(host.ticks).toHaveLength(1);
expect(host.resumes).toHaveLength(1);
// Nothing has happened yet.
await loop.tick();
expect(changes).toBe(0);
// The rest ends; the next tick (here from a resume) reports it.
const { restEndsAt } = (await readSave(test.app)).pilots.find((p) => p.id === owner.avatarId)!;
test.clock.now = restEndsAt! + 1;
host.resumes[0]!();
await loop.tick();
expect(changes).toBe(1);
loop.stop();
expect(host.ticks).toHaveLength(0);
expect(host.resumes).toHaveLength(0);
});
it("does nothing before a company exists", async () => {
const test = newTestApp();
let changes = 0;
await new ResolutionLoop(test.app, new FakeHost(), () => changes++).start();
expect(changes).toBe(0);
expect(test.backend.slots.size).toBe(0);
});
});