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

138 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { content } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import {
boardPeriodIndex,
boardPeriodMs,
generateContractBoard,
requiredLongHaulLevel,
type BoardInput,
} from "../src/index";
const earth = content.location("earth")!;
const discoveredIds = new Set(["earth", "moon"]);
const periodIndex = boardPeriodIndex(Date.parse("2100-01-01T00:00:00Z"), content.config);
const input: BoardInput = { location: earth, discoveredIds, periodIndex, worldSeed: 12345 };
/** A band's lower bound for goods: the lightest band has none (CR-007, light goods). */
const floorOf = (band: { min: number }) =>
band.min === Math.min(...content.config.contractMassBands.map((b) => b.min)) ? 0 : band.min;
describe("contract board generation", () => {
it("produces an identical board from the same inputs", () => {
expect(generateContractBoard(input, content)).toEqual(generateContractBoard(input, content));
});
it("produces a different board for a different period", () => {
const next = generateContractBoard({ ...input, periodIndex: periodIndex + 1 }, content);
expect(next).not.toEqual(generateContractBoard(input, content));
});
it("has the configured number of contracts, none to its own origin", () => {
const board = generateContractBoard(input, content);
expect(board).toHaveLength(content.config.contractBoardSize);
for (const contract of board) {
expect(contract.originId).toBe("earth");
expect(contract.destinationId).not.toBe("earth");
}
});
it("expires every contract at the end of its board period", () => {
const periodEnd = (periodIndex + 1) * boardPeriodMs(content.config);
for (const contract of generateContractBoard(input, content)) {
expect(contract.expiresAt).toBe(periodEnd);
}
});
it("ties tier to skill: Standard is tier 0 with no skill, others 1–5 matching the skill level", () => {
const moon = content.location("moon")!;
for (let p = 0; p < 50; p++) {
for (const c of generateContractBoard(
{ ...input, location: moon, periodIndex: p },
content,
)) {
if (c.cargoTypeId === "standard") {
expect(c.tier).toBe(0);
expect(c.requiredSkill).toBeNull();
} else {
expect(c.tier).toBeGreaterThanOrEqual(1);
expect(c.tier).toBeLessThanOrEqual(5);
expect(c.requiredSkill?.level).toBe(c.tier);
}
const bands = content.config.contractMassBands;
expect(c.massTonnes).toBeGreaterThan(0);
expect(c.massTonnes).toBeLessThanOrEqual(Math.max(...bands.map((b) => b.max)));
}
}
});
it("fills each mass band with its configured number of contracts", () => {
const board = generateContractBoard(input, content);
let slot = 0;
for (const band of content.config.contractMassBands) {
for (let k = 0; k < band.count; k++, slot++) {
expect(board[slot]!.massTonnes).toBeGreaterThanOrEqual(floorOf(band));
expect(board[slot]!.massTonnes).toBeLessThanOrEqual(band.max);
}
}
expect(slot).toBe(board.length);
});
it("always offers each ship size a Standard job to a discovered place (no skill needed)", () => {
for (let p = 0; p < 500; p++) {
const board = generateContractBoard({ ...input, periodIndex: p }, content);
for (const ship of content.shipModels.filter((s) => s.purchasable)) {
const flyable = board.some(
(c) =>
c.massTonnes <= ship.cargoCapacityTonnes &&
c.requiredSkill === null &&
!c.firstContact &&
discoveredIds.has(c.destinationId),
);
expect(flyable).toBe(true);
}
}
});
it("makes 5–15% of contracts first contact over 1,000 boards, all to undiscovered places", () => {
let total = 0;
let firstContact = 0;
for (let p = 0; p < 1000; p++) {
for (const contract of generateContractBoard({ ...input, periodIndex: p }, content)) {
total++;
if (contract.firstContact) {
firstContact++;
expect(discoveredIds.has(contract.destinationId)).toBe(false);
} else {
expect(discoveredIds.has(contract.destinationId)).toBe(true);
}
}
}
const share = firstContact / total;
expect(share).toBeGreaterThanOrEqual(0.05);
expect(share).toBeLessThanOrEqual(0.15);
});
});
describe("location boards (CR-002)", () => {
it("generates a deterministic Moon board from the location alone, with no bay data", () => {
const moon = content.location("moon")!;
const moonInput: BoardInput = { location: moon, discoveredIds, periodIndex, worldSeed: 12345 };
const board = generateContractBoard(moonInput, content);
expect(board).toHaveLength(content.config.contractBoardSize);
for (const c of board) {
expect(c.originId).toBe("moon");
expect(c.destinationId).not.toBe("moon");
}
expect(generateContractBoard(moonInput, content)).toEqual(board);
});
});
describe("required Long Haul level", () => {
it("counts the cut-offs a distance exceeds", () => {
expect(requiredLongHaulLevel(0.4, content.config)).toBe(0);
expect(requiredLongHaulLevel(100, content.config)).toBe(0);
expect(requiredLongHaulLevel(101, content.config)).toBe(1);
expect(requiredLongHaulLevel(900, content.config)).toBe(5);
});
});