import { describe, expect, it } from "vitest"; import type { z } from "zod"; import { cargoTypeSchema, gameConfigSchema, locationSchema, onboardingGoalSchema, scriptedTutorialContractSchema, shipModelSchema, skillSchema, } from "../src/index"; const validLocation = { id: "earth", name: "Earth", type: "atmospheric-world", landingRequirement: "atmospheric", body: "earth", launchCostMultiplier: 3, startsDiscovered: true, bayLeasePrice: 5000, sprite: "body-mars", produces: ["standard"], demands: ["heavy"], }; const validShipModel = { id: "courier", name: "Courier", archetype: "courier", price: 20000, cruiseSpeedKmPerGameHour: 300000, cargoCapacityTonnes: 10, fuelRangeMillionKm: 5, hullRating: 1, landingClass: "atmospheric", overheadGameHours: 2, fuelCostPerMillionKm: 100, purchasable: true, sprite: "ship-courier", }; const validCargoType = { id: "standard", name: "Standard", basePayPerTonnePerMillionKm: 10, tierPayMultiplier: 1, requiredSkillId: null, }; const validSkill = { id: "long-haul", name: "Long Haul", maxLevel: 5, description: "Access to longer routes.", }; const validGameConfig = { timeCompression: 60, epoch: { realStart: "2026-01-01T00:00:00Z", ephemerisDate: "2100-01-01T00:00:00Z" }, startingCredits: 0, tutorialPayoutShare: 0.2, contractBoardSize: 8, tutorialBoardSize: 50, contractBoardPeriodRealMinutes: 60, firstContactChance: 0.1, firstContactPayoutMultiplier: 2, contractMassBands: [ { min: 1, max: 20, count: 4 }, { min: 21, max: 100, count: 4 }, ], longHaulCutoffsMillionKm: [100, 200, 300, 400, 500], xpPerDelivery: { base: 10, perMillionKm: 0.1, perCargoTier: 5 }, pilotLevelXpThresholds: [100, 250, 500], skillPointsPerLevel: 1, pilotHireCost: 1000, pilotPayPerDelivery: 100, tutorialLoanerShipModelId: "company-courier", tutorialRunsBeforeDeal: 4, tutorialMassTonnes: { light: 5, medium: 12, full: 20 }, handlingGameHoursPerTonne: 0.02, loanRepaymentShare: 0.3, dealLoanAmount: 5000, }; const validScripted = { id: "unmarked-crates", run: 3, name: "Unmarked crates", cargoTypeId: "standard", payMultiplier: 2, flavorMessage: "Nobody asked.", }; const validGoal = { id: "first-hire", title: "Hire your first pilot", condition: { kind: "pilots-hired", count: 1 }, reward: 1000, }; const cases: [string, z.ZodType, Record][] = [ ["Location", locationSchema, validLocation], ["ShipModel", shipModelSchema, validShipModel], ["CargoType", cargoTypeSchema, validCargoType], ["Skill", skillSchema, validSkill], ["GameConfig", gameConfigSchema, validGameConfig], ["ScriptedTutorialContract", scriptedTutorialContractSchema, validScripted], ["OnboardingGoal", onboardingGoalSchema, validGoal], ]; describe.each(cases)("%s schema", (_name, schema, valid) => { it("accepts a valid example", () => { expect(schema.safeParse(valid).success).toBe(true); }); it.each(Object.keys(valid))("rejects an object missing %s", (field) => { const missing = { ...valid }; delete missing[field]; expect(schema.safeParse(missing).success).toBe(false); }); }); describe("GameConfig refinements", () => { it("rejects tutorial mass tiers that do not increase", () => { const config = { ...validGameConfig, tutorialMassTonnes: { light: 12, medium: 12, full: 20 } }; expect(gameConfigSchema.safeParse(config).success).toBe(false); }); it("rejects a goal condition of an unknown kind", () => { const goal = { ...validGoal, condition: { kind: "win-the-game" } }; expect(onboardingGoalSchema.safeParse(goal).success).toBe(false); }); it("rejects a mass band with min above max", () => { const config = { ...validGameConfig, contractMassBands: [ { min: 50, max: 10, count: 4 }, { min: 21, max: 100, count: 4 }, ], }; expect(gameConfigSchema.safeParse(config).success).toBe(false); }); it("rejects mass band counts that do not add up to the board size", () => { const config = { ...validGameConfig, contractMassBands: [{ min: 1, max: 20, count: 3 }] }; expect(gameConfigSchema.safeParse(config).success).toBe(false); }); it("rejects Long Haul cut-offs that are not increasing", () => { const config = { ...validGameConfig, longHaulCutoffsMillionKm: [100, 100, 300] }; expect(gameConfigSchema.safeParse(config).success).toBe(false); }); });