Step 6.2: test full player state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
906fb275d7
commit
2a8f788ce8
|
|
@ -262,3 +262,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
- **Game-rule failures return `409`** with their error code (`gameError`), except `not-found` (`404`).
|
- **Game-rule failures return `409`** with their error code (`gameError`), except `not-found` (`404`).
|
||||||
- **Routes live in `src/routes/*`** and share an `AppContext` (db, auth, clock, route options).
|
- **Routes live in `src/routes/*`** and share an `AppContext` (db, auth, clock, route options).
|
||||||
- **Test helpers** (`test/game.ts`): `useTestApp` (fresh app plus a movable `TestClock`), `signUp` (a real guest session via `POST /api/guest`), `call`/`getState`, `insertDispatch` (puts a sim-built dispatch in the database). `makeOwner` / `setCredits` (`test/db.ts`) reshape a signed-up player.
|
- **Test helpers** (`test/game.ts`): `useTestApp` (fresh app plus a movable `TestClock`), `signUp` (a real guest session via `POST /api/guest`), `call`/`getState`, `insertDispatch` (puts a sim-built dispatch in the database). `makeOwner` / `setCredits` (`test/db.ts`) reshape a signed-up player.
|
||||||
|
|
||||||
|
## Step 6.2
|
||||||
|
|
||||||
|
- **No new code was needed**: Step 5.5 built the state reader and Step 6.1 added resolution, so this step is the full test. A pilot's `level` and `skillPointsAvailable` are derived from XP and points spent each time state is read, never stored.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { content } from "@deadhead/content";
|
||||||
|
import { playerStateSchema } from "@deadhead/schema";
|
||||||
|
import { pilotLevel } from "@deadhead/sim";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { pilots, ships } from "../src/db/schema";
|
||||||
|
import { makeOwner, setCredits, TEST_NOW, useTestDatabase } from "./db";
|
||||||
|
import { call, insertDispatch, signUp, useTestApp } from "./game";
|
||||||
|
|
||||||
|
const database = useTestDatabase();
|
||||||
|
const testApp = useTestApp(database);
|
||||||
|
|
||||||
|
describe("full player state", () => {
|
||||||
|
it("reflects one ship in flight and one idle, and validates against its schema", async () => {
|
||||||
|
const { app, clock } = testApp();
|
||||||
|
const { db } = database();
|
||||||
|
const guest = await signUp(app);
|
||||||
|
const flyingShipId = await makeOwner(db, guest.playerId, guest.avatarId);
|
||||||
|
await setCredits(db, guest.playerId, 9000);
|
||||||
|
|
||||||
|
// A second ship at the Moon crewed by a hired pilot with some XP and a skill.
|
||||||
|
const idleShipId = randomUUID();
|
||||||
|
const hiredId = randomUUID();
|
||||||
|
await db.insert(ships).values({
|
||||||
|
id: idleShipId,
|
||||||
|
playerId: guest.playerId,
|
||||||
|
modelId: "freighter",
|
||||||
|
name: "Slow Ox",
|
||||||
|
locationId: "moon",
|
||||||
|
createdAt: TEST_NOW,
|
||||||
|
});
|
||||||
|
await db.insert(pilots).values({
|
||||||
|
id: hiredId,
|
||||||
|
playerId: guest.playerId,
|
||||||
|
name: "Rae Okafor",
|
||||||
|
xp: 260,
|
||||||
|
skills: { "heavy-cargo": 1 },
|
||||||
|
skillPointsSpent: 1,
|
||||||
|
assignedShipId: idleShipId,
|
||||||
|
createdAt: TEST_NOW,
|
||||||
|
});
|
||||||
|
const record = await insertDispatch(db, {
|
||||||
|
playerId: guest.playerId,
|
||||||
|
shipId: flyingShipId,
|
||||||
|
pilotId: guest.avatarId,
|
||||||
|
departAt: clock.now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await call(app, guest, "GET", "/api/state");
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
const state = playerStateSchema.parse(response.json());
|
||||||
|
|
||||||
|
expect(state.company.credits).toBe(9000);
|
||||||
|
expect(state.tutorial).toBeNull();
|
||||||
|
expect(state.bays.map((b) => b.locationId)).toEqual(["earth"]);
|
||||||
|
|
||||||
|
const flying = state.ships.find((s) => s.id === flyingShipId)!;
|
||||||
|
const idle = state.ships.find((s) => s.id === idleShipId)!;
|
||||||
|
expect(flying).toMatchObject({
|
||||||
|
status: "in-flight",
|
||||||
|
locationId: null,
|
||||||
|
dispatchId: record.id,
|
||||||
|
pilotId: guest.avatarId,
|
||||||
|
});
|
||||||
|
expect(idle).toMatchObject({
|
||||||
|
status: "idle",
|
||||||
|
locationId: "moon",
|
||||||
|
dispatchId: null,
|
||||||
|
pilotId: hiredId,
|
||||||
|
modelId: "freighter",
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatar = state.pilots.find((p) => p.id === guest.avatarId)!;
|
||||||
|
const hired = state.pilots.find((p) => p.id === hiredId)!;
|
||||||
|
expect(avatar).toMatchObject({
|
||||||
|
status: "in-flight",
|
||||||
|
locationId: null,
|
||||||
|
assignedShipId: flyingShipId,
|
||||||
|
});
|
||||||
|
expect(hired).toMatchObject({
|
||||||
|
status: "idle",
|
||||||
|
locationId: "moon",
|
||||||
|
assignedShipId: idleShipId,
|
||||||
|
xp: 260,
|
||||||
|
level: pilotLevel(260, content.config),
|
||||||
|
skills: { "heavy-cargo": 1 },
|
||||||
|
});
|
||||||
|
expect(hired.level).toBe(2);
|
||||||
|
expect(hired.skillPointsAvailable).toBe(2 * content.config.skillPointsPerLevel - 1);
|
||||||
|
|
||||||
|
expect(state.activeDispatches).toEqual([
|
||||||
|
expect.objectContaining({ id: record.id, status: "in-flight", arrivalAt: record.arrivalAt }),
|
||||||
|
]);
|
||||||
|
expect(state.summaries).toEqual([]);
|
||||||
|
expect(state.locations).toHaveLength(content.locations.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
## Phase 6 — Server Game Logic
|
## Phase 6 — Server Game Logic
|
||||||
|
|
||||||
- [x] **Step 6.1 — Lazy resolution on read.** `resolveArrivedDispatches` (`src/game/resolve.ts`) resolves arrived dispatches in arrival order inside `withPlayerLock` (`src/game/lock.ts`, a transaction holding the company row lock); a conditional status update makes each resolve exactly once. `GET /api/state` runs it before reading. Routes moved to `src/routes/`.
|
- [x] **Step 6.1 — Lazy resolution on read.** `resolveArrivedDispatches` (`src/game/resolve.ts`) resolves arrived dispatches in arrival order inside `withPlayerLock` (`src/game/lock.ts`, a transaction holding the company row lock); a conditional status update makes each resolve exactly once. `GET /api/state` runs it before reading. Routes moved to `src/routes/`.
|
||||||
- [ ] Step 6.2 — Get full player state
|
- [x] **Step 6.2 — Get full player state.** Endpoint from Step 5.5 plus lazy resolution; test covers a player with one ship in flight and one idle (crewed by a hired pilot), checking ships, pilots (XP, level, skills, points, assignment, status, location), bays, dispatches, summaries and schema validity.
|
||||||
- [ ] Step 6.3 — Tutorial board endpoint
|
- [ ] Step 6.3 — Tutorial board endpoint
|
||||||
- [ ] Step 6.4 — Tutorial dispatch
|
- [ ] Step 6.4 — Tutorial dispatch
|
||||||
- [ ] Step 6.5 — Buy a ship
|
- [ ] Step 6.5 — Buy a ship
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue