Step 3.7: add range and landing eligibility
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ace0d8502d
commit
0e5faab454
|
|
@ -102,3 +102,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
## Step 3.5
|
## Step 3.5
|
||||||
|
|
||||||
- **Travel time takes the distance as a number**, not two locations and a time. The caller measures the distance at departure, so the function cannot accidentally use a mid-flight distance.
|
- **Travel time takes the distance as a number**, not two locations and a time. The caller measures the distance at departure, so the function cannot accidentally use a mid-flight distance.
|
||||||
|
|
||||||
|
## Step 3.7
|
||||||
|
|
||||||
|
- **Landing is checked before range**, so a ship that can neither land nor reach reports `cannot-land`.
|
||||||
|
- **The route check looks only at the destination's landing requirement.** A ship is always already sitting at its origin, so it can take off from there.
|
||||||
|
- **Range tests find each planet's closest and farthest point from Earth by sampling** every 4 days over 12 years. All Step 2.3 range choices hold, including decision 0.5.25 (a Freighter reaches Jupiter at its closest).
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
- [x] **Step 3.4 — Route distance.** `src/distance.ts`: `routeDistance(from, to, gameTime)`, straight-line million km.
|
- [x] **Step 3.4 — Route distance.** `src/distance.ts`: `routeDistance(from, to, gameTime)`, straight-line million km.
|
||||||
- [x] **Step 3.5 — Travel time.** `src/travel.ts`: `travelTimeGameHours(ship, distance)` = cruise time + overhead.
|
- [x] **Step 3.5 — Travel time.** `src/travel.ts`: `travelTimeGameHours(ship, distance)` = cruise time + overhead.
|
||||||
- [x] **Step 3.6 — Fuel cost.** `src/fuel.ts`: `fuelCost(ship, origin, distance)` = distance × cost per million km × origin launch multiplier.
|
- [x] **Step 3.6 — Fuel cost.** `src/fuel.ts`: `fuelCost(ship, origin, distance)` = distance × cost per million km × origin launch multiplier.
|
||||||
- [ ] Step 3.7 — Range and landing eligibility
|
- [x] **Step 3.7 — Range and landing eligibility.** `src/eligibility.ts`: `checkRoute` returns `ok` or `cannot-land` / `out-of-range`; `canLandAt` ranks vacuum < atmospheric < hardened. Step 2.3 ranges confirmed against sampled real distances.
|
||||||
- [ ] Step 3.8 — Contract model and payout
|
- [ ] Step 3.8 — Contract model and payout
|
||||||
- [ ] Step 3.9 — Contract board generation
|
- [ ] Step 3.9 — Contract board generation
|
||||||
- [ ] Step 3.10 — Tutorial contract board
|
- [ ] Step 3.10 — Tutorial contract board
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { LandingClass, Location, ShipModel } from "@deadhead/content";
|
||||||
|
import type { GameTime } from "./clock";
|
||||||
|
import { routeDistance } from "./distance";
|
||||||
|
|
||||||
|
const LANDING_RANK: Record<LandingClass, number> = { vacuum: 0, atmospheric: 1, hardened: 2 };
|
||||||
|
|
||||||
|
export type RouteCheck = { ok: true } | { ok: false; reason: "cannot-land" | "out-of-range" };
|
||||||
|
|
||||||
|
/** Whether a ship's landing class meets a location's landing requirement. */
|
||||||
|
export function canLandAt(ship: ShipModel, location: Location): boolean {
|
||||||
|
return LANDING_RANK[ship.landingClass] >= LANDING_RANK[location.landingRequirement];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether a ship can fly from one location to another at a game time, or why not. */
|
||||||
|
export function checkRoute(
|
||||||
|
ship: ShipModel,
|
||||||
|
from: Location,
|
||||||
|
to: Location,
|
||||||
|
gameTime: GameTime,
|
||||||
|
): RouteCheck {
|
||||||
|
if (!canLandAt(ship, to)) return { ok: false, reason: "cannot-land" };
|
||||||
|
if (routeDistance(from, to, gameTime) > ship.fuelRangeMillionKm) {
|
||||||
|
return { ok: false, reason: "out-of-range" };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,4 @@ export * from "./positions";
|
||||||
export * from "./distance";
|
export * from "./distance";
|
||||||
export * from "./travel";
|
export * from "./travel";
|
||||||
export * from "./fuel";
|
export * from "./fuel";
|
||||||
|
export * from "./eligibility";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { content, type ShipModel } from "@deadhead/content";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { checkRoute, routeDistance } from "../src/index";
|
||||||
|
|
||||||
|
const earth = content.location("earth")!;
|
||||||
|
const moon = content.location("moon")!;
|
||||||
|
const mars = content.location("mars")!;
|
||||||
|
const ceres = content.location("ceres-station")!;
|
||||||
|
const jupiter = content.location("jupiter-cloud-platform")!;
|
||||||
|
const courier = content.shipModel("courier")!;
|
||||||
|
const freighter = content.shipModel("freighter")!;
|
||||||
|
const bulkHauler = content.shipModel("bulk-hauler")!;
|
||||||
|
|
||||||
|
/** Test-only ship that cannot land on atmospheric worlds. */
|
||||||
|
const vacuumShip: ShipModel = { ...courier, id: "test-vacuum", landingClass: "vacuum" };
|
||||||
|
|
||||||
|
// Every 4 days across 12 years (one Jupiter orbit).
|
||||||
|
const start = Date.parse("2100-01-01T00:00:00Z");
|
||||||
|
const samples = Array.from({ length: 1096 }, (_, i) => start + i * 4 * 86_400_000);
|
||||||
|
|
||||||
|
/** The sampled time when `to` is closest to (or farthest from) Earth. */
|
||||||
|
function extremeTime(to: typeof mars, pick: "closest" | "farthest"): number {
|
||||||
|
let best = samples[0]!;
|
||||||
|
let bestDistance = routeDistance(earth, to, best);
|
||||||
|
for (const t of samples) {
|
||||||
|
const d = routeDistance(earth, to, t);
|
||||||
|
if (pick === "closest" ? d < bestDistance : d > bestDistance) {
|
||||||
|
best = t;
|
||||||
|
bestDistance = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("route eligibility", () => {
|
||||||
|
it("lets a Courier fly Earth→Moon and Moon→Earth", () => {
|
||||||
|
expect(checkRoute(courier, earth, moon, start)).toEqual({ ok: true });
|
||||||
|
expect(checkRoute(courier, moon, earth, start)).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a Courier out of range of Mars, Ceres and Jupiter even at their closest", () => {
|
||||||
|
for (const to of [mars, ceres, jupiter]) {
|
||||||
|
expect(checkRoute(courier, earth, to, extremeTime(to, "closest"))).toEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: "out-of-range",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a vacuum-only ship at Earth because it cannot land", () => {
|
||||||
|
expect(checkRoute(vacuumShip, moon, earth, start)).toEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: "cannot-land",
|
||||||
|
});
|
||||||
|
expect(checkRoute(vacuumShip, earth, moon, start)).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets a Freighter reach Mars and Ceres even at their farthest", () => {
|
||||||
|
for (const to of [mars, ceres]) {
|
||||||
|
expect(checkRoute(freighter, earth, to, extremeTime(to, "farthest"))).toEqual({ ok: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets only the Bulk Hauler reach Jupiter at its farthest", () => {
|
||||||
|
const t = extremeTime(jupiter, "farthest");
|
||||||
|
expect(checkRoute(bulkHauler, earth, jupiter, t)).toEqual({ ok: true });
|
||||||
|
expect(checkRoute(freighter, earth, jupiter, t).ok).toBe(false);
|
||||||
|
expect(checkRoute(courier, earth, jupiter, t).ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets a Freighter reach Jupiter at its closest (decision 0.5.25)", () => {
|
||||||
|
expect(checkRoute(freighter, earth, jupiter, extremeTime(jupiter, "closest"))).toEqual({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue