import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { checkRoute, fullTankReach, wouldStrand } from "../src/index"; /** * CR-015 §4: reach for base ships on the fixed map, a plain yes or no per leg * (decision 0.5.81 as amended). Replaces CR-012's date-sampled table. */ const NAMES: Record = { earth: "Earth", moon: "Moon", mars: "Mars", "ceres-station": "Ceres", "jupiter-cloud-platform": "Jupiter", }; const ids = Object.keys(NAMES); function reaches(modelId: string, fromId: string, toId: string): boolean { const ship = content.shipModel(modelId)!; return checkRoute(ship, content.location(fromId)!, content.location(toId)!, content.config).ok; } /** [model, from, to] for every leg the table says yes or no to. */ type Leg = [string, string, string]; const both = (model: string, a: string, b: string): Leg[] => [ [model, a, b], [model, b, a], ]; const toJupiterFromAnywhere = (model: string): Leg[] => ids .filter((id) => id !== "jupiter-cloud-platform") .map((from) => [model, from, "jupiter-cloud-platform"]); const yes: Leg[] = [ ...both("courier", "earth", "moon"), ...both("lighter", "earth", "moon"), ...both("clipper", "earth", "moon"), ...both("clipper", "earth", "mars"), ["clipper", "moon", "mars"], ["clipper", "mars", "ceres-station"], // The Freighter does everything the Clipper does, plus Earth↔Ceres and Ceres↔Jupiter. ...both("freighter", "earth", "moon"), ...both("freighter", "earth", "mars"), ["freighter", "moon", "mars"], ["freighter", "mars", "ceres-station"], ...both("freighter", "earth", "ceres-station"), ...both("freighter", "ceres-station", "jupiter-cloud-platform"), ...ids.flatMap((from) => ids.filter((to) => to !== from).map((to): Leg => ["bulk-hauler", from, to]), ), ]; const no: Leg[] = [ ["courier", "earth", "mars"], ["courier", "moon", "mars"], ["lighter", "earth", "mars"], ["lighter", "moon", "mars"], ["clipper", "earth", "ceres-station"], ["clipper", "moon", "ceres-station"], ...toJupiterFromAnywhere("clipper"), ["freighter", "earth", "jupiter-cloud-platform"], ["freighter", "moon", "jupiter-cloud-platform"], ["freighter", "mars", "jupiter-cloud-platform"], ]; const label = ([model, from, to]: Leg) => `${model} ${NAMES[from]}→${NAMES[to]}`; describe("reach table (CR-015 §4)", () => { it.each(yes.map((leg) => [label(leg), leg] as const))("yes: %s", (_name, [m, from, to]) => { expect(reaches(m, from, to)).toBe(true); }); it.each(no.map((leg) => [label(leg), leg] as const))("no: %s", (_name, [m, from, to]) => { expect(reaches(m, from, to)).toBe(false); }); }); /** CR-016 §3: additions for Venus and the Jupiter system (decision 0.5.102). */ const NEW_NAMES: Record = { "venus-cloud-city": "Venus", "io-sulfur-works": "Io", "europa-ice-station": "Europa", }; const allNames = { ...NAMES, ...NEW_NAMES }; const allIds = Object.keys(allNames); const label16 = ([model, from, to]: Leg) => `${model} ${allNames[from]}→${allNames[to]}`; const loop = ["jupiter-cloud-platform", "io-sulfur-works", "europa-ice-station"]; const localLoop = (model: string): Leg[] => loop.flatMap((from) => loop.filter((to) => to !== from).map((to): Leg => [model, from, to])); const inner = ["earth", "moon", "mars", "venus-cloud-city"]; const innerToJupiterSystem = (model: string): Leg[] => inner.flatMap((from) => loop.map((to): Leg => [model, from, to])); const yes16: Leg[] = [ ...localLoop("courier"), ...localLoop("lighter"), ...both("clipper", "earth", "venus-cloud-city"), ["clipper", "moon", "venus-cloud-city"], ...localLoop("clipper"), // The Freighter does everything the Clipper does, plus these. ...both("freighter", "earth", "venus-cloud-city"), ["freighter", "moon", "venus-cloud-city"], ...localLoop("freighter"), ["freighter", "venus-cloud-city", "mars"], ...both("freighter", "ceres-station", "io-sulfur-works"), ...both("freighter", "ceres-station", "europa-ice-station"), ...allIds.flatMap((from) => allIds.filter((to) => to !== from).map((to): Leg => ["bulk-hauler", from, to]), ), ]; const no16: Leg[] = [ ["courier", "earth", "venus-cloud-city"], ["courier", "moon", "venus-cloud-city"], ["lighter", "earth", "venus-cloud-city"], ["lighter", "moon", "venus-cloud-city"], ["clipper", "venus-cloud-city", "mars"], ...innerToJupiterSystem("clipper"), ["freighter", "venus-cloud-city", "ceres-station"], ...innerToJupiterSystem("freighter"), ]; describe("reach additions (CR-016 §3)", () => { it.each(yes16.map((leg) => [label16(leg), leg] as const))("yes: %s", (_name, [m, from, to]) => { expect(reaches(m, from, to)).toBe(true); }); it.each(no16.map((leg) => [label16(leg), leg] as const))("no: %s", (_name, [m, from, to]) => { expect(reaches(m, from, to)).toBe(false); }); it("gives the Bulk Hauler at least 900M km on a full tank", () => { const bulk = content.shipModel("bulk-hauler")!; expect(fullTankReach(bulk, content.location("earth")!, content.config)).toBeGreaterThanOrEqual( 900, ); }); it("lets a Courier at Jupiter's platform fly to Io and Europa and back, without stranding", () => { const courier = content.shipModel("courier")!; const all = new Set(allIds); for (const moon of ["io-sulfur-works", "europa-ice-station"]) { for (const [from, to] of [ ["jupiter-cloud-platform", moon], [moon, "jupiter-cloud-platform"], ] as const) { expect(reaches("courier", from, to), `${from}→${to}`).toBe(true); expect(wouldStrand(courier, content.location(to)!, 0, all, content)).toBe(false); } } }); });