From dff2c576cf1ccbfcf2521bf42f389890fbc26abb Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sat, 3 Oct 2026 14:41:56 -0400 Subject: [PATCH] Step 10B.1: generate contract boards for any location Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 4 ++++ apps/server/src/game/boards.ts | 2 +- memory-bank/progress.md | 2 +- packages/sim/src/board.ts | 8 +++++--- packages/sim/src/resolution.ts | 2 +- packages/sim/test/board.test.ts | 21 +++++++++++++++++++-- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index badc608..ae8025e 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -617,3 +617,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Change request CR-002 is stored in `memory-bank/change-requests/`** and inserted as Phase 10B, before Phase 11. - **Decision numbering:** the CR's new 0.5.27 (Ship delivery) and 0.5.28 (Undiscovered locations) are recorded as **0.5.35** and **0.5.36**, since 27–34 were already in use. Decision 0.5.5 is replaced and the glossary gains "Contract board" and "Deadhead". - **Error code:** the CR's `location_undiscovered` is the existing `location-undiscovered`. + +## Step 10B.1 + +- **The board generator's input is `location`** (the freight market's location). Sim never knew about bays for boards; the old field name `origin` was the only "bay-board" naming, and comments now say "location board". Generation rules and seeds (`board::`) are unchanged. diff --git a/apps/server/src/game/boards.ts b/apps/server/src/game/boards.ts index 8389fc8..0598db1 100644 --- a/apps/server/src/game/boards.ts +++ b/apps/server/src/game/boards.ts @@ -148,7 +148,7 @@ export async function bayBoard( .where(eq(discoveredLocations.playerId, playerId)); return generateContractBoard( { - origin, + location: origin, discoveredIds: new Set(discovered.map((d) => d.locationId)), periodIndex, worldSeed: player!.worldSeed, diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 9f44534..b441a6a 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -134,7 +134,7 @@ Spec: `memory-bank/change-requests/CR-001-faster-start.md`. Decisions recorded a Spec: `memory-bank/change-requests/CR-002-open-markets.md`. New decisions recorded as plan §0.5.35–0.5.36. -- [ ] Step 10B.1 — Sim: boards at any location +- [x] **Step 10B.1 — Sim: boards at any location.** `BoardInput.location` (was `origin`) is the board's location; no bay notion in sim board generation. Moon board test added. - [ ] Step 10B.2 — Sim: deadhead validation - [ ] Step 10B.3 — Database: boards keyed by location - [ ] Step 10B.4 — Server: board endpoint for any discovered location diff --git a/packages/sim/src/board.ts b/packages/sim/src/board.ts index 4b7dfde..e3c7d12 100644 --- a/packages/sim/src/board.ts +++ b/packages/sim/src/board.ts @@ -42,7 +42,8 @@ export function standardCargo(content: SimContent): CargoType { } export interface BoardInput { - origin: Location; + /** The location whose freight market this is; every contract starts here (CR-002). */ + location: Location; discoveredIds: ReadonlySet; periodIndex: number; worldSeed: Seed; @@ -109,12 +110,13 @@ function pickTier(rng: Rng, cargoType: CargoType, content: SimContent): number { } /** - * Generates a bay's contract board for one period: `contractMassBands` decide how + * Generates a location's contract board for one period (any discovered + * location, bay or not, CR-002): `contractMassBands` decide how * many contracts fall in each ship-size mass band. Same inputs always give the * same board. */ export function generateContractBoard(input: BoardInput, content: SimContent): Contract[] { - const { origin, discoveredIds, periodIndex, worldSeed } = input; + const { location: origin, discoveredIds, periodIndex, worldSeed } = input; const { config } = content; const rng = createRng(deriveSeed(worldSeed, `board:${origin.id}:${periodIndex}`)); diff --git a/packages/sim/src/resolution.ts b/packages/sim/src/resolution.ts index df46120..120e2b1 100644 --- a/packages/sim/src/resolution.ts +++ b/packages/sim/src/resolution.ts @@ -40,7 +40,7 @@ export function resolveDispatch(input: ResolveInput, content: SimContent): Resol const { contract } = record; const base = { shipLocationId: record.destinationId }; if (contract === null) { - // Repositions earn nothing and only fly to bays, which are already discovered. + // Deadheads earn nothing and only fly to discovered locations. return { status: "resolved", resolution: { diff --git a/packages/sim/test/board.test.ts b/packages/sim/test/board.test.ts index 28e641b..333174a 100644 --- a/packages/sim/test/board.test.ts +++ b/packages/sim/test/board.test.ts @@ -11,7 +11,7 @@ import { const earth = content.location("earth")!; const discoveredIds = new Set(["earth", "moon"]); const periodIndex = boardPeriodIndex(Date.parse("2100-01-01T00:00:00Z"), content.config); -const input: BoardInput = { origin: earth, discoveredIds, periodIndex, worldSeed: 12345 }; +const input: BoardInput = { location: earth, discoveredIds, periodIndex, worldSeed: 12345 }; describe("contract board generation", () => { it("produces an identical board from the same inputs", () => { @@ -42,7 +42,10 @@ describe("contract board generation", () => { it("ties tier to skill: Standard is tier 0 with no skill, others 1–5 matching the skill level", () => { const moon = content.location("moon")!; for (let p = 0; p < 50; p++) { - for (const c of generateContractBoard({ ...input, origin: moon, periodIndex: p }, content)) { + for (const c of generateContractBoard( + { ...input, location: moon, periodIndex: p }, + content, + )) { if (c.cargoTypeId === "standard") { expect(c.tier).toBe(0); expect(c.requiredSkill).toBeNull(); @@ -106,6 +109,20 @@ describe("contract board generation", () => { }); }); +describe("location boards (CR-002)", () => { + it("generates a deterministic Moon board from the location alone, with no bay data", () => { + const moon = content.location("moon")!; + const moonInput: BoardInput = { location: moon, discoveredIds, periodIndex, worldSeed: 12345 }; + const board = generateContractBoard(moonInput, content); + expect(board).toHaveLength(content.config.contractBoardSize); + for (const c of board) { + expect(c.originId).toBe("moon"); + expect(c.destinationId).not.toBe("moon"); + } + expect(generateContractBoard(moonInput, content)).toEqual(board); + }); +}); + describe("required Long Haul level", () => { it("counts the cut-offs a distance exceeds", () => { expect(requiredLongHaulLevel(0.4, content.config)).toBe(0);