From 8d8df71ba99b73b17ac6a5b7ee6d0c6069c14658 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Mon, 5 Oct 2026 01:38:50 -0400 Subject: [PATCH] Step 10L.5: upgrade caps for the new lineup Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 7 +++ apps/web/test/effective-stats-ui.test.tsx | 6 +-- memory-bank/progress.md | 2 +- packages/content/src/data/upgrades.ts | 6 +-- packages/content/test/upgrades.test.ts | 7 +++ packages/sim/test/courier-mars.test.ts | 7 +-- packages/sim/test/upgrade-caps.test.ts | 54 +++++++++++++++++++++++ 7 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 packages/sim/test/upgrade-caps.test.ts diff --git a/DECISIONS.md b/DECISIONS.md index a188012..863605a 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1157,3 +1157,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Clipper stays at 198**: Earth→Mars is in range on just over 30 % of dates, and Moon→Ceres (198 × 1.3 = 257.4) stays below Ceres's closest approach (262.8). The margins are small, so the Clipper's range shouldn't move without re-running this table. - **Decision 0.5.25 is superseded** (noted in the plan): the Freighter no longer reaches Jupiter from Earth at all. - **No other stats changed.** The existing upgrade-cap tests (0.5.67) and the Courier-to-Mars test (0.5.68) still pass at these values; 10L.5 adds the new caps. + +## Step 10L.5 + +- **Courier tanks 600 → 450 % per level.** Over 16 years Mars comes as close as 58.9M km from Earth, so the old level 2 (65M km) would have reached it, breaking decision 0.5.68. At 450 % level 2 is 50M km (never) and level 3 is 72.5M km, which reaches Mars only in the closest approaches (months 150 and 176–177 of the 16 years sampled). A maxed Courier now reaches Mars rarely, which fits "near closest approach only". +- **The Freighter's 15 % tanks override stays**: 450 → 652.5M km maxed, short of the Bulk Hauler (1,250) and of Jupiter from Earth at its farthest (965). Its comment now quotes the new numbers. +- **No caps for the new models**: with the default percentages a maxed Clipper holds 43.5 t (Freighter 100), a maxed Courier flies 3.9M km/h (Clipper 6.5M), and a maxed Lighter's range is 16M km (20.8 from the Moon), far below Mars's closest 58.5. +- **The Lighter cap is a sim test** because it needs distances; the plain stat inequalities stay in the content test. diff --git a/apps/web/test/effective-stats-ui.test.tsx b/apps/web/test/effective-stats-ui.test.tsx index b39b46b..24c371c 100644 --- a/apps/web/test/effective-stats-ui.test.tsx +++ b/apps/web/test/effective-stats-ui.test.tsx @@ -7,9 +7,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; -/** Mars near closest approach in the first two game years (~82M km, decision 0.5.68). */ +/** Mars at its closest in 16 game years (~59M km, month 176; decision 0.5.68). */ const gameStart = realToGame(Date.parse(content.config.epoch.realStart), content.config); -const NOW = gameToReal(gameStart + 23 * 30.44 * 24 * 3_600_000, content.config); +const NOW = gameToReal(gameStart + 176 * 30.44 * 24 * 3_600_000, content.config); const courier = (upgrades: Record): Ship => ({ id: "s1", @@ -99,7 +99,7 @@ describe("effective stats in the client (Step 10I.7)", () => { content.location("mars")!, realToGame(NOW, content.config), ); - expect(distance).toBeLessThan(95); + expect(distance).toBeLessThan(72.5); const open = async () => { const card = (await screen.findByText("Dusty Mule")).closest("li")!; diff --git a/memory-bank/progress.md b/memory-bank/progress.md index e2a7dff..5dfa78c 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -273,7 +273,7 @@ Spec: `memory-bank/change-requests/CR-012-lighter-clipper-range.md` (received as - [x] **Step 10L.2 — Sim: effective range from origin.** `rangeFrom(ship, from)` = range × origin's `launchRangeFactor`; `checkRoute` compares the leg against it (dispatch, reposition, board eligibility and the map all go through it). The dispatch sheet quotes range from the origin. Tests (`range-from.test.ts`): 100 → 130 from the Moon and 115 from Mars; a leg between 1× and 1.3× fails from Earth and passes from the Moon; with every factor 1 the result matches the old check over 16 years of samples. - [x] **Step 10L.3 — Sim: no-strand rule.** `wouldStrand(stats, origin, destination, mass, departure, discoveredIds, content)`: at arrival plus `noStrandMarginGameHours` (48), can the ship reach any other discovered location from the destination with its range from there? `validateDispatch` (new optional `discoveredIds`; absent = all) and `validateReposition` fail with `would-strand` after range and landing. The server passes the player's discovered ids for contract dispatches; server and web reason maps gained the code (full wording in 10L.6/10L.9). Tests (`no-strand.test.ts`): a fixture heavy-Moon hop is refused, then passes once a reachable location is discovered; the same for a contract; across all six models, every ordered pair and monthly samples over 16 years, an allowed trip always leaves somewhere to fly at arrival plus margin. - [x] **Step 10L.4 — Content tuning: reach table.** Ranges: Courier 5, Lighter 10, Clipper 198 (unchanged from 10L.1), **Freighter 650 → 450**, **Bulk Hauler 1000 → 1250**. Step 3.7's range tests (`eligibility.test.ts`) now name their origin; the Freighter-to-Ceres-at-farthest and Freighter-to-Jupiter-at-closest tests become "reaches Ceres at its closest, not its farthest" and "never reaches Jupiter from Earth". Tests (`reach-table.test.ts`): one per §3 cell over monthly samples for 16 game years, plus the two hop tests. -- [ ] Step 10L.5 — Upgrade caps for the new lineup +- [x] **Step 10L.5 — Upgrade caps for the new lineup.** Courier fuel-tanks override 600 → **450 %** per level (5 → 27.5 → 50 → 72.5M km); the Freighter override stays at 15 % (450 → 652.5). No Lighter or Clipper caps were needed. Tests: the new 0.5.67 inequalities (content `upgrades.test.ts`; sim `upgrade-caps.test.ts`: a maxed Lighter never reaches Mars from Earth or the Moon, only a Bulk Hauler reaches Jupiter from Earth at its farthest even with every ship maxed); `courier-mars.test.ts` now samples 16 years from Earth. The web deadhead-to-Mars test moved to month 176, Mars's closest approach. - [ ] Step 10L.6 — Schema and server - [ ] Step 10L.7 — Client: Market shows roles and reach - [ ] Step 10L.8 — Client: reachability from where ships actually are diff --git a/packages/content/src/data/upgrades.ts b/packages/content/src/data/upgrades.ts index 41b9570..5d5d593 100644 --- a/packages/content/src/data/upgrades.ts +++ b/packages/content/src/data/upgrades.ts @@ -27,9 +27,9 @@ export const upgrades: Upgrade[] = [ percentPerLevel: 20, maxLevel: 3, modelOverrides: { - // 5 → 35 → 65 → 95 M km: Mars (≈ 55–400 M km) only near closest approach, and only at level 3. - courier: { percentPerLevel: 600 }, - // 650 → 942.5 M km at level 3: still short of a base Bulk Hauler's 1000 (and of Jupiter at its farthest). + // 5 → 27.5 → 50 → 72.5 M km: Mars from Earth (≈ 59–400 M km) only near closest approach, and only at level 3. + courier: { percentPerLevel: 450 }, + // 450 → 652.5 M km at level 3: short of a base Bulk Hauler's 1250 and of Jupiter from Earth at its farthest (≈ 965). freighter: { percentPerLevel: 15 }, }, }, diff --git a/packages/content/test/upgrades.test.ts b/packages/content/test/upgrades.test.ts index 9ad6625..622397f 100644 --- a/packages/content/test/upgrades.test.ts +++ b/packages/content/test/upgrades.test.ts @@ -71,5 +71,12 @@ describe("ship upgrades content (CR-009)", () => { expect(maxed("freighter", "fuelRangeMillionKm")).toBeLessThan( model("bulk-hauler").fuelRangeMillionKm, ); + // Amended by CR-012 (the Lighter's Mars cap is a sim test: upgrade-caps.test.ts). + expect(maxed("clipper", "cargoCapacityTonnes")).toBeLessThan( + model("freighter").cargoCapacityTonnes, + ); + expect(maxed("courier", "cruiseSpeedKmPerGameHour")).toBeLessThan( + model("clipper").cruiseSpeedKmPerGameHour, + ); }); }); diff --git a/packages/sim/test/courier-mars.test.ts b/packages/sim/test/courier-mars.test.ts index f74a8b0..31a2a09 100644 --- a/packages/sim/test/courier-mars.test.ts +++ b/packages/sim/test/courier-mars.test.ts @@ -14,14 +14,15 @@ const withTanks = (level: number): ShipModel => ({ courier.fuelRangeMillionKm * (1 + (upgradePercentPerLevel(tanks, "courier") * level) / 100), }); -/** Monthly samples over the first two game years. */ +/** Monthly samples over 16 game years (CR-012). */ const MONTH = 30.44 * 24 * 3_600_000; const start = realToGame(Date.parse(content.config.epoch.realStart), content.config); -const samples = Array.from({ length: 25 }, (_, i) => start + i * MONTH); +const samples = Array.from({ length: 16 * 12 + 1 }, (_, i) => start + i * MONTH); const inRange = (ship: ShipModel) => samples.map((t) => checkRoute(ship, earth, mars, t)).map((r) => r.ok); -describe("Courier to Mars (decision 0.5.68)", () => { +// Measured from Earth (decision 0.5.68 as amended by CR-012). +describe("Courier to Mars from Earth (decision 0.5.68)", () => { it("reaches Mars with maxed fuel tanks near closest approach, but not at its farthest", () => { const reach = inRange(withTanks(3)); expect(reach.some(Boolean)).toBe(true); diff --git a/packages/sim/test/upgrade-caps.test.ts b/packages/sim/test/upgrade-caps.test.ts new file mode 100644 index 0000000..1946f13 --- /dev/null +++ b/packages/sim/test/upgrade-caps.test.ts @@ -0,0 +1,54 @@ +import { content, upgradeMaxLevel, upgradePercentPerLevel, upgrades } from "@deadhead/content"; +import { describe, expect, it } from "vitest"; +import { checkRoute, effectiveStats, realToGame, routeDistance } from "../src/index"; + +/** Every upgrade at its highest level for the model (decision 0.5.63). */ +const maxedStats = (modelId: string) => + effectiveStats( + content.shipModel(modelId)!, + Object.fromEntries(upgrades.map((u) => [u.id, upgradeMaxLevel(u, modelId)])), + content, + ); + +const MONTH = 30.44 * 24 * 3_600_000; +const start = realToGame(Date.parse(content.config.epoch.realStart), content.config); +const samples = Array.from({ length: 16 * 12 + 1 }, (_, i) => start + i * MONTH); +const earth = content.location("earth")!; +const moon = content.location("moon")!; +const mars = content.location("mars")!; +const jupiter = content.location("jupiter-cloud-platform")!; + +describe("upgrade caps for the CR-012 lineup (decision 0.5.67)", () => { + it("a maxed Lighter never reaches Mars from Earth or the Moon", () => { + const lighter = maxedStats("lighter"); + for (const from of [earth, moon]) { + for (const t of samples) expect(checkRoute(lighter, from, mars, t).ok).toBe(false); + } + }); + + it("a maxed Clipper carries less than a base Freighter", () => { + expect(maxedStats("clipper").cargoCapacityTonnes).toBeLessThan( + content.shipModel("freighter")!.cargoCapacityTonnes, + ); + }); + + it("a maxed Courier is slower than a base Clipper", () => { + expect(maxedStats("courier").cruiseSpeedKmPerGameHour).toBeLessThan( + content.shipModel("clipper")!.cruiseSpeedKmPerGameHour, + ); + }); + + it("only Bulk Haulers reach Jupiter from Earth at its farthest, even maxed", () => { + const farthest = samples.reduce((a, b) => + routeDistance(earth, jupiter, b) > routeDistance(earth, jupiter, a) ? b : a, + ); + for (const ship of content.shipModels) { + const reaches = checkRoute(maxedStats(ship.id), earth, jupiter, farthest).ok; + expect(reaches, ship.id).toBe(ship.id === "bulk-hauler"); + } + }); + + it("gives the Courier 450 % range per tanks level", () => { + expect(upgradePercentPerLevel(content.upgrade("fuel-tanks")!, "courier")).toBe(450); + }); +});