The full clockwork curriculum: keys, vengeance, spellcraft, and voice

Every play-strength item, in one education. DOORS: the pathfinder
walks through doors it can open — already-opened ones freely, locked
ones when holding Master Key or Pick Lock, casting the key at the
door on the path before stepping through (and a loop where the
clockwork re-keyed an already-open door forever is fixed: opened
doors read as open). TREASURE DEFENSE: a wizard carrying the
automaton's gold becomes the priority — chased over all objectives,
shaken down with DROP OBJECT when held, and attacked first. WIDER
SPELLCRAFT: blaster wands charged by number and fired; teleport as
escape when wounded and hunted, and as a counteraction clear of big
incoming spells; SPEED cast on sight; WARD armed to guard the gold;
shieldstone displayed so spare numbers soak small hits; ANTI-ANTI
pressed against counters (never against escapes); the berserker grows
BIG with an enemy near, the worrier fades invisible or slams a
CREATE WALL in its pursuer's face; ambushes armed from held
interrupts — the worrier trapping its doorstep, the others trapping
the treasure. Tournament suite green across all temperaments.

And the clockwork speaks: sparing, temperament-voiced table talk on
its own deeds — "ACQUISITION COMPLETE.", "SCHEDULED DEMISE:
DELIVERED.", "good wall. safe wall." — through the same chat ledger
as everyone else. The tally gains "games fought against the
clockwork", and automatons no longer pollute the count of wizards
seated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 17:10:47 -04:00
co-authored by Claude Fable 5
parent ca16faac73
commit 3eda686e29
4 changed files with 289 additions and 43 deletions
+47
View File
@@ -191,6 +191,52 @@ function broadcast(room: Room, makeMessage: (playerId: PlayerId) => unknown): vo
* broadcast as it lands, until the maze wants a human again.
*/
const BOT_STEP_MS = 1500;
/** The clockwork occasionally speaks. Sparingly — menace over chatter. */
const BOT_LINES: Record<string, Record<string, string[]>> = {
hunter: {
treasurePickedUp: ["ACQUISITION COMPLETE.", "YOUR GOLD HAS BEEN REALLOCATED."],
treasureDropped: ["DELIVERY CONFIRMED. PLEASURE DOING BUSINESS."],
damaged: ["AN INEFFICIENT USE OF YOUR TURN."],
died: ["INVENTORY TRANSFERRED. CONDOLENCES."],
},
berserker: {
treasurePickedUp: ["GOLD IS MERELY BAIT."],
damaged: ["YES. AGAIN.", "PAIN RECEIPT PRINTED."],
died: ["SCHEDULED DEMISE: DELIVERED.", "NEXT."],
creatureCreated: ["I HAVE MADE YOU A FRIEND. IT IS NOT FRIENDLY."],
},
worrier: {
treasurePickedUp: ["taking this. sorry. sorry."],
damaged: ["ow. logged.", "unnecessary!"],
wallCreated: ["good wall. safe wall."],
died: ["oh no. oh no. it worked?"],
},
};
function botRemark(room: Room, seat: string, events: { type: string; [k: string]: unknown }[]): void {
const bot = room.bots.get(seat);
if (!bot) return;
const lines = BOT_LINES[bot.style] ?? {};
for (const e of events) {
// Speak only about its own deeds, and rarely.
const mine =
(e.type === "treasurePickedUp" && e.player === seat) ||
(e.type === "treasureDropped" && e.player === seat) ||
(e.type === "damaged" && e.player !== seat) ||
(e.type === "died" && e.killedBy === seat) ||
(e.type === "creatureCreated" && e.controller === seat) ||
(e.type === "wallCreated" && e.caster === seat);
const pool = lines[e.type];
if (!mine || !pool || Math.random() > 0.35) continue;
const text = pool[Math.floor(Math.random() * pool.length)]!;
const said = addChat(room, seat, text);
if (!("error" in said)) {
broadcast(room, () => ({ type: "chat", player: seat, text: said.text, at: said.at }));
}
return; // one remark per step at most
}
}
const pumping = new Set<string>();
function runBots(room: Room): void {
if (pumping.has(room.id)) return;
@@ -203,6 +249,7 @@ function runBots(room: Room): void {
}
broadcast(room, (playerId) => ({ type: "events", events: redactFor(step.events, playerId) }));
broadcast(room, (playerId) => ({ type: "state", view: viewForPlayer(room, playerId), seq: room.log.length }));
botRemark(room, step.seat, step.events as { type: string }[]);
setTimeout(tick, BOT_STEP_MS);
};
setTimeout(tick, 800); // a beat after the human's own action settles