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

117 lines
4.4 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 = { origin: earth, discoveredIds, periodIndex, worldSeed: 12345 };
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, origin: 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).toBeGreaterThanOrEqual(Math.min(...bands.map((b) => b.min)));
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(band.min);
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("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);
});
});