import { describe, expect, it } from "vitest"; import { content, dealLoanAmount, gameConfig, gameConfigSchema, locations, shipModels, skills, } from "../src/index"; describe("game config", () => { it("passes the schema", () => { expect(() => gameConfigSchema.parse(gameConfig)).not.toThrow(); }); it("uses 1 real minute = 1 game hour", () => { expect(gameConfig.timeCompression).toBe(60); }); it("has one Long Haul cut-off per Long Haul level", () => { const longHaul = skills.find((s) => s.id === "long-haul")!; expect(gameConfig.longHaulCutoffsMillionKm).toHaveLength(longHaul.maxLevel); }); // The "fuel for three Courier runs after the deal" check (Step 2.5 as amended // by CR-001) runs on measured numbers in packages/sim/test/balance.test.ts (Step 12.2). it("sizes the tutorial board and loaner for the three mass tiers", () => { expect(gameConfig.tutorialBoardSize).toBe(3); const loaner = shipModels.find((s) => s.id === gameConfig.tutorialLoanerShipModelId)!; expect(loaner.purchasable).toBe(false); expect(gameConfig.tutorialMassTonnes.full).toBeLessThanOrEqual(loaner.cargoCapacityTonnes); }); it("lends the Courier and Clipper prices plus an Earth bay lease in the deal (0.5.83)", () => { const price = (id: string) => shipModels.find((s) => s.id === id)!.price; const earth = locations.find((l) => l.id === "earth")!; expect(gameConfig.deal).toEqual({ avatarShipModelId: "courier", veteranShipModelId: "clipper", bayLocationId: "earth", }); expect(content.dealLoanAmount).toBe(price("courier") + price("clipper") + earth.bayLeasePrice); }); it("recomputes the deal loan when any of those prices change", () => { const base = dealLoanAmount({ config: gameConfig, shipModels, locations }); const bump = (id: string) => shipModels.map((s) => (s.id === id ? { ...s, price: s.price + 100 } : s)); expect(dealLoanAmount({ config: gameConfig, shipModels: bump("courier"), locations })).toBe( base + 100, ); expect(dealLoanAmount({ config: gameConfig, shipModels: bump("clipper"), locations })).toBe( base + 100, ); const pricierEarth = locations.map((l) => l.id === "earth" ? { ...l, bayLeasePrice: l.bayLeasePrice + 7 } : l, ); expect(dealLoanAmount({ config: gameConfig, shipModels, locations: pricierEarth })).toBe( base + 7, ); }); it("has a contract mass band each purchasable ship can carry", () => { for (const ship of shipModels.filter((s) => s.purchasable)) { expect( gameConfig.contractMassBands.some((band) => band.max <= ship.cargoCapacityTonnes), ).toBe(true); } const total = gameConfig.contractMassBands.reduce((sum, b) => sum + b.count, 0); expect(total).toBe(gameConfig.contractBoardSize); }); it("rests a pilot for at least one contract board period of real time (CR-004)", () => { const restRealMinutes = (gameConfig.restGameHours * 60) / gameConfig.timeCompression; expect(restRealMinutes).toBeGreaterThanOrEqual(gameConfig.contractBoardPeriodRealMinutes); }); it("has positive duty numbers (CR-004)", () => { expect(gameConfig.dutyLimitGameHours).toBeGreaterThan(0); expect(gameConfig.longHaulDutyBonusGameHours).toBeGreaterThan(0); }); }); describe("tutorial settings (Step 17.1, decisions 0.5.106, 0.5.107)", () => { it("runs five tutorial runs before the deal, at double XP", () => { expect(gameConfig.tutorialRunsBeforeDeal).toBe(5); expect(gameConfig.tutorialXpMultiplier).toBe(2); }); it("never lets the tutorial XP multiplier fall below 1", () => { expect(gameConfigSchema.safeParse(gameConfig).success).toBe(true); expect(gameConfigSchema.safeParse({ ...gameConfig, tutorialXpMultiplier: 0.5 }).success).toBe( false, ); }); }); describe("independent pilots config (Step 21.1, CR-021)", () => { const settings = gameConfig.independentPilots; const withSettings = (change: Partial) => gameConfigSchema.safeParse({ ...gameConfig, independentPilots: { ...settings, ...change } }) .success; it("has every setting, all positive, at the CR's defaults", () => { const lighter = shipModels.find((s) => s.id === "lighter")!; expect(settings).toEqual({ dispatchOfficePrice: lighter.price, firstLookRealMinutes: 3, turnaroundGameMinutes: 15, emptyPeriodsBeforeHome: 3, maxCatchUpRealDays: 7, defaultSpendingFloor: 500, }); for (const value of Object.values(settings)) expect(value).toBeGreaterThan(0); }); it("rejects zero or negative values", () => { for (const key of Object.keys(settings) as (keyof typeof settings)[]) { expect(withSettings({ [key]: 0 }), key).toBe(false); expect(withSettings({ [key]: -1 }), key).toBe(false); } }); it("keeps the first-look window shorter than the board period", () => { expect(settings.firstLookRealMinutes).toBeLessThan(gameConfig.contractBoardPeriodRealMinutes); expect(withSettings({ firstLookRealMinutes: gameConfig.contractBoardPeriodRealMinutes })).toBe( false, ); }); }); describe("fleet sleep config (Step 24.1, decision 0.5.137)", () => { const withSleep = (fleetSleep: { defaultHours: number; optionsHours: number[] }) => gameConfigSchema.safeParse({ ...gameConfig, fleetSleep }).success; it("defaults to 24 hours, from 12 h, 24 h, 48 h or 3 days", () => { expect(gameConfig.fleetSleep).toEqual({ defaultHours: 24, optionsHours: [12, 24, 48, 72] }); }); it("rejects a default that isn't an option, and options out of order or repeated", () => { expect(withSleep({ defaultHours: 36, optionsHours: [12, 24, 48] })).toBe(false); expect(withSleep({ defaultHours: 24, optionsHours: [24, 12] })).toBe(false); expect(withSleep({ defaultHours: 24, optionsHours: [24, 24] })).toBe(false); expect(withSleep({ defaultHours: 0, optionsHours: [0] })).toBe(false); expect(withSleep({ defaultHours: 12, optionsHours: [12] })).toBe(true); }); }); describe("auto-repair config (Step 27.1, decision 0.5.157)", () => { const withAuto = (autoRepair: typeof gameConfig.autoRepair) => gameConfigSchema.safeParse({ ...gameConfig, autoRepair }).success; it("defaults to 40%, from 10% to 90% in steps of 5", () => { expect(gameConfig.autoRepair).toEqual({ defaultThresholdPct: 40, minPct: 10, maxPct: 90, stepPct: 5, }); }); it("rejects a default off the steps or outside the range", () => { expect(withAuto({ defaultThresholdPct: 42, minPct: 10, maxPct: 90, stepPct: 5 })).toBe(false); expect(withAuto({ defaultThresholdPct: 95, minPct: 10, maxPct: 90, stepPct: 5 })).toBe(false); expect(withAuto({ defaultThresholdPct: 40, minPct: 10, maxPct: 90, stepPct: 5 })).toBe(true); }); });