import { content } from "@deadhead/content"; import { boardPeriodMs } from "@deadhead/sim"; import { describe, expect, it } from "vitest"; import { boardFleet } from "../src/game/boards"; import { encodeSave } from "../src/index"; import { addPilot, addShip, readSave, setCredits, signUpOwner, TEST_NOW, useTestApp, } from "./support"; const testApp = useTestApp(); const COURIER_HOLD = content.shipModel("courier")!.cargoCapacityTonnes; const PERIOD = boardPeriodMs(content.config); describe("completable boards in the engine (Step 19.3, decisions 0.5.114–0.5.117)", () => { it("hides a heavy job once the only ship that could take it has left", async () => { const { app, clock } = testApp(); await signUpOwner(app); await addShip(app, { id: "f1", modelId: "freighter", name: "Big Wren" }); await addPilot(app, { id: "p1", name: "Ada", assignedShipId: "f1" }); const before = (await app.call("getContractBoard", { locationId: "earth" })).contracts; const heavy = before.filter((c) => c.massTonnes > COURIER_HOLD); expect(heavy.length).toBeGreaterThan(0); // The Freighter flies empty to the Moon; the Courier is still at Earth. await app.call("createDispatch", { kind: "reposition", shipId: "f1", pilotId: "p1", destinationId: "moon", }); clock.now += 1_000; const after = (await app.call("getContractBoard", { locationId: "earth" })).contracts; expect(after.filter((c) => c.massTonnes > COURIER_HOLD)).toEqual([]); // What the Courier can take is still there, unchanged. expect(after).toEqual(before.filter((c) => c.massTonnes <= COURIER_HOLD)); }); it("adds a bigger ship's jobs at the next board period, not mid-period", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); const first = (await app.call("getContractBoard", { locationId: "earth" })).contracts; expect(first.every((c) => c.massTonnes <= COURIER_HOLD)).toBe(true); await setCredits(app, content.shipModel("freighter")!.price); const { ship } = await app.call("buyShip", { modelId: "freighter", bayId: owner.earthBayId }); await addPilot(app, { id: "p1", name: "Ada", assignedShipId: ship.id }); clock.now += 1_000; expect((await app.call("getContractBoard", { locationId: "earth" })).contracts).toEqual(first); // Next period: the board is made for the Courier and the Freighter. clock.now = (Math.floor(TEST_NOW / PERIOD) + 1) * PERIOD + 1_000; let heavy = 0; for (let period = 0; period < 5; period++, clock.now += PERIOD) { const board = (await app.call("getContractBoard", { locationId: "earth" })).contracts; heavy += board.filter((c) => c.massTonnes > COURIER_HOLD).length; } expect(heavy).toBeGreaterThan(0); }); it("stores the qualifying fleet with each board it generates", async () => { const { app } = testApp(); await signUpOwner(app); await app.call("getContractBoard", { locationId: "earth" }); const [board] = (await readSave(app)).contractBoards.filter((b) => b.kind === "location"); expect(board!.fleet).toEqual([ { modelId: "courier", skills: {}, upgrades: {}, fuelFu: content.shipModel("courier")!.tankCapacityFu, }, ]); }); it("opens an older save with no stored boards, and regenerates them on view", async () => { const { app } = testApp(); await signUpOwner(app); const before = (await app.call("getContractBoard", { locationId: "earth" })).contracts; const old = await readSave(app); expect(old.contractBoards.length).toBeGreaterThan(0); // As version 3 wrote it: boards without a fleet snapshot. const v3 = { ...old, saveVersion: 3, contractBoards: old.contractBoards.map(({ fleet: _fleet, ...rest }) => rest), }; const device = testApp().app; await device.call("importSave", { code: await encodeSave(v3 as never) }); expect((await readSave(device)).contractBoards).toEqual([]); // The same inputs regenerate the same board. expect((await device.call("getContractBoard", { locationId: "earth" })).contracts).toEqual( before, ); expect((await readSave(device)).contractBoards).toHaveLength(1); }); }); describe("boards ignore Independent pilots (decision 0.5.133)", () => { async function withIndependentFreighter() { const { app, clock } = testApp(); await signUpOwner(app); await addShip(app, { id: "f1", modelId: "freighter", name: "Big Wren" }); await addPilot(app, { id: "p1", name: "Rosa", assignedShipId: "f1" }); await setCredits(app, content.config.independentPilots.dispatchOfficePrice); await app.call("buyDispatchOffice", {}); return { app, clock }; } it("shows only jobs a Manual pilot can fly once Rosa goes Independent", async () => { const { app, clock } = await withIndependentFreighter(); const before = (await app.call("getContractBoard", { locationId: "earth" })).contracts; expect(before.some((c) => c.massTonnes > COURIER_HOLD)).toBe(true); await app.call("setPilotMode", { pilotId: "p1", mode: "independent" }); // This period: the Freighter's jobs are hidden at once. const now = (await app.call("getContractBoard", { locationId: "earth" })).contracts; expect(now.filter((c) => c.massTonnes > COURIER_HOLD)).toEqual([]); // Later periods are generated for the player's Courier alone. for (let period = 0; period < 5; period++) { clock.now += PERIOD; const board = (await app.call("getContractBoard", { locationId: "earth" })).contracts; expect(board.length).toBeGreaterThan(0); expect(board.filter((c) => c.massTonnes > COURIER_HOLD)).toEqual([]); } }); it("falls back to the whole fleet when every pair is Independent", async () => { const { app } = await withIndependentFreighter(); await app.call("setPilotMode", { pilotId: "p1", mode: "independent" }); const save = await readSave(app); // No Manual pair left: only Rosa and her Freighter. const alone = { ...save, ships: save.ships.filter((s) => s.id === "f1"), pilots: save.pilots.filter((p) => p.id === "p1"), }; expect(boardFleet(alone, "earth", TEST_NOW + PERIOD).map((m) => m.modelId)).toEqual([ "freighter", ]); expect(boardFleet(save, "earth", TEST_NOW + PERIOD).map((m) => m.modelId)).toEqual(["courier"]); }); });