deadhead-space-freight-tycoon/packages/sim/test/reach-table.test.ts

154 lines
5.6 KiB
TypeScript

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<string, string> = {
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<string, string> = {
"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);
}
}
});
});