The mystery machine keeps its mood to itself

"Random" read as random ACTIONS, and a revealed random pick is barely
a mystery anyway. The fourth workshop button is now "mystery": the
server rolls the temperament and keeps it — the join line records it
for replay, the drive loop plays it, but the room tells the table
only "mystery". Roster and scoresheet show ⚙ mystery; the machine's
behavior is the only tell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 16:36:21 -04:00
co-authored by Claude Fable 5
parent af75a024ff
commit 1aafb9e837
4 changed files with 18 additions and 9 deletions
+3 -1
View File
@@ -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]),
),
};
}
+12 -7
View File
@@ -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<PlayerId, AutomatonStyle>;
/** Seats the server itself plays: temperament, and whether it is told. */
bots: Map<PlayerId, { style: AutomatonStyle; secret: boolean }>;
}
const rooms = new Map<string, Room>();
@@ -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");
+2
View File
@@ -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 {
+1 -1
View File
@@ -1055,7 +1055,7 @@
<button class="stamp tiny" onclick={() => net.addBot("hunter")}>hunter</button>
<button class="stamp tiny" onclick={() => net.addBot("berserker")}>berserker</button>
<button class="stamp tiny" onclick={() => net.addBot("worrier")}>worrier</button>
<button class="stamp tiny" onclick={() => net.addBot()}>random</button>
<button class="stamp tiny" onclick={() => net.addBot()}>mystery</button>
</span>
{/if}
<label class="check">