114 lines
3.8 KiB
TypeScript
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);
|
|
});
|
|
});
|