97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import { beforeAll, describe, expect, it } from "vitest";
|
||
import { simulate, type Milestones, type SimulationResult } from "../balance/simulate";
|
||
import { content } from "@deadhead/content";
|
||
import { MS_PER_HOUR, realToGame, tripFuelCost } from "../src/index";
|
||
|
||
/**
|
||
* Balance targets (plan Step 12.1 as amended by CR-001 to CR-013), checked on
|
||
* the median of several worlds: one world's job luck moves a milestone by tens
|
||
* of minutes. The invariants must hold in every world.
|
||
*/
|
||
const SEEDS = [12345, 777, 4242, 99, 2026, 31337, 8];
|
||
const RUN = { durationMs: 8 * MS_PER_HOUR };
|
||
|
||
let lighterFirst: SimulationResult[];
|
||
let freighterFirst: SimulationResult[];
|
||
beforeAll(() => {
|
||
lighterFirst = SEEDS.map((worldSeed) => simulate({ ...RUN, worldSeed, policy: "lighter-first" }));
|
||
freighterFirst = SEEDS.map((worldSeed) =>
|
||
simulate({ ...RUN, worldSeed, policy: "freighter-first" }),
|
||
);
|
||
});
|
||
|
||
/** Median real minutes to a milestone; never reached counts as infinitely late. */
|
||
function median(results: SimulationResult[], milestone: keyof Milestones): number {
|
||
const sorted = results
|
||
.map((r) => r.milestones[milestone] ?? Number.POSITIVE_INFINITY)
|
||
.sort((a, b) => a - b);
|
||
return sorted[Math.floor(sorted.length / 2)]!;
|
||
}
|
||
|
||
describe("balance simulation (Step 12.1)", () => {
|
||
it("gives identical output for the same seed", () => {
|
||
const a = simulate({ ...RUN, worldSeed: 4242, policy: "lighter-first" });
|
||
const b = simulate({ ...RUN, worldSeed: 4242, policy: "lighter-first" });
|
||
expect(b).toEqual(a);
|
||
});
|
||
|
||
it("accepts the deal in under 10 real minutes", () => {
|
||
expect(median(lighterFirst, "dealAccepted")).toBeLessThan(10);
|
||
});
|
||
|
||
it("buys the first Market ship in 45–75 minutes (CR-013)", () => {
|
||
const minutes = median(lighterFirst, "firstMarketPurchase");
|
||
expect(minutes).toBeGreaterThanOrEqual(45);
|
||
expect(minutes).toBeLessThanOrEqual(75);
|
||
});
|
||
|
||
it("discovers a new location in under 90 minutes", () => {
|
||
expect(median(lighterFirst, "firstDiscovery")).toBeLessThan(90);
|
||
});
|
||
|
||
it("pays off the loan in under 4 hours of active play (CR-013)", () => {
|
||
expect(median(lighterFirst, "loanPaidOff")).toBeLessThan(240);
|
||
});
|
||
|
||
it("reaches four ships Lighter-first and Freighter-first within 25 % of each other", () => {
|
||
const a = median(lighterFirst, "ownFourShips");
|
||
const b = median(freighterFirst, "ownFourShips");
|
||
expect(Number.isFinite(a) && Number.isFinite(b)).toBe(true);
|
||
expect(Math.max(a, b)).toBeLessThanOrEqual(1.25 * Math.min(a, b));
|
||
});
|
||
|
||
it("never soft-locks, strands an idle ship or flies a resting pilot, in any world", () => {
|
||
for (const result of [...lighterFirst, ...freighterFirst]) {
|
||
expect(result.violations, `${result.policy}`).toEqual([]);
|
||
}
|
||
});
|
||
|
||
it("has the veteran fly at least one contract in the first session (CR-010)", () => {
|
||
for (const result of lighterFirst)
|
||
expect(result.veteranContractsFirstSession).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("leaves fuel for at least three Courier runs from Earth after the deal (Step 2.5, measured)", () => {
|
||
// Step 12.2: the tutorial share and starting credits as actually earned.
|
||
const courier = content.shipModel("courier")!;
|
||
const [earth, moon] = [content.location("earth")!, content.location("moon")!];
|
||
const start = realToGame(Date.parse(content.config.epoch.realStart), content.config);
|
||
const fuel = tripFuelCost(courier, earth, moon, start, content.config);
|
||
for (const result of lighterFirst) {
|
||
expect(result.tutorialRuns).toBe(content.config.tutorialRunsBeforeDeal);
|
||
expect(result.tutorialShareEarned).toBeGreaterThan(0);
|
||
expect(result.creditsAtDeal).toBe(
|
||
content.config.startingCredits + result.tutorialShareEarned,
|
||
);
|
||
expect(result.creditsAtDeal!).toBeGreaterThanOrEqual(3 * fuel);
|
||
}
|
||
});
|
||
|
||
it("reports credits and pilot levels over time", () => {
|
||
const [run] = lighterFirst;
|
||
expect(run!.samples.length).toBeGreaterThan(5);
|
||
expect(run!.samples[0]).toMatchObject({ minute: 0, credits: expect.any(Number) });
|
||
expect(run!.samples.at(-1)!.pilotLevels.length).toBeGreaterThanOrEqual(2);
|
||
});
|
||
});
|