From d626989779f359bcb289077ec25727bba59430a5 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sat, 10 Oct 2026 13:39:46 -0400 Subject: [PATCH] Step 16.3: content tuning: reach Every cell of the CR-015 reach table and the CR-016 additions holds with the existing tanks, so no content changes: the step adds one test per new cell, the Bulk Hauler's 900M km minimum, and the Courier's Jupiter-system loop. Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 4 ++ memory-bank/progress.md | 2 +- packages/sim/test/reach-table.test.ts | 79 ++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index e5442d8..3c6182e 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1512,3 +1512,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Each new place's Standard good came with the locations**, ahead of 16.2: the board generator refuses a place that makes nothing Standard. The rest of the goods changes stay in 16.2. - **First Market purchase target widened to 45–95 min** (user decision). With Venus as a second first-contact destination, the median moved from 64 to ~92 min. Purchases happen at the Clipper's long landings, so the median jumps between about 49, 64 and 92 min with small content changes; no tank-share, Lighter-price or first-contact-pay change landed it reliably inside 75. - **CR-007's class-mix regression is measured on its original five places**: new destinations change the mix legitimately (and so do first contacts to undiscovered places), so the baseline is compared against the content it was taken from. + +## Step 16.3 + +- **No content changed**: the Phase 15 tank sizes already satisfy CR-016 §3 with margins (the closest call is a Freighter from Mars to Io, ≈ 529M km of reach for ≈ 592). The step adds the tests only. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index e197f34..e797199 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -395,7 +395,7 @@ Spec: `memory-bank/change-requests/CR-016-venus-io-europa-belt.md` (received as - [x] **Step 16.1 — Content: new locations.** Venus Cloud City (cloud platform, atmospheric, 108M km at 110°, range factor 1.0, fuel × 1.3, bay 20,000), Io Sulfur Works (new type `orbital-station`, `vacuum` landing so any ship can land, 0.42M km out from Jupiter, factor 1.2, fuel × 1.0, bay 70,000) and Europa Ice Station (new type `surface-base`, `vacuum`, 0.67M km out from Jupiter, factor 1.3, fuel × 0.7, bay 70,000); all undiscovered, selling fuel, with the CR's shippers and class mixes. Pulled forward so the game keeps working: each new place's Standard good (Cloud-Grown Botanicals at Venus, a new good; Mail & Parcels also produced at Io and Europa), since boards can't be generated where nothing Standard is made; and the three body sprites in the art manifest, with their generated art (the belt is still 16.5). The balance check's first Market purchase moved from 64 to ~92 min (the Clipper's landings decide it, and Venus changed which first contacts boards offer); the user widened the window to 45–95. Tests: `packages/content/test/fixed-map.test.ts` (Earth/Moon–Venus ≈ 200, Venus–Mars ≈ 332, Venus–Ceres ≈ 520, Venus–Jupiter ≈ 885, Ceres–Io/Europa ≈ 393, all within 2 %; the Jupiter system within 1.1M km; Io and Europa ride with Jupiter at their offsets; the three undiscovered, selling fuel, not Hardened; only Earth and the Moon start discovered); `locations.test.ts` (eight locations; fuel prices ordered Jupiter < Europa < Ceres < Io < Moon < Venus < Mars < Earth). Updated: bundle count, first contacts may go to Mars or Venus, the CR-007 class-mix baseline measured on its original five places, orbit rings for every body but those riding a parent, the Market reach line now listing Venus. - [x] **Step 16.2 — Content: goods.** New goods: Atmospheric Data Cores (High-Value tier 2, Venus → Earth), Refined Sulfur (Heavy, Io → Earth, Mars, Venus), Volcanic Isotopes (High-Value tier 3, Io → Earth), Subsurface Ocean Samples (High-Value tier 3, Priority, Europa → Earth); Cloud-Grown Botanicals came in 16.1. Water Ice and Liquid Oxygen are also made at Europa, Industrial Chemicals at Venus; Water Ice goes to Io and Venus, Liquid Oxygen to Io, Habitat Prefabs to Io and Europa, and Packaged Food, Consumer Goods, Mail & Parcels, Medical Supplies, Electronics and Machine Parts to all three new places. Balance medians: first purchase 92.3, discovery 48.9, loan 160.5, four ships 147 vs 156. Tests (`packages/content/test/goods.test.ts`): the five new goods with their classes, tiers and routes (Sulfur has no tier of its own); every location makes a Standard good and wants at least one; each new place is a destination for at least five goods; the new producers on existing goods. -- [ ] Step 16.3 — Content tuning: reach +- [x] **Step 16.3 — Content tuning: reach.** No tuning needed: with Phase 15's tanks every cell of CR-015 §4 and of CR-016 §3 already holds (Clipper ≈ 252M km reaches Venus at ≈ 200 but not Mars from Venus at ≈ 332; Freighter ≈ 460 reaches Mars from Venus and Io/Europa from Ceres at ≈ 393 but not Ceres from Venus at ≈ 520, nor Io from Mars at ≈ 592 × 1.15 short; Bulk Hauler ≈ 1,251 covers Venus–Jupiter at ≈ 885). Tests (`reach-table.test.ts`, CR-016 §3): one test per new cell (the Jupiter-system loop for Courier, Lighter, Clipper and Freighter; Earth↔Venus and Moon→Venus for Clipper and Freighter, not Courier or Lighter; Venus→Mars only from the Freighter up; Ceres↔Io/Europa for the Freighter; nothing from the inner system to the Jupiter system short of the Bulk Hauler, which flies every leg between all eight); the Bulk Hauler's full-tank reach ≥ 900M km; a Courier at the platform flies to Io and Europa and back without stranding. The no-strand test already covers every leg between all eight places. - [ ] Step 16.4 — Engine: save migration and boards - [ ] Step 16.5 — Assets: sprites and belt rocks - [ ] Step 16.6 — Map: belt haze diff --git a/packages/sim/test/reach-table.test.ts b/packages/sim/test/reach-table.test.ts index e8dc010..e7fb050 100644 --- a/packages/sim/test/reach-table.test.ts +++ b/packages/sim/test/reach-table.test.ts @@ -1,6 +1,6 @@ import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; -import { checkRoute } from "../src/index"; +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 @@ -74,3 +74,80 @@ describe("reach table (CR-015 §4)", () => { 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 = { + "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); + } + } + }); +});