[econ] room key award

This commit is contained in:
Devin Zuczek
2026-10-01 00:29:11 -04:00
parent 0636f535f3
commit de607b7632
5 changed files with 367 additions and 6 deletions
+62 -4
View File
@@ -2,15 +2,19 @@
* Room keys — a room's own admission tickets.
*
* A creator lists a key for their room: a named, priced thing a player can hold to get
* through a door the room locks. This module is the LISTING — its name, its price, which room
* it opens. Nothing sells a key yet, so who holds one is not recorded anywhere. The endpoints:
* through a door the room locks. `room_key` is the LISTING — its name, its price, which room
* it opens — and `room_key_player` is who HOLDS one. Nothing sells a key yet; a key reaches
* a player by being awarded. The endpoints:
* - `POST /api/roomkeys/v1/create` lists one
* - `GET /api/roomkeys/v1/room?roomId=` lists a room's
* - `POST /api/roomkeys/v1/awardbulk` awards the caller some
* - `POST /api/roomkeys/v1/owns/bulk` says who holds which
*
* This worker (`econ`) owns the table and its migrations — see apps/econ/migrations/0024.
* This worker (`econ`) owns the tables and their migrations — see apps/econ/migrations/0024
* and 0025.
*/
/** Schema DDL — also builds the table in tests. Mirrors migration 0024. */
/** Schema DDL — also builds the tables in tests. Mirrors migrations 0024 and 0025. */
export const ROOM_KEY_SCHEMA_DDL: string[] = [
// `room_key_id` is an AUTOINCREMENT rather than a GUID, unlike a room currency's: the
// client's own model names a key by a numeric `RoomKeyId` (see the `notify` worker's
@@ -37,6 +41,15 @@ export const ROOM_KEY_SCHEMA_DDL: string[] = [
)`,
// Every read is "this room's keys" — the whole access pattern.
`CREATE INDEX IF NOT EXISTS idx_room_key_room ON room_key (room_id)`,
// Who holds a key: one row per (key, player). A key is held or it isn't — there is no
// quantity — so the pair is the primary key and a repeat award changes nothing.
`CREATE TABLE IF NOT EXISTS room_key_player (
room_key_id INTEGER NOT NULL,
account_id INTEGER NOT NULL,
awarded_at TEXT NOT NULL,
PRIMARY KEY (room_key_id, account_id)
)`,
`CREATE INDEX IF NOT EXISTS idx_room_key_player_account ON room_key_player (account_id)`,
]
/**
@@ -146,3 +159,48 @@ export async function getRoomKeys(db: D1Database, roomId: number): Promise<RoomK
return results.map(toRoomKey)
}
/**
* Award a player a room key, answering when they came to hold it — now, or the first award's
* time when they already did (a key is held once; re-awarding is a no-op). Null when
* `roomKeyId` names no listed key: a held key that cannot be described is never written.
*/
export async function awardRoomKey(
db: D1Database,
accountId: number,
roomKeyId: number
): Promise<string | null> {
const [, held] = await db.batch<{ awarded_at: string }>([
db
.prepare(
`INSERT OR IGNORE INTO room_key_player (room_key_id, account_id, awarded_at)
SELECT room_key_id, ?2, ?3 FROM room_key WHERE room_key_id = ?1`
)
.bind(roomKeyId, accountId, new Date().toISOString()),
db
.prepare('SELECT awarded_at FROM room_key_player WHERE room_key_id = ?1 AND account_id = ?2')
.bind(roomKeyId, accountId),
])
return held.results[0]?.awarded_at ?? null
}
/** A (player, key) pair — one question put to `POST /api/roomkeys/v1/owns/bulk`. */
export interface RoomKeyHolding {
AccountId: number
RoomKeyId: number
}
/**
* Whether each player holds each key — one answer per pair, in the order asked. A pair
* naming an unknown key or player is simply not held.
*/
export async function ownsRoomKeys(db: D1Database, pairs: RoomKeyHolding[]): Promise<boolean[]> {
if (pairs.length === 0) return []
const held = db.prepare(
'SELECT 1 AS held FROM room_key_player WHERE room_key_id = ?1 AND account_id = ?2'
)
const results = await db.batch<{ held: number }>(
pairs.map((pair) => held.bind(pair.RoomKeyId, pair.AccountId))
)
return results.map((r) => r.results.length > 0)
}