import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { boardPeriodMs, checkPilotEligibility, checkRoute, contractTripGameHours, generateContractBoard, type BoardInput, type Contract, type FleetMember, } from "../src/index"; const earth = content.location("earth")!; const discoveredIds = new Set(["earth", "moon", "mars"]); const base: BoardInput = { location: earth, discoveredIds, periodIndex: 500, worldSeed: 777 }; const { config } = content; const bands = config.contractMassBands; /** Whether this crew can fly the contract (the sim's own route and pilot rules). */ function fits(contract: Contract, member: FleetMember): boolean { const model = content.shipModel(member.modelId)!; return ( !contract.firstContact && contract.massTonnes <= model.cargoCapacityTonnes && checkRoute(model, earth, content.location(contract.destinationId)!).ok && checkPilotEligibility(member.skills, contract).ok && // Priority (CR-007): the ship must make the delivery window. (!contract.priority || contractTripGameHours(model, contract, content) <= contract.deliveryWindowGameHours!) ); } const floorOf = (band: { min: number }) => band.min === Math.min(...bands.map((b) => b.min)) ? 0 : band.min; const courier: FleetMember = { modelId: "courier", skills: {} }; describe("boards that fit the fleet (CR-006)", () => { it("ignores an empty fleet: the same board as with no fleet", () => { expect(generateContractBoard({ ...base, fleet: [] }, content)).toEqual( generateContractBoard(base, content), ); }); it("is deterministic with a fleet", () => { const input = { ...base, fleet: [courier] }; expect(generateContractBoard(input, content)).toEqual(generateContractBoard(input, content)); }); it("gives a lone Courier mostly jobs it can fly, keeping one preview per bigger band", () => { const previews = (bands.length - 1) * config.boardPreviewSlots; const expectedFit = config.contractBoardSize - previews - config.boardOpenSlotsPerBand; for (let p = 0; p < 200; p++) { const board = generateContractBoard({ ...base, periodIndex: p, fleet: [courier] }, content); expect(board).toHaveLength(config.contractBoardSize); expect(board.filter((c) => fits(c, courier)).length).toBeGreaterThanOrEqual(expectedFit); // Each band the Courier can't carry still shows a preview. for (const band of bands.slice(1)) { expect(board.some((c) => c.massTonnes >= band.min && c.massTonnes <= band.max)).toBe(true); } } }); it("offers skill cargo up to the pilot's level, never above", () => { const skilled: FleetMember = { modelId: "courier", skills: { "expensive-cargo": 3 } }; let skillJobs = 0; for (let p = 0; p < 200; p++) { const board = generateContractBoard({ ...base, periodIndex: p, fleet: [skilled] }, content); for (const c of board.filter((c) => fits(c, skilled))) { if (c.requiredSkill) { skillJobs++; expect(c.requiredSkill.level).toBeLessThanOrEqual(3); } } } expect(skillJobs).toBeGreaterThan(0); }); it("keeps the configured band counts when the fleet covers every band", () => { const fleet: FleetMember[] = ["courier", "freighter", "bulk-hauler"].map((modelId) => ({ modelId, skills: {}, })); const board = generateContractBoard({ ...base, fleet }, content); let slot = 0; for (const band of bands) { for (let k = 0; k < band.count; k++, slot++) { expect(board[slot]!.massTonnes).toBeGreaterThanOrEqual(floorOf(band)); expect(board[slot]!.massTonnes).toBeLessThanOrEqual(band.max); } } for (const [i, band] of bands.entries()) { const member = fleet[i]!; const inBand = board.filter((c) => c.massTonnes >= floorOf(band) && c.massTonnes <= band.max); const fitting = inBand.filter((c) => fits(c, member)); expect(fitting.length).toBeGreaterThanOrEqual(band.count - config.boardOpenSlotsPerBand); } }); it("expires fit jobs with the board period", () => { const end = (base.periodIndex + 1) * boardPeriodMs(config); for (const c of generateContractBoard({ ...base, fleet: [courier] }, content)) { expect(c.expiresAt).toBe(end); } }); }); describe("fit jobs carry varied goods (CR-008 fix)", () => { // The user's save: one Courier at the Moon, pilot Heavy Cargo 1 and Long Haul 1. const pilotCourier: FleetMember = { modelId: "courier", skills: { "heavy-cargo": 1, "long-haul": 1 }, }; const moonInput: BoardInput = { location: content.location("moon")!, discoveredIds: new Set(["earth", "moon"]), periodIndex: 0, worldSeed: 4242, fleet: [pilotCourier], }; it("offers the Courier several goods it can really carry, not undersized Lunar Titanium", () => { let distinct = 0; for (let p = 0; p < 100; p++) { const board = generateContractBoard({ ...moonInput, periodIndex: p }, content); const flyable = board.filter((c) => fits(c, pilotCourier)); expect(flyable.length).toBeGreaterThanOrEqual(5); for (const c of flyable) { const { min, max } = content.good(c.goodId)!.massTonnes; expect(c.massTonnes).toBeGreaterThanOrEqual(min); expect(c.massTonnes).toBeLessThanOrEqual(max); expect(c.goodId).not.toBe("lunar-titanium"); } distinct += new Set(flyable.map((c) => c.goodId)).size; } expect(distinct / 100).toBeGreaterThanOrEqual(3); }); });