Step 10F.2: board preview and open slot config

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 12:15:39 -04:00
parent bd2d2df14d
commit dc24bdeb7a
4 changed files with 28 additions and 0 deletions

View File

@ -194,3 +194,4 @@ Spec: `memory-bank/change-requests/CR-005-ui-overhaul.md`. New decisions recorde
Spec: `memory-bank/change-requests/CR-006-boards-that-fit.md`. New decisions recorded as plan §0.5.52–0.5.53. Spec: `memory-bank/change-requests/CR-006-boards-that-fit.md`. New decisions recorded as plan §0.5.52–0.5.53.
- [x] **Step 10F.1 — Client: weight warning.** `carryLimit(state, locationId)` (`LocationBoards.tsx`): the largest capacity among the player's ships at the location, else the whole fleet, else null. Contract cards show the mass in rose and a rose "Too heavy" pill when it is over that limit. Test: `weight-warning.test.tsx`. - [x] **Step 10F.1 — Client: weight warning.** `carryLimit(state, locationId)` (`LocationBoards.tsx`): the largest capacity among the player's ships at the location, else the whole fleet, else null. Contract cards show the mass in rose and a rose "Too heavy" pill when it is over that limit. Test: `weight-warning.test.tsx`.
- [x] **Step 10F.2 — Content: board config.** GameConfig gains `boardPreviewSlots` (1) and `boardOpenSlotsPerBand` (1), non-negative integers. Test: `schemas.test.ts` (missing-field coverage plus a negative/fractional rejection test).

View File

@ -39,6 +39,10 @@ export const gameConfig: GameConfig = {
{ min: 21, max: 100, count: 3 }, { min: 21, max: 100, count: 3 },
{ min: 101, max: 400, count: 2 }, { min: 101, max: 400, count: 2 },
], ],
// Boards that fit the fleet (CR-006): a band no ship can carry keeps one
// preview job; flyable bands keep one open job, the rest fit a ship and pilot.
boardPreviewSlots: 1,
boardOpenSlotsPerBand: 1,
// Mars at its closest needs no Long Haul; Jupiter at its farthest needs level 5. // Mars at its closest needs no Long Haul; Jupiter at its farthest needs level 5.
longHaulCutoffsMillionKm: [100, 250, 400, 600, 800], longHaulCutoffsMillionKm: [100, 250, 400, 600, 800],
xpPerDelivery: { base: 10, perMillionKm: 0.2, perCargoTier: 5 }, xpPerDelivery: { base: 10, perMillionKm: 0.2, perCargoTier: 5 },

View File

@ -158,6 +158,13 @@ export const gameConfigSchema = z
.refine((band) => band.min <= band.max, { message: "min must not exceed max" }), .refine((band) => band.min <= band.max, { message: "min must not exceed max" }),
) )
.min(1), .min(1),
/**
* Contracts kept in a mass band the player's fleet can't carry: a preview
* of bigger ships' work (decision 0.5.52). The rest move to flyable bands.
*/
boardPreviewSlots: nonNegativeInt,
/** Contracts per flyable band left open (skill-gated, first contact) rather than fit (0.5.52). */
boardOpenSlotsPerBand: nonNegativeInt,
/** One cut-off per Long Haul level (decision 0.5.7). */ /** One cut-off per Long Haul level (decision 0.5.7). */
longHaulCutoffsMillionKm: increasingList, longHaulCutoffsMillionKm: increasingList,
xpPerDelivery: z.object({ xpPerDelivery: z.object({

View File

@ -72,6 +72,8 @@ const validGameConfig = {
{ min: 1, max: 20, count: 4 }, { min: 1, max: 20, count: 4 },
{ min: 21, max: 100, count: 4 }, { min: 21, max: 100, count: 4 },
], ],
boardPreviewSlots: 1,
boardOpenSlotsPerBand: 1,
longHaulCutoffsMillionKm: [100, 200, 300, 400, 500], longHaulCutoffsMillionKm: [100, 200, 300, 400, 500],
xpPerDelivery: { base: 10, perMillionKm: 0.1, perCargoTier: 5 }, xpPerDelivery: { base: 10, perMillionKm: 0.1, perCargoTier: 5 },
pilotLevelXpThresholds: [100, 250, 500], pilotLevelXpThresholds: [100, 250, 500],
@ -151,6 +153,20 @@ describe("GameConfig refinements", () => {
expect(gameConfigSchema.safeParse(config).success).toBe(false); expect(gameConfigSchema.safeParse(config).success).toBe(false);
}); });
it("rejects negative or fractional board slot counts (CR-006)", () => {
for (const bad of [-1, 0.5]) {
expect(
gameConfigSchema.safeParse({ ...validGameConfig, boardPreviewSlots: bad }).success,
).toBe(false);
expect(
gameConfigSchema.safeParse({ ...validGameConfig, boardOpenSlotsPerBand: bad }).success,
).toBe(false);
}
expect(
gameConfigSchema.safeParse({ ...validGameConfig, boardOpenSlotsPerBand: 0 }).success,
).toBe(true);
});
it("rejects Long Haul cut-offs that are not increasing", () => { it("rejects Long Haul cut-offs that are not increasing", () => {
const config = { ...validGameConfig, longHaulCutoffsMillionKm: [100, 100, 300] }; const config = { ...validGameConfig, longHaulCutoffsMillionKm: [100, 100, 300] };
expect(gameConfigSchema.safeParse(config).success).toBe(false); expect(gameConfigSchema.safeParse(config).success).toBe(false);