A duel server: rooms, seat tokens, ledgers, and resolution when all have moved

Plain HTTP for actions and a websocket that only says "fetch again". A
room is its append-only ledger replayed from the top; the engine is
deterministic given the seed and the recorded inputs. Each seat sees a
view filtered to what the rules let it know. Bot seats are filled at
resolution time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-09-22 16:54:29 -04:00
co-authored by Claude Fable 5.1
parent 95a1915cae
commit 8366c9d7ff
11 changed files with 1024 additions and 4 deletions
+3 -3
View File
@@ -153,8 +153,8 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
/** Time-stop entries and log lines belong to the turn just played. */
const turn = actor ? state.turn - 1 : state.turn;
const events: string[] = [];
const log = (text: string, kind: LogKind = 'info', surface = kind !== 'gesture' && kind !== 'damage') => {
state.log.push({ turn, kind, text });
const log = (text: string, kind: LogKind = 'info', surface = kind !== 'gesture' && kind !== 'damage', hiddenFrom?: WizardId[]) => {
state.log.push(hiddenFrom?.length ? { turn, kind, text, hiddenFrom } : { turn, kind, text });
if (surface) events.push(text);
};
const rng = () => nextRandom(state);
@@ -256,7 +256,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
w.history.push({ left, right, turn, phase: entryPhase, hiddenFrom });
if (left === 'P' && right === 'P') w.surrendered = true;
const prefix = entryPhase === 'haste' ? `${w.name}, hastened, adds` : `${w.name}:`;
log(`${prefix} left ${lone(left, right)}, right ${lone(right, left)}.`, 'gesture');
log(`${prefix} left ${lone(left, right)}, right ${lone(right, left)}.`, 'gesture', false, hiddenFrom);
};
/** History indexes of the entries each acting wizard made this turn. */
const entries: Record<WizardId, number[]> = Object.fromEntries(seats.map((id) => [id, []]));