import { beforeAll, describe, expect, it } from "vitest"; import { simulate, type Milestones, type SimulationResult } from "../balance/simulate"; import { content } from "@deadhead/content"; import { fuelPriceAt, MS_PER_HOUR, tripFuel } 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 30–95 minutes (CR-013, widened for CR-016 and CR-018)", () => { const minutes = median(lighterFirst, "firstMarketPurchase"); // Cheaper fuel and full deal tanks let the player buy sooner (user decision, CR-018). expect(minutes).toBeGreaterThanOrEqual(30); expect(minutes).toBeLessThanOrEqual(95); }); 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("never needs emergency fuel with positive credits (CR-015, Step 15.9; Manual since CR-018)", () => { for (const result of [...lighterFirst, ...freighterFirst]) { const stuck = result.violations.filter((v) => v.rule === "needs-emergency-fuel"); expect(stuck, `${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. Since // CR-015 the deal Courier's tank counts (full since CR-018), with credits to buy more at Earth. const courier = content.shipModel("courier")!; const [earth, moon] = [content.location("earth")!, content.location("moon")!]; const runFu = tripFuel(courier, earth, moon, content.config); const inTank = courier.tankCapacityFu; const earthPrice = fuelPriceAt(earth, 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(inTank + result.creditsAtDeal! / earthPrice).toBeGreaterThanOrEqual(3 * runFu); } }); 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); }); });