Prototype two-player four-faction setup
This commit is contained in:
1 parent
70ef142577
commit
24255905ed
28 files changed
+1091
-73
No files matched your search
+60
@@ -4,6 +4,7 @@ import {
|
||||
normalizeAutomaticGame,
|
||||
createGame,
|
||||
joinGame,
|
||||
joinControlledFaction,
|
||||
newPlayer,
|
||||
viewGame,
|
||||
synchronizeAuctionClock,
|
||||
@@ -446,6 +447,65 @@ export async function joinRoom(
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** Add a separately authenticated faction under an existing two-person controller. */
|
||||
export async function addControlledFaction(
|
||||
code: string, auth: SeatAuth, version: number, name: string, faction: FactionId,
|
||||
entryInput?: unknown,
|
||||
): Promise<EntryResult & { token: string }> {
|
||||
const id = crypto.randomUUID();
|
||||
const player = newPlayer(id, name, faction);
|
||||
const entry = await prepareEntry(entryInput, {
|
||||
kind: 'controlled-faction', room: code, actor: auth.playerId, version,
|
||||
name: player.name, faction,
|
||||
});
|
||||
// A receipt is not authority: revalidate the original controller on every replay.
|
||||
async function checkedReplay() {
|
||||
await readSeatView(code, auth);
|
||||
const snapshot = await readRoomSnapshot(code);
|
||||
const controller = snapshot.game.twoPlayerControllers?.controllers.find(c => c.seats.includes(auth.playerId));
|
||||
if (!controller) throw new RuleError('Your seat is not a two-person controller.');
|
||||
const replay = await replayEntry(entry);
|
||||
if (replay && !controller.seats.includes(replay.view.me))
|
||||
throw new RoomEntryError('This room-entry receipt does not belong to your controller.', 'ENTRY_RECEIPT_INVALID');
|
||||
return replay;
|
||||
}
|
||||
try {
|
||||
const replay = await checkedReplay();
|
||||
if (replay) return replay;
|
||||
await requireUnusedEntryToken(entry);
|
||||
const { game: g, control } = await readRoomSnapshot(code);
|
||||
if (control.closed) throw new RoomClosedError();
|
||||
if (control.paused || control.joinLocked)
|
||||
throw new RoomEntryError(
|
||||
control.paused ? 'An administrator paused this room. New players may join after it resumes.' : 'New joins are locked by an administrator.',
|
||||
control.paused ? 'ROOM_PAUSED' : 'ROOM_JOIN_LOCKED',
|
||||
);
|
||||
if (g.version !== version) throw new RoomEntryError('The table changed. Retry the same room-entry request.', 'ENTRY_CONFLICT');
|
||||
joinControlledFaction(g, player, auth.playerId);
|
||||
g.version++;
|
||||
const token = entry?.sessionToken ?? Array.from(crypto.getRandomValues(new Uint8Array(32)), n => n.toString(16).padStart(2, '0')).join('');
|
||||
const tokenHash = entry?.sessionHash ?? await hash(token);
|
||||
const results = await db().batch([
|
||||
db().prepare('UPDATE rooms SET state = ?, version = version + 1, updated_at = ? WHERE code = ? AND version = ? AND NOT EXISTS (SELECT 1 FROM room_removals WHERE room_code = rooms.code AND removed = 1) AND NOT EXISTS (SELECT 1 FROM room_closures WHERE room_code = rooms.code AND closed = 1) AND NOT EXISTS (SELECT 1 FROM room_controls WHERE room_code = rooms.code AND (paused = 1 OR join_locked = 1)) AND EXISTS (SELECT 1 FROM seats WHERE room_code = rooms.code AND player_id = ? AND token_hash = ? AND revoked = 0)')
|
||||
.bind(JSON.stringify(g), Date.now(), code, version, auth.playerId, auth.tokenHash),
|
||||
db().prepare('INSERT INTO seats(token_hash,room_code,player_id) SELECT ?,?,? WHERE changes() = 1').bind(tokenHash, code, id),
|
||||
...(entry ? [db().prepare('INSERT INTO room_entry_receipts(operation_hash,request_hash,session_hash,room_code,player_id) SELECT ?,?,?,?,? WHERE changes() = 1')
|
||||
.bind(entry.operationHash, entry.requestHash, entry.sessionHash, code, id)] : []),
|
||||
]);
|
||||
if (results[0].meta.changes !== 1) {
|
||||
await requireRoomNotRemoved(code);
|
||||
await requireRoomNotClosed(code);
|
||||
throw new RoomEntryError('The table or your seat changed. Retry the same room-entry request.', 'ENTRY_CONFLICT');
|
||||
}
|
||||
return { token, view: await readSeatView(code, { playerId: id, tokenHash }),
|
||||
...(entry ? { entryReceipt: { operationId: entry.operationId, replayed: false } } : {}) };
|
||||
} catch (error) {
|
||||
const replay = await checkedReplay();
|
||||
if (replay) return replay;
|
||||
await requireUnusedEntryToken(entry);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
export async function act(
|
||||
code: string,
|
||||
auth: SeatAuth,
|
||||
|
||||
Reference in new issue
Block a user