Step 10B.1: generate contract boards for any location

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-03 14:41:56 -04:00
parent ea37ce8ced
commit dff2c576cf
6 changed files with 31 additions and 8 deletions

View File

@ -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:<location>:<period>`) are unchanged.

View File

@ -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,

View File

@ -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

View File

@ -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<string>;
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}`));

View File

@ -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: {

View File

@ -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);