154 lines
5.6 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
});
|
|
});
|