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
+5 -1
View File
@@ -24,6 +24,8 @@ export interface EngagementStats {
firstGameAt: string | null;
/** How many of the games were hotseat tables reporting in anonymously. */
hotseatGames: number;
/** Games with at least one clockwork wizard at the table. */
automatonGames: number;
}
interface StatsFile extends Omit<EngagementStats, "wizardsSeated"> {
@@ -40,7 +42,7 @@ let data: StatsFile = {
commandsPlayed: 0, minutesAtTable: 0,
winsByTreasure: 0, winsByLastStanding: 0,
longestGameCommands: 0, fullestTable: 0, firstGameAt: null,
hotseatGames: 0, wizards: [], roomStages: {},
hotseatGames: 0, automatonGames: 0, wizards: [], roomStages: {},
};
let wizardSet = new Set<string>();
let loaded = false;
@@ -84,6 +86,7 @@ export function recordRoom(room: Room): void {
const stage = data.roomStages[room.id];
for (const p of room.players) {
if (room.bots.has(p)) continue; // the clockwork are not wizards seated
const key = p.toLowerCase();
if (!wizardSet.has(key)) { wizardSet.add(key); dirty = true; }
}
@@ -95,6 +98,7 @@ export function recordRoom(room: Room): void {
}
if (room.state && data.roomStages[room.id] === "created") {
data.gamesStarted++;
if (room.bots.size > 0) data.automatonGames++;
data.fullestTable = Math.max(data.fullestTable, room.players.length);
data.roomStages[room.id] = "started";
dirty = true;