From 08015fe97c1a3c0f930578bc66769c9d17521208 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Thu, 15 Oct 2026 21:20:09 -0400 Subject: [PATCH] Step 24.4: engine: waking and Back to work Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 5 +++ memory-bank/architecture.md | 1 + memory-bank/progress.md | 2 +- packages/engine/src/game/commands.ts | 6 ++++ packages/engine/test/fleet-sleep.test.ts | 43 ++++++++++++++++++++++++ packages/schema/src/routes.ts | 9 +++++ packages/schema/test/routes.test.ts | 2 ++ 7 files changed, 67 insertions(+), 1 deletion(-) diff --git a/DECISIONS.md b/DECISIONS.md index f7acf0d..f900acb 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1826,3 +1826,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Sleep is one more limit on decisions in catch-up**, beside the 7-day cap: `decideLimit = min(until, decideUntil, sleepsAt − 1 ms)`. Nothing else in the queue changes, so arrivals, rest and shop ends resolve as before, and catch-up stays deterministic. Pilots due after the limit wait at `until`, the time of the command or loop pass that caught up, which is how a manual decision then wakes them from that moment (Step 24.4). - **"At or after the sleep time"**: a decision exactly at `sleepsAt` doesn't happen (hence the −1 ms). - **`asleep` is computed, not stored**: `now ≥ sleepsAt`. The client decides whether to show it, since a fleet with no Independent pilots has nothing to wake. + +## Step 24.4 + +- **"Back to work" stamps the time itself**, as well as through the catalog flag: the engine stamps after the handler, and the command's reply (the player state) should already read awake. +- **Waking needs no other change**: pilots asleep were left waiting at the time of the last catch-up, which is the moment of the waking command, so the next catch-up has them decide from then. diff --git a/memory-bank/architecture.md b/memory-bank/architecture.md index b7d58ab..e122bd5 100644 --- a/memory-bank/architecture.md +++ b/memory-bank/architecture.md @@ -64,6 +64,7 @@ The `routes` catalog is the list of engine commands (CR-014). Each entry has `re | `spendSkillPoint` | `POST /api/pilots/:pilotId/skills` | `pilotId`, `skillId` | `{ pilot }` | yes | | `acknowledgeSummaries` | `POST /api/summaries/acknowledge` | `dispatchIds`, optional `shipIds` (finished upgrades seen; at least one id overall) | `{ acknowledged }` | yes | | `acceptDeal` | `POST /api/deal/accept` | — | `PlayerState` | yes | +| `backToWork` | `POST /api/fleet/wake` | — | `PlayerState` (awake) | yes; a manual decision that does nothing else: wakes the fleet (CR-024) | | `exportSave` | engine only (CR-014) | — | `{ code }`: `DEADHEAD1.` + base64url(gzip(save JSON)) | yes | | `importSave` | engine only (CR-014) | `code` (whitespace ignored) | `{ playerId, state }`; `invalid-save` if it isn't a good save | no | | `devAdvanceClock` | `POST /api/test/advance-clock` | `advanceMs` | `{ offsetMs, realTime }` | yes; developer builds only | diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 5b23e4e..c714b81 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -524,7 +524,7 @@ Spec: `memory-bank/change-requests/CR-024-fleet-sleep.md` (received as "CR-022 - [x] **Step 24.1 — Content and save.** Config `fleetSleep` (default 24 h; options 12, 24, 48 and 72 h, rising, the default among them). Save version 9: `company.lastManualDecisionAt` and `company.sleepAfterHours`; a new company starts with its creation time and the default. The 8 → 9 migration sets the default and leaves the last decision null; the engine's catch-up sets a null one to the time it first runs on the save, which is the migration time in effect. Tests: `packages/content/test/config.test.ts`, `packages/schema/test/save.test.ts` (8 → 9), `packages/engine/test/fleet-sleep.test.ts` (a new company; an imported version-8 save is stamped with the import time). - [x] **Step 24.2 — Engine: manual decision flag.** `RouteDef.manualDecision` is required on every command in the catalog. Manual: dispatch and deadhead (`createDispatch`), buying ships, upgrades, refuelling, refuel policy, emergency fuel, leasing, hiring, assigning and unassigning, resting a pilot, skill points, the Dispatch Office, pilot mode and orders, the spending floor, the deal, renaming the company, the loan and goals. Not: reads, `acknowledgeSummaries`, `markWelcomeSeen`, export and import, new company, dev tools. The engine stamps `lastManualDecisionAt` after a flagged command succeeds; Independent pilots' trips go through the commands' functions, not the catalog, so they never do. Tests: `packages/schema/test/routes.test.ts` (every command declares it; the manual list); `packages/engine/test/fleet-sleep.test.ts` (a dispatch stamps it; viewing, a board, dismissing a summary, the welcome and an export don't; a refused command doesn't; Independent pilots' trips don't). - [x] **Step 24.3 — Engine: sleep in live play and catch-up.** `catchUp` processes Independent pilots' decisions only before the sleep time (`sleepsAt`, in `state.ts`), alongside the 7-day cap; arrivals, rest and shop ends resolve as before, and pilots due later wait at `until`. Live play is the same catch-up run by every command and by the loop. `PlayerState.fleetSleep` (`afterHours`, `sleepsAt`, `asleep`) tells the client, which shows "Waiting for orders" from it (Step 24.5). Tests (`packages/engine/test/fleet-sleep.test.ts`): two Independent pilots left 7 days start nothing at or after 24 hours, and the trips in flight then still resolve with pay; one go equals 5-hour pieces over 3 days; a trip the player sent lands after the sleep time as normal; a 48-hour threshold moves the cutoff. The 7-day cap test now uses a 30-day threshold so only the cap stops the pilots. -- [ ] Step 24.4 — Engine: waking and "Back to work" +- [x] **Step 24.4 — Engine: waking and "Back to work".** New command `backToWork` (`POST /api/fleet/wake`, a manual decision): it sets the last decision to now and returns the player state, awake. Any manual decision does the same, and pilots waiting at that time decide from it. Tests (`packages/engine/test/fleet-sleep.test.ts`): after two days asleep, "Back to work" brings Independent trips again, none before the moment of waking; so does buying fuel. `packages/schema/test/routes.test.ts` lists it as manual. - [ ] Step 24.5 — Client: return banner, status, and setting - [ ] Step 24.6 — Client: sleep notification (native) - [ ] Step 24.7 — Plan amendments and regression diff --git a/packages/engine/src/game/commands.ts b/packages/engine/src/game/commands.ts index f4a9b8b..e78bab4 100644 --- a/packages/engine/src/game/commands.ts +++ b/packages/engine/src/game/commands.ts @@ -68,6 +68,12 @@ export function commandTable(): CommandTable { acknowledgeSummaries: ({ save, now }, input) => ({ acknowledged: acknowledge(save, input, now), }), + // Wakes the fleet now (decision 0.5.138). The engine stamps every manual decision + // after the command; stamping here too makes the returned state awake. + backToWork: ({ save, now }) => { + save.company.lastManualDecisionAt = now; + return playerState(save, now); + }, markWelcomeSeen: ({ save, now }) => { save.player.welcomeSeen = true; return playerState(save, now); diff --git a/packages/engine/test/fleet-sleep.test.ts b/packages/engine/test/fleet-sleep.test.ts index 8e52d0e..289ccfa 100644 --- a/packages/engine/test/fleet-sleep.test.ts +++ b/packages/engine/test/fleet-sleep.test.ts @@ -186,3 +186,46 @@ describe("fleet sleep in catch-up (Step 24.3, decision 0.5.137)", () => { expect(Math.max(...departures)).toBeLessThan(decided + 48 * HOUR); }); }); + +describe("waking the fleet (Step 24.4, decision 0.5.138)", () => { + /** Asleep for two days, then the player decides something at `wake`. */ + async function asleep() { + const test = newTestApp(); + const decided = await sleepyFleet(test); + const wake = decided + 3 * DAY; + test.clock.now = wake; + expect((await test.app.call("getPlayerState", {})).fleetSleep.asleep).toBe(true); + return { test, wake }; + } + + const tripsFrom = async (test: TestApp, from: number) => + independentTrips(await readSave(test.app)).filter((d) => d.record.departureAt >= from); + + it('puts Independent pilots back to work with "Back to work"', async () => { + const { test, wake } = await asleep(); + expect(await tripsFrom(test, wake - DAY)).toEqual([]); + + const state = await test.app.call("backToWork", {}); + expect(state.fleetSleep).toEqual({ afterHours: 24, sleepsAt: wake + DAY, asleep: false }); + test.clock.now = wake + HOUR; + await test.app.call("getPlayerState", {}); + const trips = await tripsFrom(test, wake); + expect(trips.length).toBeGreaterThan(0); + // From the moment of waking, not before. + expect(Math.min(...trips.map((d) => d.record.departureAt))).toBeGreaterThanOrEqual(wake); + }); + + it("wakes them with any other manual decision, such as buying fuel", async () => { + const { test, wake } = await asleep(); + const ship = (await readSave(test.app)).ships.find( + (s) => s.locationId === "earth" && s.fuelFu > 10, + )!; + await edit(test.app, (save) => { + save.ships.find((s) => s.id === ship.id)!.fuelFu -= 10; + }); + await test.app.call("refuelShip", { shipId: ship.id, amountFu: 5 }); + test.clock.now = wake + HOUR; + expect((await test.app.call("getPlayerState", {})).fleetSleep.asleep).toBe(false); + expect((await tripsFrom(test, wake)).length).toBeGreaterThan(0); + }); +}); diff --git a/packages/schema/src/routes.ts b/packages/schema/src/routes.ts index 87d19f6..5b679b4 100644 --- a/packages/schema/src/routes.ts +++ b/packages/schema/src/routes.ts @@ -240,6 +240,15 @@ export const routes = { manualDecision: false, }, /** CR-022: the welcome cards are closed for this company (decision 0.5.131). */ + backToWork: { + method: "POST", + path: "/api/fleet/wake", + request: emptyRequestSchema, + response: playerStateSchema, + auth: true, + // "Back to work" wakes the fleet: it is a decision (decision 0.5.138). + manualDecision: true, + }, markWelcomeSeen: { method: "POST", path: "/api/welcome/seen", diff --git a/packages/schema/test/routes.test.ts b/packages/schema/test/routes.test.ts index 0f3bb79..3f1edc7 100644 --- a/packages/schema/test/routes.test.ts +++ b/packages/schema/test/routes.test.ts @@ -31,6 +31,7 @@ describe("route catalog", () => { "setSpendingFloor", "acknowledgeSummaries", "acceptDeal", + "backToWork", "markWelcomeSeen", "exportSave", "importSave", @@ -95,6 +96,7 @@ describe("manual decisions in the catalog (Step 24.2, decision 0.5.136)", () => expect(manual).toEqual( [ "acceptDeal", + "backToWork", "assignPilot", "buyDispatchOffice", "buyShip",