From 36bdbf3af0a74abfd63956880ebd70495bd25c19 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Fri, 2 Oct 2026 18:42:27 -0400 Subject: [PATCH] Step 3.5: add travel time Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 4 ++++ memory-bank/progress.md | 2 +- packages/sim/src/index.ts | 1 + packages/sim/src/travel.ts | 10 ++++++++++ packages/sim/test/travel.test.ts | 28 ++++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/sim/src/travel.ts create mode 100644 packages/sim/test/travel.test.ts diff --git a/DECISIONS.md b/DECISIONS.md index a1b50ae..f453b6a 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -98,3 +98,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` ## Step 3.4 - **Route distance is the straight line between the two positions at one game time.** Orbital transfer paths are not modelled. + +## 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. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index dacbf7e..394f65b 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -23,7 +23,7 @@ - [x] **Step 3.2 — Seeded RNG.** `src/rng.ts`: `createRng(seed)` (mulberry32) with `next`, `int`, `pick`; `deriveSeed(parent, label)` for child seeds. - [x] **Step 3.3 — Location positions.** `src/positions.ts`: heliocentric ecliptic positions (million km) from `astronomy-engine` for Earth, Mars and Jupiter; Moon = Earth + fixed offset; Ceres on a circular orbit. - [x] **Step 3.4 — Route distance.** `src/distance.ts`: `routeDistance(from, to, gameTime)`, straight-line million km. -- [ ] Step 3.5 — Travel time +- [x] **Step 3.5 — Travel time.** `src/travel.ts`: `travelTimeGameHours(ship, distance)` = cruise time + overhead. - [ ] Step 3.6 — Fuel cost - [ ] Step 3.7 — Range and landing eligibility - [ ] Step 3.8 — Contract model and payout diff --git a/packages/sim/src/index.ts b/packages/sim/src/index.ts index cc8060d..5ddcd31 100644 --- a/packages/sim/src/index.ts +++ b/packages/sim/src/index.ts @@ -2,3 +2,4 @@ export * from "./clock"; export * from "./rng"; export * from "./positions"; export * from "./distance"; +export * from "./travel"; diff --git a/packages/sim/src/travel.ts b/packages/sim/src/travel.ts new file mode 100644 index 0000000..10d3faa --- /dev/null +++ b/packages/sim/src/travel.ts @@ -0,0 +1,10 @@ +import type { ShipModel } from "@deadhead/content"; + +/** + * Trip duration in game hours: cruise time plus the ship's fixed takeoff/landing + * overhead. Pass the distance at departure; it is not recalculated mid-flight. + */ +export function travelTimeGameHours(ship: ShipModel, distanceMillionKm: number): number { + const cruiseHours = (distanceMillionKm * 1e6) / ship.cruiseSpeedKmPerGameHour; + return cruiseHours + ship.overheadGameHours; +} diff --git a/packages/sim/test/travel.test.ts b/packages/sim/test/travel.test.ts new file mode 100644 index 0000000..edfa157 --- /dev/null +++ b/packages/sim/test/travel.test.ts @@ -0,0 +1,28 @@ +import { content } from "@deadhead/content"; +import { describe, expect, it } from "vitest"; +import { routeDistance, travelTimeGameHours } from "../src/index"; + +const courier = content.shipModel("courier")!; +const freighter = content.shipModel("freighter")!; + +describe("travel time", () => { + it("matches a hand calculation", () => { + // 150M km at 2M km/h = 75 h cruise, plus 4 h overhead. + expect(travelTimeGameHours(freighter, 150)).toBe(79); + }); + + it("gets a faster ship there sooner on the same route", () => { + for (const distance of [0.384, 50, 400]) { + expect(travelTimeGameHours(courier, distance)).toBeLessThan( + travelTimeGameHours(freighter, distance), + ); + } + }); + + it("is mostly overhead for a Courier on Earth→Moon", () => { + const t = Date.parse("2100-01-01T00:00:00Z"); + const distance = routeDistance(content.location("earth")!, content.location("moon")!, t); + const total = travelTimeGameHours(courier, distance); + expect(courier.overheadGameHours).toBeGreaterThan(total / 2); + }); +});