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:
co-authored by
Claude Fable 5.1
parent
95a1915cae
commit
8366c9d7ff
@@ -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, []]));
|
||||
|
||||
@@ -107,6 +107,8 @@ export interface LogEntry {
|
||||
turn: number;
|
||||
kind: LogKind;
|
||||
text: string;
|
||||
/** Wizards who may not read this line: it describes gestures they could not see. */
|
||||
hiddenFrom?: WizardId[];
|
||||
}
|
||||
|
||||
/** What a reveal did to each wizard's health, for the result strip. */
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// What one wizard is allowed to know. Gestures made while they were blind, or
|
||||
// while the other wizard was invisible, are unknown to them; the log lines that
|
||||
// describe those gestures are withheld too. Spells cast are always visible, as
|
||||
// the rules say.
|
||||
|
||||
import { visibleHistory, type GameState, type WizardId } from './state';
|
||||
|
||||
export function viewFor(state: GameState, viewer: WizardId): GameState {
|
||||
const view = structuredClone(state);
|
||||
for (const id of view.seats) {
|
||||
if (id === viewer) continue;
|
||||
view.wizards[id].history = visibleHistory(state, viewer, id);
|
||||
}
|
||||
view.log = view.log.filter((l) => !l.hiddenFrom?.includes(viewer));
|
||||
return view;
|
||||
}
|
||||
Reference in New Issue
Block a user