diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 45f6d10..0d47965 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -172,7 +172,9 @@ function roomInfo(room: Room) { hostId: room.hostId, started: room.state !== null, colors: Object.fromEntries(room.colorChoices), - bots: Object.fromEntries(room.bots), + bots: Object.fromEntries( + [...room.bots].map(([name, b]) => [name, b.secret ? "mystery" : b.style]), + ), }; } diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index f5dfecc..6d2819e 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -47,8 +47,8 @@ export interface Room { events: GameEvent[]; // full history (unredacted — redact per recipient) /** Table talk, persisted with the room (public to all seats). */ chat: { player: PlayerId; text: string; at: string }[]; - /** Seats the server itself plays, and each one's temperament. */ - bots: Map; + /** Seats the server itself plays: temperament, and whether it is told. */ + bots: Map; } const rooms = new Map(); @@ -247,12 +247,14 @@ export function addAutomaton(room: Room, styleWanted?: string): { name: PlayerId if (room.players.length >= 6) return { error: "room is full" }; const name = AUTOMATON_NAMES.find((n) => !room.players.includes(n)); if (!name) return { error: "the workshop is empty" }; - const style: AutomatonStyle = AUTOMATON_STYLES.includes(styleWanted as AutomatonStyle) + const known = AUTOMATON_STYLES.includes(styleWanted as AutomatonStyle); + const style: AutomatonStyle = known ? (styleWanted as AutomatonStyle) : AUTOMATON_STYLES[randomInt(AUTOMATON_STYLES.length)]!; + const secret = !known; // the mystery machine keeps its mood to itself room.players.push(name); - room.bots.set(name, style); - appendLine(room.id, { kind: "join", name, bot: true, style }); + room.bots.set(name, { style, secret }); + appendLine(room.id, { kind: "join", name, bot: true, style, ...(secret ? { secret: true } : {}) }); return { name }; } @@ -277,7 +279,7 @@ export function driveOneAutomaton(room: Room): { seat: PlayerId; events: GameEve const seat = actingSeat(room); if (!seat || !room.bots.has(seat)) return null; const view = viewFor(room.state!, seat); - const cmd = automatonCommand(view, room.bots.get(seat)) ?? automatonFallback(view); + const cmd = automatonCommand(view, room.bots.get(seat)?.style) ?? automatonFallback(view); let r = runCommand(room, seat, cmd); if ("error" in r) { r = runCommand(room, seat, automatonFallback(view)); @@ -494,7 +496,10 @@ export function loadPersistedRooms(): void { if (line.kind === "join") { if (line.bot) { room.players.push(line.name); - room.bots.set(line.name, (line.style as AutomatonStyle) ?? "hunter"); + room.bots.set(line.name, { + style: (line.style as AutomatonStyle) ?? "hunter", + secret: line.secret === true, + }); } else { const joinHash = line.tokenHash ?? (line.token ? hashToken(line.token) : null); if (!joinHash) throw new Error("join line has no token"); diff --git a/packages/server/src/store.ts b/packages/server/src/store.ts index f63d107..6a3f71d 100644 --- a/packages/server/src/store.ts +++ b/packages/server/src/store.ts @@ -28,6 +28,8 @@ export interface JoinLine { bot?: true; /** The automaton's temperament. */ style?: string; + /** A mystery machine: the temperament is not revealed to the table. */ + secret?: true; } export interface StartLine { diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 49a7739..3018e63 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1055,7 +1055,7 @@ - + {/if}