140 lines
5.4 KiB
TypeScript
140 lines
5.4 KiB
TypeScript
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)!, content.config).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);
|
|
});
|
|
});
|