Step 10B.3: key contract boards by location
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
553983590e
commit
b3d4975111
|
|
@ -627,3 +627,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Deadhead checks, in order:** ship idle, pilot idle, pilot assigned to the ship, destination not the current location (`already-there`), destination discovered (`location-undiscovered`), landing, range, fuel credits. Bays play no part.
|
||||
- **`no-bay-at-destination` is gone** from sim reasons and API error codes; `location-undiscovered` (already a server code) is now also a sim reason.
|
||||
- **The server change landed in this step** (it must pass `discoveredIds` to compile), so the Step 6.11 "no bay at destination" test became "undiscovered destination" here; Step 10B.5 adds the stranded-ship scenario.
|
||||
|
||||
## Step 10B.3
|
||||
|
||||
- **The board key was already per location:** (player, kind, location, period, tutorial run) since CR-001. This step renames the `board_kind` value `bay` to `location`, so the stored data says what it is.
|
||||
- **The migration relabels existing `bay` rows as `location`** in one `UPDATE` while the column is temporarily text. That keeps the user's development database usable without a reset; it is a one-line relabel, not a data migration of board contents.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
ALTER TABLE "contract_boards" ALTER COLUMN "kind" SET DATA TYPE text;--> statement-breakpoint
|
||||
UPDATE "contract_boards" SET "kind" = 'location' WHERE "kind" = 'bay';--> statement-breakpoint
|
||||
DROP TYPE "public"."board_kind";--> statement-breakpoint
|
||||
CREATE TYPE "public"."board_kind" AS ENUM('location', 'tutorial');--> statement-breakpoint
|
||||
ALTER TABLE "contract_boards" ALTER COLUMN "kind" SET DATA TYPE "public"."board_kind" USING "kind"::"public"."board_kind";
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1791039630016,
|
||||
"tag": "0002_cr001_faster_start",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1791053200611,
|
||||
"tag": "0003_cr002_location_boards",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ const time = (name: string) => bigint(name, { mode: "number" });
|
|||
|
||||
export const activityStatus = pgEnum("activity_status", ["idle", "in_flight"]);
|
||||
export const dispatchStatus = pgEnum("dispatch_status", ["in_flight", "resolved"]);
|
||||
export const boardKind = pgEnum("board_kind", ["bay", "tutorial"]);
|
||||
export const boardKind = pgEnum("board_kind", ["location", "tutorial"]);
|
||||
|
||||
export const players = pgTable("players", {
|
||||
id: text("id").primaryKey(),
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ export async function getOrCreateBoard(
|
|||
tx: Queryable,
|
||||
key: {
|
||||
playerId: string;
|
||||
kind: "bay" | "tutorial";
|
||||
kind: "location" | "tutorial";
|
||||
locationId: string;
|
||||
periodIndex: number;
|
||||
/** Tutorial boards: runs completed (CR-001). Bay boards: 0. */
|
||||
/** Tutorial boards: runs completed (CR-001). Location boards: 0. */
|
||||
tutorialRun?: number;
|
||||
},
|
||||
generate: () => Contract[] | Promise<Contract[]>,
|
||||
|
|
@ -140,7 +140,7 @@ export async function bayBoard(
|
|||
const periodIndex = boardPeriodIndex(now, content.config);
|
||||
const contracts = await getOrCreateBoard(
|
||||
tx,
|
||||
{ playerId, kind: "bay", locationId, periodIndex },
|
||||
{ playerId, kind: "location", locationId, periodIndex },
|
||||
async () => {
|
||||
const discovered = await tx
|
||||
.select({ locationId: discoveredLocations.locationId })
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const LOANER_SHIP_ID = "loaner";
|
|||
export async function findBoardContract(
|
||||
tx: Queryable,
|
||||
playerId: string,
|
||||
kind: "bay" | "tutorial",
|
||||
kind: "location" | "tutorial",
|
||||
contractId: string,
|
||||
): Promise<{ contract: Contract; periodIndex: number } | undefined> {
|
||||
const [board] = await tx
|
||||
|
|
@ -60,7 +60,7 @@ export async function findBoardContract(
|
|||
export async function claimableContract(
|
||||
tx: Queryable,
|
||||
playerId: string,
|
||||
kind: "bay" | "tutorial",
|
||||
kind: "location" | "tutorial",
|
||||
contractId: string,
|
||||
): Promise<{ contract: Contract; periodIndex: number }> {
|
||||
const found = await findBoardContract(tx, playerId, kind, contractId);
|
||||
|
|
@ -212,7 +212,12 @@ export async function dispatchContract(
|
|||
input: { contractId: string; shipId: string; pilotId: string },
|
||||
now: RealTime,
|
||||
): Promise<Dispatch> {
|
||||
const { contract, periodIndex } = await claimableContract(tx, playerId, "bay", input.contractId);
|
||||
const { contract, periodIndex } = await claimableContract(
|
||||
tx,
|
||||
playerId,
|
||||
"location",
|
||||
input.contractId,
|
||||
);
|
||||
const ship = await requireOwnShip(tx, playerId, input.shipId);
|
||||
const pilot = await requireOwnPilot(tx, playerId, input.pilotId);
|
||||
const player = await requirePlayer(tx, playerId);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { useTestDatabase } from "./db";
|
||||
import { putBoard, signUp, testContract, useTestApp } from "./game";
|
||||
|
||||
const database = useTestDatabase();
|
||||
const testApp = useTestApp(database);
|
||||
|
||||
describe("contract boards keyed by location (CR-002)", () => {
|
||||
it("lets one player have boards at two locations in the same period", async () => {
|
||||
const { db } = database();
|
||||
const guest = await signUp(testApp().app);
|
||||
await putBoard(db, guest.playerId, [testContract()], undefined, "earth");
|
||||
await putBoard(
|
||||
db,
|
||||
guest.playerId,
|
||||
[testContract({ originId: "moon", destinationId: "earth" })],
|
||||
undefined,
|
||||
"moon",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a second board for the same player, location and period", async () => {
|
||||
const { db } = database();
|
||||
const guest = await signUp(testApp().app);
|
||||
await putBoard(db, guest.playerId, [testContract()], undefined, "moon");
|
||||
await expect(
|
||||
putBoard(db, guest.playerId, [testContract()], undefined, "moon"),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -186,13 +186,19 @@ export async function insertDispatch(
|
|||
return record;
|
||||
}
|
||||
|
||||
/** Stores a hand-made Earth bay board for the current period. */
|
||||
export async function putBoard(db: Db, playerId: string, contracts: Contract[], now = TEST_NOW) {
|
||||
/** Stores a hand-made location board (Earth by default) for the period containing `now`. */
|
||||
export async function putBoard(
|
||||
db: Db,
|
||||
playerId: string,
|
||||
contracts: Contract[],
|
||||
now = TEST_NOW,
|
||||
locationId = "earth",
|
||||
) {
|
||||
await db.insert(contractBoards).values({
|
||||
id: randomUUID(),
|
||||
playerId,
|
||||
kind: "bay",
|
||||
locationId: "earth",
|
||||
kind: "location",
|
||||
locationId,
|
||||
periodIndex: boardPeriodIndex(now, content.config),
|
||||
contracts,
|
||||
createdAt: now,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Postgres via Drizzle (`pg` driver). Migrations are SQL files in `apps/server/dri
|
|||
| `pilots` | XP, skills (jsonb), points spent, assignment, location (unassigned only), status | unique `assigned_ship_id` (one pilot per ship) |
|
||||
| `bays` | Leased bays | unique (`player_id`, `location_id`) |
|
||||
| `discovered_locations` | Discovered location ids | pk (`player_id`, `location_id`) |
|
||||
| `contract_boards` | Stored bay boards and tutorial boards per period (tutorial boards also per run) (jsonb contracts) | unique (`player_id`, `kind`, `location_id`, `period_index`, `tutorial_run`) |
|
||||
| `contract_boards` | Stored location boards (one per discovered location, CR-002) and tutorial boards per period (tutorial boards also per run); `kind` is `location` or `tutorial` (jsonb contracts) | unique (`player_id`, `kind`, `location_id`, `period_index`, `tutorial_run`) |
|
||||
| `dispatches` | Full sim record (jsonb, with contract snapshot), status, arrival, resolution (jsonb), resolved-at, acknowledged-at | index (`status`, `arrival_at`), (`player_id`, `status`) |
|
||||
| `accepted_contracts` | Contracts taken from a board, linked to their dispatch | pk (`player_id`, `contract_id`) |
|
||||
| `claimed_goals` | Onboarding goals whose reward was claimed | pk (`player_id`, `goal_id`) |
|
||||
|
|
@ -106,7 +106,7 @@ const time = (name: string) => bigint(name, { mode: "number" });
|
|||
|
||||
export const activityStatus = pgEnum("activity_status", ["idle", "in_flight"]);
|
||||
export const dispatchStatus = pgEnum("dispatch_status", ["in_flight", "resolved"]);
|
||||
export const boardKind = pgEnum("board_kind", ["bay", "tutorial"]);
|
||||
export const boardKind = pgEnum("board_kind", ["location", "tutorial"]);
|
||||
|
||||
export const players = pgTable("players", {
|
||||
id: text("id").primaryKey(),
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ Spec: `memory-bank/change-requests/CR-002-open-markets.md`. New decisions record
|
|||
|
||||
- [x] **Step 10B.1 — Sim: boards at any location.** `BoardInput.location` (was `origin`) is the board's location; no bay notion in sim board generation. Moon board test added.
|
||||
- [x] **Step 10B.2 — Sim: deadhead validation.** `validateReposition` takes `discoveredIds` (not bays): any origin, destination discovered (`location-undiscovered`) and not the current location, range and landing. `REPOSITION_FAILURES` swaps `no-bay-at-destination` for `location-undiscovered`. Server passes the player's discovered locations.
|
||||
- [ ] Step 10B.3 — Database: boards keyed by location
|
||||
- [x] **Step 10B.3 — Database: boards keyed by location.** Migration `0003_cr002_location_boards`: `board_kind` is now `location` / `tutorial` (old `bay` rows relabelled); unique key (player, kind, location, period, tutorial run) means one board per player, location and period. Test: two locations coexist, a duplicate is rejected.
|
||||
- [ ] Step 10B.4 — Server: board endpoint for any discovered location
|
||||
- [ ] Step 10B.5 — Server: no more stranded ships
|
||||
- [ ] Step 10B.6 — Server: ship delivery to a bay
|
||||
|
|
|
|||
Loading…
Reference in New Issue