deadhead-space-freight-tycoon/packages/engine/test/service-routing.test.ts

163 lines
6.4 KiB
TypeScript

import { content } from "@deadhead/content";
import type { SaveDocument } from "@deadhead/schema";
import { MS_PER_MINUTE } from "@deadhead/sim";
import { describe, expect, it, vi } from "vitest";
import {
addPilot,
addShip,
edit,
newTestApp,
putBoard,
readSave,
setCredits,
signUpOwner,
testContract,
TEST_NOW,
type TestApp,
} from "./support";
// Long catch-ups: slow under coverage on CI.
vi.setConfig({ testTimeout: 60_000 });
const HOUR = 60 * MS_PER_MINUTE;
const FIRST_LOOK = content.config.independentPilots.firstLookRealMinutes * MS_PER_MINUTE;
/** An owner (HQ at Earth) with the Dispatch Office and Independent pilot "pa" crewing a dented Courier at `at`. */
async function bookedPilot(at = "moon") {
const test = newTestApp();
test.clock.now = TEST_NOW + FIRST_LOOK;
await signUpOwner(test.app);
await setCredits(test.app, content.config.independentPilots.dispatchOfficePrice + 200_000);
await test.app.call("buyDispatchOffice", {});
await addShip(test.app, {
id: "ship-pa",
modelId: "courier",
name: "Courier pa",
locationId: at,
hullConditionPct: 60,
});
await addPilot(test.app, { id: "pa", name: "Pilot pa", assignedShipId: "ship-pa" });
await test.app.call("setPilotMode", { pilotId: "pa", mode: "independent" });
return test;
}
const tripsOf = (save: SaveDocument) => save.dispatches.filter((d) => d.pilotId === "pa");
const shipPa = (save: SaveDocument) => save.ships.find((s) => s.id === "ship-pa")!;
const serviceLog = (save: SaveDocument) =>
save.logbook.flatMap((e) => (e.kind === "service" && e.shipId === "ship-pa" ? [e.phase] : []));
/** A second engine holding a copy of `test`'s save, at the same time. */
async function copyOf(test: TestApp): Promise<TestApp> {
const { code } = await test.app.call("exportSave", {});
const copy = newTestApp();
copy.clock.now = test.clock.now;
await copy.app.call("importSave", { code });
return copy;
}
describe("Independent pilots and booked service (Step 27.4, decision 0.5.154)", () => {
it("takes the job that ends at the booked HQ, and the work starts on landing", async () => {
const test = await bookedPilot();
const home = testContract({ id: "moon:home", originId: "moon", destinationId: "earth" });
const rich = testContract({
id: "moon:rich",
originId: "moon",
destinationId: "mars",
payout: 20_000,
});
await putBoard(test.app, [rich, home], test.clock.now, "moon");
await test.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null });
expect(
(await test.app.call("getPlayerState", {})).ships.find((s) => s.id === "ship-pa")!.booking,
).toMatchObject({ status: "heading", hqId: "earth" });
test.clock.now += 2 * HOUR;
await test.app.call("getPlayerState", {});
const save = await readSave(test.app);
expect(tripsOf(save)[0]!.record.contract?.id).toBe("moon:home");
// Back to work afterward: wear and auto-repair may start the next round.
expect(serviceLog(save).slice(0, 2)).toEqual(["started", "finished"]);
expect(save.logbook.find((e) => e.kind === "repair" && e.shipId === "ship-pa")).toBeTruthy();
});
it("deadheads to the HQ when no job goes there", async () => {
const test = await bookedPilot();
const away = testContract({ id: "moon:away", originId: "moon", destinationId: "mars" });
await putBoard(test.app, [away], test.clock.now, "moon");
await test.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null });
test.clock.now += 2 * HOUR;
await test.app.call("getPlayerState", {});
const first = tripsOf(await readSave(test.app))[0]!;
expect(first.record.contract).toBeNull();
expect(first.record.destinationId).toBe("earth");
});
it("still flies to booked service while the fleet is asleep, then waits", async () => {
const test = await bookedPilot();
await putBoard(test.app, [], test.clock.now, "moon");
await edit(test.app, (save) => {
// An automatic booking (not a manual decision), and the fleet has gone quiet.
shipPa(save).booking = {
repair: true,
upgradeId: null,
hqId: "earth",
by: "auto",
createdAt: test.clock.now,
startedAt: null,
waitingSince: null,
};
save.company.lastManualDecisionAt = test.clock.now - 25 * HOUR;
});
expect((await test.app.call("getPlayerState", {})).fleetSleep.asleep).toBe(true);
test.clock.now += 12 * HOUR;
await test.app.call("getPlayerState", {});
const save = await readSave(test.app);
expect(tripsOf(save).map((d) => d.record.destinationId)).toEqual(["earth"]);
expect(serviceLog(save)).toEqual(["started", "finished"]);
expect(shipPa(save).locationId).toBe("earth");
});
it("works as usual when the booked HQ is out of reach", async () => {
const test = await bookedPilot("earth");
// An HQ at Jupiter: a Courier can't get there from Earth in one leg.
await edit(test.app, (save) => {
save.bays.push({
id: "jupiter-bay",
locationId: "jupiter-cloud-platform",
createdAt: TEST_NOW,
});
save.discoveredLocations.push({
locationId: "jupiter-cloud-platform",
discoveredAt: TEST_NOW,
});
});
const moon = testContract({ id: "earth:moon" });
await putBoard(test.app, [moon], test.clock.now, "earth");
const { ship } = await test.app.call("bookService", {
shipId: "ship-pa",
repair: true,
upgradeId: null,
hqId: "jupiter-cloud-platform",
});
expect(ship.booking!.status).toBe("cant-reach");
test.clock.now += HOUR;
await test.app.call("getPlayerState", {});
expect(tripsOf(await readSave(test.app))[0]!.record.contract?.id).toBe("earth:moon");
});
it("gives the same save whether the service trip is caught up at once or an hour at a time", async () => {
const once = await bookedPilot();
await once.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null });
const pieces = await copyOf(once);
once.clock.now += 8 * HOUR;
await once.app.call("getPlayerState", {});
for (let hour = 0; hour < 8; hour++) {
pieces.clock.now += HOUR;
await pieces.app.call("getPlayerState", {});
}
const a = await readSave(once.app);
expect(serviceLog(a).slice(0, 2)).toEqual(["started", "finished"]);
expect({ ...(await readSave(pieces.app)), lastTimeSeen: 0 }).toEqual({ ...a, lastTimeSeen: 0 });
});
});