deadhead-space-freight-tycoon/packages/sim/test/no-strand.test.ts

109 lines
3.4 KiB
TypeScript

import { content, type Location } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import {
checkRoute,
effectiveStats,
validateDispatch,
validateReposition,
wouldStrand,
type Contract,
type PilotState,
type RepositionProposal,
type ShipState,
type SimContent,
} from "../src/index";
const loc = (id: string) => content.location(id)!;
const now = Date.parse("2100-01-01T00:30:00Z");
const pilot: PilotState = {
id: "p1",
isAvatar: false,
assignedShipId: "s1",
inFlight: false,
skills: {},
restEndsAt: null,
};
/**
* Fixture: a short-legged ship and a heavy Moon (factor 0.5), so the ship can
* reach the Moon from Earth but can't climb back out. "moon-station" shares the
* Moon's position, so it is always in reach from there.
*/
const heavyMoon: Location = { ...loc("moon"), launchRangeFactor: 0.5 };
const station: Location = { ...heavyMoon, id: "moon-station", name: "Moon Station" };
const fixture: SimContent = {
...content,
locations: [...content.locations.filter((l) => l.id !== "moon"), heavyMoon, station],
shipModels: [
...content.shipModels,
{ ...content.shipModel("courier")!, id: "hopper", fuelRangeMillionKm: 0.45 },
],
};
const hopper: ShipState = { id: "s1", modelId: "hopper", locationId: "earth", inFlight: false };
const proposal: RepositionProposal = {
ship: hopper,
pilot,
destinationId: "moon",
discoveredIds: new Set(["earth", "moon"]),
credits: 100_000,
now,
};
const contract: Contract = {
id: "c1",
originId: "earth",
destinationId: "moon",
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 10,
payout: 2000,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: Date.parse("2100-01-01T00:00:00Z"),
expiresAt: Date.parse("2100-01-01T01:00:00Z"),
firstContact: false,
};
describe("no-strand rule (Step 10L.3, decision 0.5.79)", () => {
it("rejects a deadhead that would leave the ship unable to fly anywhere", () => {
expect(validateReposition(proposal, fixture)).toEqual({ ok: false, reason: "would-strand" });
});
it("passes the same trip once a reachable location is discovered", () => {
const discoveredIds = new Set(["earth", "moon", "moon-station"]);
expect(validateReposition({ ...proposal, discoveredIds }, fixture)).toEqual({ ok: true });
});
it("applies to contract dispatches too", () => {
const base = { ship: hopper, pilot, contract, tutorial: false, credits: 100_000, now };
expect(
validateDispatch({ ...base, discoveredIds: new Set(["earth", "moon"]) }, fixture),
).toEqual({ ok: false, reason: "would-strand" });
expect(
validateDispatch(
{ ...base, discoveredIds: new Set(["earth", "moon", "moon-station"]) },
fixture,
),
).toEqual({ ok: true });
});
it("never blocks a base ship on a leg it can fly between the five locations (fixed map)", () => {
const ids = content.locations.map((l) => l.id);
for (const base of content.shipModels) {
const model = effectiveStats(base, {}, content);
for (const from of ids) {
for (const to of ids) {
if (from === to || !checkRoute(model, loc(from), loc(to)).ok) continue;
expect(wouldStrand(model, loc(to), undefined, content), `${model.id} ${from}→${to}`).toBe(
false,
);
}
}
}
});
});