Step 16.4: engine: save migration and boards
CI / check (push) Has been cancelled Details

Older saves open with the three new places undiscovered and nothing
else changed (the save lists only discovered places, so no migration is
needed), Earth boards offer first contacts to Venus, and delivering one
discovers Venus and opens its board with Venus goods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-10 13:41:58 -04:00
parent d626989779
commit 50d23436aa
3 changed files with 105 additions and 1 deletions

View File

@ -1516,3 +1516,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
## 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.
## Step 16.4
- **No save migration for new locations**: CR-016 §9 asks for one that adds the three places as undiscovered, but the save (since CR-014) stores only discovered places, so there is nothing to add. Bumping the save version for no data change would only make older builds refuse newer saves for no reason; the test shows an older save opens correctly instead.

View File

@ -396,7 +396,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.
- [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
- [x] **Step 16.4 — Engine: save migration and boards.** No save migration and no save-version change: the save lists only discovered places, so a place that is in content but not in that list is undiscovered, and a save from before CR-016 opens with the three new places undiscovered and nothing else changed. Boards needed no change either: first contacts and fit slots already draw from undiscovered places in reach. Tests (`packages/engine/test/new-places.test.ts`): a pre-CR-016 save imported through the engine shows eight places with Venus, Io and Europa undiscovered, and its save version, discoveries and company are unchanged; 200 Earth boards for the deal's Clipper and veteran include first contacts to Venus; a Clipper delivering a first contact to Venus discovers it (the logbook says so) and Venus then has its own board, every contract on it a good made at Venus.
- [ ] Step 16.5 — Assets: sprites and belt rocks
- [ ] Step 16.6 — Map: belt haze
- [ ] Step 16.7 — Map: belt rocks

View File

@ -0,0 +1,100 @@
import { content } from "@deadhead/content";
import { boardPeriodIndex, generateContractBoard } from "@deadhead/sim";
import { describe, expect, it } from "vitest";
import { fullSave } from "../../schema/test/save-fixture";
import { deliveredTank } from "../src/game/state";
import { encodeSave } from "../src/index";
import {
edit,
getState,
putBoard,
readSave,
setCredits,
signUpOwner,
TEST_NOW,
testContract,
useTestApp,
} from "./support";
const testApp = useTestApp();
const NEW_PLACES = ["venus-cloud-city", "io-sulfur-works", "europa-ice-station"];
describe("Venus, Io and Europa in the engine (Step 16.4)", () => {
it("opens a save from before CR-016 with eight places, the new three undiscovered", async () => {
const { app } = testApp();
const before = fullSave();
const { state } = await app.call("importSave", { code: await encodeSave(before) });
expect(state.locations).toHaveLength(8);
for (const id of NEW_PLACES) {
expect(
state.locations.find((l) => l.id === id),
id,
).toEqual({ id, discovered: false });
}
// Nothing else about what the player knows changed: no save data is needed for them.
const after = await readSave(app);
expect(after.saveVersion).toBe(before.saveVersion);
expect(after.discoveredLocations).toEqual(before.discoveredLocations);
expect(after.company).toEqual(before.company);
});
it("offers first contacts to Venus on Earth boards over time", () => {
const start = boardPeriodIndex(TEST_NOW, content.config);
const toVenus = Array.from({ length: 200 }, (_, k) =>
generateContractBoard(
{
location: content.location("earth")!,
discoveredIds: new Set(["earth", "moon"]),
periodIndex: start + k,
worldSeed: 4242,
// The deal's Clipper and veteran, after Long Haul training.
fleet: [{ modelId: "clipper", skills: { "expensive-cargo": 1, "long-haul": 2 } }],
},
content,
),
)
.flat()
.filter((c) => c.firstContact && c.destinationId === "venus-cloud-city");
expect(toVenus.length).toBeGreaterThan(0);
});
it("discovers Venus on delivery and opens its board with Venus goods", async () => {
const { app, clock } = testApp();
const owner = await signUpOwner(app);
await setCredits(app, 10_000);
// A Clipper with a full tank: Venus is a Clipper leg from Earth (CR-016 §3).
await edit(app, (save) => {
Object.assign(
save.ships.find((s) => s.id === owner.shipId)!,
{
modelId: "clipper",
...deliveredTank("clipper", "earth"),
},
);
});
const contract = testContract({
destinationId: "venus-cloud-city",
firstContact: true,
payout: 40_000,
});
await putBoard(app, [contract]);
const { dispatch } = await app.call("createDispatch", {
kind: "contract",
contractId: contract.id,
shipId: owner.shipId,
pilotId: owner.avatarId,
});
clock.now = dispatch.arrivalAt;
const state = await getState(app);
expect(state.locations.find((l) => l.id === "venus-cloud-city")!.discovered).toBe(true);
expect(state.summaries[0]!.discoveredLocationId).toBe("venus-cloud-city");
const { contracts } = await app.call("getContractBoard", { locationId: "venus-cloud-city" });
expect(contracts.length).toBeGreaterThan(0);
for (const c of contracts) {
expect(c.originId).toBe("venus-cloud-city");
expect(content.good(c.goodId)!.producedAt, c.goodId).toContain("venus-cloud-city");
}
});
});