Step 20.1: content and config
CI / check (push) Has been cancelled Details

GameConfig.independentPilots: Dispatch Office price, first-look window,
turnaround, empty periods before heading home, catch-up cap and spending
floor (CR-020).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-10 23:01:55 -04:00
parent f7bbc6c1ea
commit 83cb2ae306
6 changed files with 83 additions and 4 deletions

View File

@ -1669,3 +1669,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Numbering:** received as "CR-019 — Independent Pilots" for "Phase 19"; stored as **CR-020**, built as **Phase 20** (its 19.n = 20.n). Its decisions CR-019.D1–D9 are **0.5.118–0.5.126**. Its references: CR-005 → CR-007, CR-007 → CR-010, CR-011 → CR-014 (its 10K.6 → Step 14.6), CR-014 → CR-015, CR-016 → CR-017, CR-017 → CR-018, CR-018 → CR-019; CR-001 and CR-004 are unchanged.
- **Plan 0.3:** automation/standing routes is struck from the deferred list; standing orders, pilot traits, decision events and a hiring market stay deferred.
## Step 20.1
- **The settings live in one `independentPilots` group** in GameConfig.
- **The Dispatch Office costs exactly a Lighter (36,000 cr)**, the CR's "start around the price of a Lighter", until Step 20.8's balance pass says otherwise.

View File

@ -462,7 +462,7 @@ Spec: `memory-bank/change-requests/CR-020-bridge-dashboard.md`. New decisions re
Spec: `memory-bank/change-requests/CR-020-independent-pilots.md` (received as "CR-019 … Phase 19"). New decisions recorded as plan §0.5.118–0.5.126.
- [ ] Step 20.1 — Content and config
- [x] **Step 20.1 — Content and config.** `config.independentPilots`: `dispatchOfficePrice` 36,000 (a Lighter), `firstLookRealMinutes` 3, `turnaroundGameMinutes` 15 (15 real seconds at ×60), `emptyPeriodsBeforeHome` 3, `maxCatchUpRealDays` 7 and `defaultSpendingFloor` 500. All must be positive, and the first-look window must be shorter than the board period. Two stale quarter-tank comments from CR-015 are gone. Tests (`packages/content/test/config.test.ts`): the defaults, with the Dispatch Office at the Lighter's price; zero and negative values rejected; a first-look window as long as the board period rejected.
- [ ] Step 20.2 — Sim: eligibility and job choice
- [ ] Step 20.3 — Engine: Dispatch Office, modes, orders
- [ ] Step 20.4 — Engine: event-queue catch-up

View File

@ -25,8 +25,7 @@ export const gameConfig: GameConfig = {
loanRepaymentShare: 0.25,
// The deal (CR-013): Halvorsen's Courier for the avatar, his Clipper for the
// veteran, and the Earth bay. The loan is computed from their prices.
// The ships come with a quarter tank (CR-015, user decision): the Clipper earns
// locally before the player can fuel a Mars run, as before fuel tanks.
// The deal's ships come with full tanks (CR-018, decision 0.5.111).
deal: {
avatarShipModelId: "courier",
veteranShipModelId: "clipper",
@ -87,4 +86,15 @@ export const gameConfig: GameConfig = {
pilotHireCost: 2000,
// A quarter of each contract's profit (payout − fuel) goes to a hired pilot (CR-010).
pilotProfitShare: 0.25,
// Independent pilots (CR-020): the Dispatch Office costs about a Lighter; the
// player has the first 3 minutes of every 10-minute board; pilots turn round
// in 15 game minutes (15 real seconds) and head home after 3 empty boards.
independentPilots: {
dispatchOfficePrice: 36_000,
firstLookRealMinutes: 3,
turnaroundGameMinutes: 15,
emptyPeriodsBeforeHome: 3,
maxCatchUpRealDays: 7,
defaultSpendingFloor: 500,
},
};

View File

@ -242,7 +242,6 @@ export const gameConfigSchema = z
avatarShipModelId: z.string().min(1),
veteranShipModelId: z.string().min(1),
bayLocationId: z.string().min(1),
/** How full the deal's ships' tanks are (CR-015, user decision): Halvorsen's leftovers. */
}),
contractBoardPeriodRealMinutes: z.number().positive(),
/** Past board periods whose boards and accepted contracts are kept (Step 7.3). */
@ -330,12 +329,35 @@ export const gameConfigSchema = z
pilotHireCost: nonNegativeInt,
/** Hired pilots' share of profit (payout − fuel) per contract delivery (decision 0.5.12, CR-010). */
pilotProfitShare: fraction,
/** Independent pilots (CR-020, decisions 0.5.119–0.5.125). */
independentPilots: z.object({
/** Credits: the one-time Dispatch Office (0.5.119). */
dispatchOfficePrice: positiveInt,
/** Real minutes into each board period that its jobs are the player's alone (0.5.122). */
firstLookRealMinutes: z.number().positive(),
/** Game minutes from a pilot becoming able to act to their next job (CR-020 §2). */
turnaroundGameMinutes: z.number().positive(),
/** Empty board periods in a row away from home before heading home (0.5.124). */
emptyPeriodsBeforeHome: positiveInt,
/** Real days of absence catch-up replays at most (0.5.125). */
maxCatchUpRealDays: z.number().positive(),
/** Credits pilots never spend the company below, by default (0.5.123). */
defaultSpendingFloor: positiveInt,
}),
})
.refine(
(config) =>
config.contractMassBands.reduce((sum, band) => sum + band.count, 0) ===
config.contractBoardSize,
{ message: "Mass band counts must add up to contractBoardSize", path: ["contractMassBands"] },
)
.refine(
(config) =>
config.independentPilots.firstLookRealMinutes < config.contractBoardPeriodRealMinutes,
{
message: "The first-look window must be shorter than the board period",
path: ["independentPilots", "firstLookRealMinutes"],
},
);
export type GameConfig = z.infer<typeof gameConfigSchema>;

View File

@ -96,3 +96,37 @@ describe("tutorial settings (Step 17.1, decisions 0.5.106, 0.5.107)", () => {
);
});
});
describe("independent pilots config (Step 20.1, CR-020)", () => {
const settings = gameConfig.independentPilots;
const withSettings = (change: Partial<typeof settings>) =>
gameConfigSchema.safeParse({ ...gameConfig, independentPilots: { ...settings, ...change } })
.success;
it("has every setting, all positive, at the CR's defaults", () => {
const lighter = shipModels.find((s) => s.id === "lighter")!;
expect(settings).toEqual({
dispatchOfficePrice: lighter.price,
firstLookRealMinutes: 3,
turnaroundGameMinutes: 15,
emptyPeriodsBeforeHome: 3,
maxCatchUpRealDays: 7,
defaultSpendingFloor: 500,
});
for (const value of Object.values(settings)) expect(value).toBeGreaterThan(0);
});
it("rejects zero or negative values", () => {
for (const key of Object.keys(settings) as (keyof typeof settings)[]) {
expect(withSettings({ [key]: 0 }), key).toBe(false);
expect(withSettings({ [key]: -1 }), key).toBe(false);
}
});
it("keeps the first-look window shorter than the board period", () => {
expect(settings.firstLookRealMinutes).toBeLessThan(gameConfig.contractBoardPeriodRealMinutes);
expect(withSettings({ firstLookRealMinutes: gameConfig.contractBoardPeriodRealMinutes })).toBe(
false,
);
});
});

View File

@ -104,6 +104,14 @@ const validGameConfig = {
skillPointsPerLevel: 1,
pilotHireCost: 1000,
pilotProfitShare: 0.25,
independentPilots: {
dispatchOfficePrice: 36_000,
firstLookRealMinutes: 3,
turnaroundGameMinutes: 15,
emptyPeriodsBeforeHome: 3,
maxCatchUpRealDays: 7,
defaultSpendingFloor: 500,
},
tutorialLoanerShipModelId: "company-courier",
tutorialRunsBeforeDeal: 5,
tutorialXpMultiplier: 2,