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); }); });