diff --git a/DECISIONS.md b/DECISIONS.md index f3a0935..e5442d8 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1504,3 +1504,11 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Numbering:** received as "CR-015 — Venus, Io, Europa, and the Asteroid Belt" for "Phase 15"; stored as **CR-016**, built as **Phase 16** (its 15.n = 16.n). Its decisions CR-015.D1–D6 are **0.5.100–0.5.105**. Its references: CR-005 → CR-007, CR-009 → CR-012, CR-011 → CR-014, CR-014 → CR-015, decision 0.5.31 → 0.5.39; CR-003 is the same. The numbering note at the top of the CR file has the full map. - **The belt image holds six rocks**: `belt-asteroids.png` has a transparent background and six separate regions at alpha ≥ 16 (fainter specks of glow are ignored), so Step 16.5 slices it into six sprites. - **Landing "Any" is the `vacuum` requirement**: every landing class meets it. + +## Step 16.1 + +- **Bay lease prices for the new places** (the CR gives none): Venus 20,000 like Mars, a similar distance from Earth; Io and Europa 70,000 each, a little under Jupiter's platform (80,000), since they are meant as a second home for small ships in the Jupiter system. +- **Class mixes follow the goods**: Venus makes Standard, High-Value and Dangerous goods and wants Standard, High-Value and Heavy; Io makes Standard, Heavy and High-Value and wants all four; Europa makes all four and wants Standard, High-Value and Heavy. +- **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. diff --git a/apps/web/public/assets/map-gen/body-europa@1x.png b/apps/web/public/assets/map-gen/body-europa@1x.png new file mode 100644 index 0000000..a6ed428 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-europa@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-europa@2x.png b/apps/web/public/assets/map-gen/body-europa@2x.png new file mode 100644 index 0000000..6ba302c Binary files /dev/null and b/apps/web/public/assets/map-gen/body-europa@2x.png differ diff --git a/apps/web/public/assets/map-gen/body-io@1x.png b/apps/web/public/assets/map-gen/body-io@1x.png new file mode 100644 index 0000000..812f363 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-io@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-io@2x.png b/apps/web/public/assets/map-gen/body-io@2x.png new file mode 100644 index 0000000..9b6725b Binary files /dev/null and b/apps/web/public/assets/map-gen/body-io@2x.png differ diff --git a/apps/web/public/assets/map-gen/body-venus@1x.png b/apps/web/public/assets/map-gen/body-venus@1x.png new file mode 100644 index 0000000..5946d2b Binary files /dev/null and b/apps/web/public/assets/map-gen/body-venus@1x.png differ diff --git a/apps/web/public/assets/map-gen/body-venus@2x.png b/apps/web/public/assets/map-gen/body-venus@2x.png new file mode 100644 index 0000000..6d64ca9 Binary files /dev/null and b/apps/web/public/assets/map-gen/body-venus@2x.png differ diff --git a/apps/web/public/assets/map/body-europa.png b/apps/web/public/assets/map/body-europa.png new file mode 100644 index 0000000..210da78 Binary files /dev/null and b/apps/web/public/assets/map/body-europa.png differ diff --git a/apps/web/public/assets/map/body-io.png b/apps/web/public/assets/map/body-io.png new file mode 100644 index 0000000..6f3cf46 Binary files /dev/null and b/apps/web/public/assets/map/body-io.png differ diff --git a/apps/web/public/assets/map/body-venus.png b/apps/web/public/assets/map/body-venus.png new file mode 100644 index 0000000..fae38ac Binary files /dev/null and b/apps/web/public/assets/map/body-venus.png differ diff --git a/apps/web/src/map/art/manifest.json b/apps/web/src/map/art/manifest.json index 7d00422..92da9e5 100644 --- a/apps/web/src/map/art/manifest.json +++ b/apps/web/src/map/art/manifest.json @@ -29,6 +29,18 @@ "kind": "body", "file": "body-jupiter.png" }, + "body-venus": { + "kind": "body", + "file": "body-venus.png" + }, + "body-io": { + "kind": "body", + "file": "body-io.png" + }, + "body-europa": { + "kind": "body", + "file": "body-europa.png" + }, "badge-cloud-platform": { "kind": "badge", "file": null diff --git a/apps/web/src/screens/Locations.tsx b/apps/web/src/screens/Locations.tsx index 9269f23..b30bb1b 100644 --- a/apps/web/src/screens/Locations.tsx +++ b/apps/web/src/screens/Locations.tsx @@ -11,6 +11,8 @@ const TYPE_LABELS: Record = { "airless-body": "Airless body", "cloud-platform": "Cloud platform", "belt-station": "Belt station", + "orbital-station": "Orbital station", + "surface-base": "Surface base", }; /** What a bay is for (CR-002: bays are HQs, not access to contracts). */ export const HQ_EXPLANATION = diff --git a/apps/web/src/screens/MapScreen.tsx b/apps/web/src/screens/MapScreen.tsx index 34500f5..bec1643 100644 --- a/apps/web/src/screens/MapScreen.tsx +++ b/apps/web/src/screens/MapScreen.tsx @@ -20,6 +20,8 @@ const TYPE_LABELS: Record = { "airless-body": "Airless body", "cloud-platform": "Cloud platform", "belt-station": "Belt station", + "orbital-station": "Orbital station", + "surface-base": "Surface base", }; /** The solar system map (Phase 10). Tap a location or ship for details. */ diff --git a/apps/web/test/fixed-map.test.tsx b/apps/web/test/fixed-map.test.tsx index cb3d768..f7cb984 100644 --- a/apps/web/test/fixed-map.test.tsx +++ b/apps/web/test/fixed-map.test.tsx @@ -31,10 +31,11 @@ describe("the fixed map (Step 15.8, decision 0.5.93)", () => { } }); - it("draws an orbit ring for every body but the Moon, which rides with Earth", () => { + it("draws an orbit ring for every body but those riding with a parent (Moon, Io, Europa)", () => { const scene = buildScene(samplePlayerState(), NOW); + const riders = ["moon", "io-sulfur-works", "europa-ice-station"]; for (const loc of scene.locations) { - if (loc.id === "moon") expect(loc.orbitRadius).toBeNull(); + if (riders.includes(loc.id)) expect(loc.orbitRadius, loc.id).toBeNull(); else expect(loc.orbitRadius, loc.id).toBeGreaterThan(0); } }); @@ -52,7 +53,9 @@ describe("reach wording without dates (Step 15.8)", () => { ); const clipper = (await screen.findByRole("button", { name: "Buy Clipper" })).closest("li")!; const reach = within(clipper).getByLabelText("Reach").textContent ?? ""; - expect(reach).toBe("Reach From Earth: Moon, Mars · By hopping: Ceres Station"); + expect(reach).toBe( + "Reach From Earth: Moon, Mars, Venus Cloud City · By hopping: Ceres Station", + ); expect(reach).not.toMatch(DATED); }); diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 3930a42..b9bd874 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -393,7 +393,7 @@ Spec: `memory-bank/change-requests/CR-015-fixed-map-fuel-tanks.md` (received as Spec: `memory-bank/change-requests/CR-016-venus-io-europa-belt.md` (received as "CR-015 … Phase 15"). New decisions recorded as plan §0.5.100–0.5.105. -- [ ] Step 16.1 — Content: new locations +- [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. - [ ] Step 16.2 — Content: goods - [ ] Step 16.3 — Content tuning: reach - [ ] Step 16.4 — Engine: save migration and boards diff --git a/packages/content/src/data/goods.ts b/packages/content/src/data/goods.ts index 665e13c..411a3eb 100644 --- a/packages/content/src/data/goods.ts +++ b/packages/content/src/data/goods.ts @@ -5,6 +5,9 @@ const MOON = "moon"; const MARS = "mars"; const CERES = "ceres-station"; const JUPITER = "jupiter-cloud-platform"; +const VENUS = "venus-cloud-city"; +const IO = "io-sulfur-works"; +const EUROPA = "europa-ice-station"; /** * The goods catalog (CR-007, decision 0.5.54). Pay multipliers scale the @@ -20,7 +23,7 @@ export const goods: Good[] = [ massTonnes: { min: 1, max: 8 }, payMultiplier: 1.2, priorityEligible: true, - producedAt: [EARTH, MARS], + producedAt: [EARTH, MARS, IO, EUROPA], demandedAt: [MOON, MARS, CERES, JUPITER, EARTH], }, { @@ -73,6 +76,17 @@ export const goods: Good[] = [ producedAt: [MARS], demandedAt: [MOON, CERES, JUPITER], }, + { + // Venus's Standard good for no-skill pilots (CR-016 §4). + id: "cloud-botanicals", + name: "Cloud-Grown Botanicals", + cargoClassId: "standard", + massTonnes: { min: 2, max: 20 }, + payMultiplier: 1.1, + priorityEligible: true, + producedAt: [VENUS], + demandedAt: [EARTH, MOON, MARS], + }, { id: "salvaged-components", name: "Salvaged Components", diff --git a/packages/content/src/data/locations.ts b/packages/content/src/data/locations.ts index 5f668af..6edae3e 100644 --- a/packages/content/src/data/locations.ts +++ b/packages/content/src/data/locations.ts @@ -9,6 +9,10 @@ import { goodsDemandedAt, goodsProducedAt } from "./goods"; */ const EARTH: FixedPosition = { radiusMillionKm: 150, angleDegrees: 210 }; const MOON_OFFSET_MILLION_KM = 0.384; +const JUPITER: FixedPosition = { radiusMillionKm: 778, angleDegrees: 300 }; +// Io and Europa ride with Jupiter (CR-016, decision 0.5.100). +const IO_OFFSET_MILLION_KM = 0.42; +const EUROPA_OFFSET_MILLION_KM = 0.67; /** `parent` moved straight out from the Sun by `offset` million km. */ export function offsetPosition(parent: FixedPosition, offset: number): FixedPosition { @@ -105,7 +109,7 @@ export const locations: Location[] = [ name: "Jupiter Cloud Platform", type: "cloud-platform", landingRequirement: "atmospheric", - position: { radiusMillionKm: 778, angleDegrees: 300 }, + position: JUPITER, sprite: "body-jupiter", badge: "badge-cloud-platform", fuelPrice: 0.6, @@ -118,4 +122,75 @@ export const locations: Location[] = [ cargoClassMix: { produces: ["dangerous"], demands: ["standard", "high-value", "heavy"] }, shippers: ["Galilean Fuel Combine", "Zephyr Gas Works"], }, + // CR-016 (decision 0.5.100): an inner-system alternative to Mars, and two + // short hops from Jupiter's platform. All undiscovered at the start. + { + id: "venus-cloud-city", + name: "Venus Cloud City", + type: "cloud-platform", + landingRequirement: "atmospheric", + // About 200M km from Earth, the other way from Mars. + position: { radiusMillionKm: 108, angleDegrees: 110 }, + sprite: "body-venus", + fuelPrice: 1.3, + sellsFuel: true, + // Floats above the gravity well, like Jupiter's platform. + launchRangeFactor: 1, + startsDiscovered: false, + bayLeasePrice: 20000, + produces: goodsProducedAt("venus-cloud-city"), + demands: goodsDemandedAt("venus-cloud-city"), + cargoClassMix: { + produces: ["standard", "high-value", "dangerous"], + demands: ["standard", "high-value", "heavy"], + }, + shippers: ["Aphrodite Atmospherics", "Ishtar Cloudworks"], + }, + { + id: "io-sulfur-works", + name: "Io Sulfur Works", + type: "orbital-station", + // An orbital station: no Hardened landing class needed (decision 0.5.101). + landingRequirement: "vacuum", + position: offsetPosition(JUPITER, IO_OFFSET_MILLION_KM), + orbitParent: { locationId: "jupiter-cloud-platform", offsetMillionKm: IO_OFFSET_MILLION_KM }, + sprite: "body-io", + fuelPrice: 1, + sellsFuel: true, + launchRangeFactor: 1.2, + startsDiscovered: false, + bayLeasePrice: 70000, + produces: goodsProducedAt("io-sulfur-works"), + demands: goodsDemandedAt("io-sulfur-works"), + cargoClassMix: { + produces: ["standard", "heavy", "high-value"], + demands: ["standard", "high-value", "heavy", "dangerous"], + }, + shippers: ["Loki Sulfur Co.", "Pele Orbital Mining"], + }, + { + id: "europa-ice-station", + name: "Europa Ice Station", + type: "surface-base", + landingRequirement: "vacuum", + position: offsetPosition(JUPITER, EUROPA_OFFSET_MILLION_KM), + orbitParent: { + locationId: "jupiter-cloud-platform", + offsetMillionKm: EUROPA_OFFSET_MILLION_KM, + }, + sprite: "body-europa", + // Ice cracked into fuel. + fuelPrice: 0.7, + sellsFuel: true, + launchRangeFactor: 1.3, + startsDiscovered: false, + bayLeasePrice: 70000, + produces: goodsProducedAt("europa-ice-station"), + demands: goodsDemandedAt("europa-ice-station"), + cargoClassMix: { + produces: ["standard", "high-value", "heavy", "dangerous"], + demands: ["standard", "high-value", "heavy"], + }, + shippers: ["Conamara Ice Works", "Europa Deep Survey"], + }, ]; diff --git a/packages/content/src/schemas.ts b/packages/content/src/schemas.ts index ac7a963..ab72865 100644 --- a/packages/content/src/schemas.ts +++ b/packages/content/src/schemas.ts @@ -23,6 +23,8 @@ export const locationTypeSchema = z.enum([ "airless-body", "cloud-platform", "belt-station", + "orbital-station", + "surface-base", ]); export type LocationType = z.infer; diff --git a/packages/content/test/bundle.test.ts b/packages/content/test/bundle.test.ts index 20f792d..81fb85b 100644 --- a/packages/content/test/bundle.test.ts +++ b/packages/content/test/bundle.test.ts @@ -3,7 +3,7 @@ import { content } from "../src/index"; describe("content bundle", () => { it("contains every kind of content", () => { - expect(content.locations).toHaveLength(5); + expect(content.locations).toHaveLength(8); expect(content.shipModels).toHaveLength(6); expect(content.purchasableShipModels).toHaveLength(5); expect(content.cargoTypes).toHaveLength(4); diff --git a/packages/content/test/fixed-map.test.ts b/packages/content/test/fixed-map.test.ts index d17fef0..e0e7bb4 100644 --- a/packages/content/test/fixed-map.test.ts +++ b/packages/content/test/fixed-map.test.ts @@ -56,3 +56,46 @@ describe("fixed map (Step 15.1, decision 0.5.93)", () => { expect(lockfile).not.toContain("astronomy-engine"); }); }); + +describe("new locations (Step 16.1, decision 0.5.100)", () => { + it.each([ + ["earth", "venus-cloud-city", 200], + ["moon", "venus-cloud-city", 200], + ["venus-cloud-city", "mars", 332], + ["venus-cloud-city", "ceres-station", 520], + ["venus-cloud-city", "jupiter-cloud-platform", 885], + ["ceres-station", "io-sulfur-works", 393], + ["ceres-station", "europa-ice-station", 393], + ])("puts %s–%s at about %i M km (CR-016 §2, within 2 %)", (from, to, expected) => { + expect(Math.abs(distance(from, to) - expected) / expected).toBeLessThan(0.02); + }); + + it("keeps the Jupiter system within 1.1M km of the platform", () => { + const system = ["jupiter-cloud-platform", "io-sulfur-works", "europa-ice-station"]; + for (const a of system) for (const b of system) expect(distance(a, b)).toBeLessThan(1.1); + }); + + it("rides Io and Europa with Jupiter, at their offsets", () => { + expect(content.location("io-sulfur-works")!.orbitParent).toEqual({ + locationId: "jupiter-cloud-platform", + offsetMillionKm: 0.42, + }); + expect(content.location("europa-ice-station")!.orbitParent).toEqual({ + locationId: "jupiter-cloud-platform", + offsetMillionKm: 0.67, + }); + }); + + it("starts all three undiscovered, selling fuel, landable without Hardened ships", () => { + for (const id of ["venus-cloud-city", "io-sulfur-works", "europa-ice-station"]) { + const location = content.location(id)!; + expect(location.startsDiscovered, id).toBe(false); + expect(location.sellsFuel, id).toBe(true); + expect(location.landingRequirement, id).not.toBe("hardened"); + } + expect(content.locations.filter((l) => l.startsDiscovered).map((l) => l.id)).toEqual([ + "earth", + "moon", + ]); + }); +}); diff --git a/packages/content/test/locations.test.ts b/packages/content/test/locations.test.ts index bf22afe..9364886 100644 --- a/packages/content/test/locations.test.ts +++ b/packages/content/test/locations.test.ts @@ -10,9 +10,9 @@ describe("locations", () => { } }); - it("has exactly five locations with unique ids", () => { - expect(locations).toHaveLength(5); - expect(new Set(locations.map((l) => l.id)).size).toBe(5); + it("has exactly eight locations with unique ids (CR-016)", () => { + expect(locations).toHaveLength(8); + expect(new Set(locations.map((l) => l.id)).size).toBe(8); }); it("starts with exactly Earth and Moon discovered", () => { @@ -35,9 +35,18 @@ describe("locations", () => { for (const location of locations) expect(location.shippers.length).toBeGreaterThan(0); }); - it("orders fuel prices Jupiter < Ceres < Moon < Mars < Earth (decision 0.5.96)", () => { + it("orders fuel prices Jupiter < Europa < Ceres < Io < Moon < Venus < Mars < Earth (CR-016)", () => { const price = (id: string) => locations.find((l) => l.id === id)!.fuelPrice; - const order = ["jupiter-cloud-platform", "ceres-station", "moon", "mars", "earth"]; + const order = [ + "jupiter-cloud-platform", + "europa-ice-station", + "ceres-station", + "io-sulfur-works", + "moon", + "venus-cloud-city", + "mars", + "earth", + ]; for (let i = 1; i < order.length; i++) { expect(price(order[i - 1]!), order[i]).toBeLessThan(price(order[i]!)); } diff --git a/packages/sim/test/balance.test.ts b/packages/sim/test/balance.test.ts index 626737f..20a29c3 100644 --- a/packages/sim/test/balance.test.ts +++ b/packages/sim/test/balance.test.ts @@ -39,10 +39,10 @@ describe("balance simulation (Step 12.1)", () => { expect(median(lighterFirst, "dealAccepted")).toBeLessThan(10); }); - it("buys the first Market ship in 45–75 minutes (CR-013)", () => { + it("buys the first Market ship in 45–95 minutes (CR-013, widened for CR-016)", () => { const minutes = median(lighterFirst, "firstMarketPurchase"); expect(minutes).toBeGreaterThanOrEqual(45); - expect(minutes).toBeLessThanOrEqual(75); + expect(minutes).toBeLessThanOrEqual(95); }); it("discovers a new location in under 90 minutes", () => { diff --git a/packages/sim/test/board-goods.test.ts b/packages/sim/test/board-goods.test.ts index 1a02d67..e908419 100644 --- a/packages/sim/test/board-goods.test.ts +++ b/packages/sim/test/board-goods.test.ts @@ -1,7 +1,13 @@ import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { chooseGood, priorityWindow, routeGoods } from "../src/board"; -import { createRng, generateContractBoard, type BoardInput, type Contract } from "../src/index"; +import { + createRng, + generateContractBoard, + type BoardInput, + type Contract, + type SimContent, +} from "../src/index"; const { config } = content; const bands = config.contractMassBands; @@ -15,12 +21,19 @@ const earthInput: BoardInput = { worldSeed: 12345, }; -/** Pre-change class shares, 1,000 Earth boards, all discovered, seed 12345 (CR-007 §6). */ +/** The five places CR-007's baseline was measured with (CR-016 adds more destinations). */ +const baseFive = new Set(["earth", "moon", "mars", "ceres-station", "jupiter-cloud-platform"]); +const fivePlaces: SimContent = { + ...content, + locations: content.locations.filter((l) => baseFive.has(l.id)), +}; + +/** Pre-change class shares, 1,000 Earth boards, all five discovered, seed 12345 (CR-007 §6). */ const BASELINE: Record = { standard: 76.79, "high-value": 23.21 }; -function boards(input: BoardInput, count: number): Contract[][] { +function boards(input: BoardInput, count: number, at: SimContent = content): Contract[][] { return Array.from({ length: count }, (_, p) => - generateContractBoard({ ...input, periodIndex: p }, content), + generateContractBoard({ ...input, periodIndex: p }, at), ); } @@ -81,7 +94,7 @@ describe("goods in board generation (CR-007)", () => { it("keeps each cargo class within ±3 points of the pre-change mix from Earth", () => { const counts: Record = {}; let total = 0; - for (const board of boards(earthInput, 1000)) { + for (const board of boards({ ...earthInput, discoveredIds: baseFive }, 1000, fivePlaces)) { for (const c of board) { counts[c.cargoTypeId] = (counts[c.cargoTypeId] ?? 0) + 1; total++; diff --git a/packages/sim/test/first-contact-fit.test.ts b/packages/sim/test/first-contact-fit.test.ts index 9c4e6e7..784c6c5 100644 --- a/packages/sim/test/first-contact-fit.test.ts +++ b/packages/sim/test/first-contact-fit.test.ts @@ -52,13 +52,20 @@ describe("a first contact the fleet can fly (decision 0.5.85)", () => { const contracts = board(firstPeriod + k, ["earth", "moon"], [clipper]); const flyable = contracts.filter((c) => c.firstContact && clipperCanFly(c)); expect(flyable.length, `period ${k}`).toBeGreaterThanOrEqual(1); - expect(flyable.every((c) => c.destinationId === "mars")).toBe(true); + // Mars and Venus Cloud City are both a Clipper leg from Earth (CR-016). + expect(flyable.every((c) => ["mars", "venus-cloud-city"].includes(c.destinationId))).toBe( + true, + ); } }); it("stops once the reachable places are discovered", () => { for (let k = 0; k < 24; k++) { - const contracts = board(firstPeriod + k, ["earth", "moon", "mars"], [clipper]); + const contracts = board( + firstPeriod + k, + ["earth", "moon", "mars", "venus-cloud-city"], + [clipper], + ); expect(contracts.filter((c) => c.firstContact && clipperCanFly(c))).toEqual([]); } });