deadhead-space-freight-tycoon/packages/sim/test/board-fleet.test.ts

103 lines
4.0 KiB
TypeScript

import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import {
boardPeriodMs,
checkPilotEligibility,
checkRoute,
generateContractBoard,
realToGame,
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)!;
const gameTime = realToGame(contract.offeredAt, config);
return (
!contract.firstContact &&
contract.massTonnes <= model.cargoCapacityTonnes &&
checkRoute(model, earth, content.location(contract.destinationId)!, gameTime).ok &&
checkPilotEligibility(member.skills, contract).ok
);
}
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(band.min);
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 >= band.min && 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);
}
});
});