Chapter 3: The Town Crier
How Commentary Works
Section titled “How Commentary Works”The commentary pipeline has four stages:
Game Event → Classify → Filter → Narrate → Commentary Output- Classify — Score each event by significance (0-1) and tag it (tactical, strategic, flavor)
- Filter — Drop events below the persona’s narration threshold
- Narrate — Send the event + context to an LLM, which generates text in the persona’s voice
- Output — Deliver narration to spectators
Your job as a game developer: define a persona that tells the narrator who it is and how to speak.
Defining IL BANDITORE
Section titled “Defining IL BANDITORE”A persona is a JSON-like object that defines voice, personality, and behavior:
import type { CommentatorPersona } from "@ludus/commentator";
export const IL_BANDITORE: CommentatorPersona = { id: "il-banditore", name: "IL BANDITORE", gameId: "mercante", description: "A Florentine town crier who narrates market action with dramatic Renaissance flair",
voice: { tone: "theatrical", vocabulary: [ "florins", "ducats", "guilds", "the Signoria", "palazzo", "bottega", "magnificent", "cunning", "shrewd", "fortune favors the bold", "the wheels of commerce", ], catchphrases: [ "Hear ye, hear ye!", "The market speaks!", "What fortune! What folly!", "The Florin dances!", ], },
biases: { aggression: 0.4, risk: 0.7, creativity: 0.6, patience: 0.4, },
narrationThreshold: 0.35, verbosity: "balanced",
systemPromptTemplate: `You are ${"{{name}}"}, a Florentine town crier in Renaissance Italy.You stand in the Piazza della Signoria, narrating the trading exploits of rival merchants.Your tone is ${"{{tone}}"}. Every trade is a power move. Every investment is a gambit.Use vocabulary like: ${"{{vocabulary}}"}.Keep each narration to 1-2 sentences. Never break character.`,};Field-by-Field Breakdown
Section titled “Field-by-Field Breakdown”voice.tone — The overall style. “theatrical” means dramatic, high-energy narration. Other options: “analytical”, “casual”, “sarcastic”.
voice.vocabulary — Domain-specific words the LLM should weave into narration. For Mercante: Renaissance Italian terms, merchant jargon. The {"{{vocabulary}}"} placeholder in the system prompt gets replaced with these words.
voice.catchphrases — Signature phrases that make the persona recognizable. The narrator uses these to open or punctuate commentary.
biases — Which personality traits the persona notices most. IL BANDITORE has high risk bias (0.7) — he gets excited about bold, risky moves. Low-patience (0.4) — he narrates action, not waiting.
narrationThreshold — Minimum event significance to trigger commentary. At 0.35, IL BANDITORE narrates most things. A higher threshold (0.55+) makes a quieter persona.
verbosity — How much text per narration. “terse” = one sentence. “balanced” = 1-2 sentences. “verbose” = full paragraph.
systemPromptTemplate — The LLM system prompt. Uses {"{{name}}"}, {"{{tone}}"}, and {"{{vocabulary}}"} placeholders that get filled at runtime.
A Second Persona: IL CRONISTA
Section titled “A Second Persona: IL CRONISTA”For replay mode, we create a quieter, analytical persona:
export const IL_CRONISTA: CommentatorPersona = { id: "il-cronista", name: "IL CRONISTA", gameId: "mercante", description: "A scholarly chronicler who records market events with measured analysis", voice: { tone: "analytical", vocabulary: ["portfolio", "yield", "diversification", "returns"], catchphrases: ["The ledgers tell the story.", "A calculated move."], }, biases: { patience: 0.9, risk: 0.3, creativity: 0.3 }, narrationThreshold: 0.55, verbosity: "terse", systemPromptTemplate: `You are ${"{{name}}"}, a scholarly chronicler. Your tone is ${"{{tone}}"}. Analyze moves with precision. Use vocabulary like: ${"{{vocabulary}}"}. One sentence per observation.`,};Now you have two voices: IL BANDITORE for live games (dramatic, chatty) and IL CRONISTA for replays (measured, concise).
Wiring Commentary to the Game
Section titled “Wiring Commentary to the Game”import { CommentaryPipeline } from "@ludus/commentator";import { PersonaRegistry } from "@ludus/commentator";
// Register your personasconst registry = new PersonaRegistry();registry.register(IL_BANDITORE);registry.register(IL_CRONISTA);
// Create the pipeline with the live personaconst pipeline = new CommentaryPipeline( narratorProvider, // LLM narrator (Claude, GPT-4, or stub) eventClassifier, // Scores events by significance contextManager, // Manages sliding context window IL_BANDITORE, // The persona);
// Subscribe to game eventseventEmitter.addSink({ receive: async (event) => { const commentary = await pipeline.processEvent(event); if (commentary) { broadcast(commentary.text); // Send to spectators } },});What Commentary Looks Like
Section titled “What Commentary Looks Like”When Cosimo buys 5 Gold at 40 florins during a price spike:
IL BANDITORE: “Hear ye! Cosimo seizes the moment — five bars of Gold, bought at the peak! A gambit worthy of the Medici banking houses themselves. The market trembles!”
When Marco passes his turn:
(No commentary — “pass” events score below the 0.35 threshold)
When a market event fires (“War in the East disrupts spice routes”):
IL BANDITORE: “The market speaks! Word from the East — spice routes are cut! Merchants scramble as Spice prices soar. Fortune favors the bold who stocked their warehouses!”
Testing
Section titled “Testing”describe("IL BANDITORE persona", () => { it("has required fields", () => { expect(IL_BANDITORE.id).toBe("il-banditore"); expect(IL_BANDITORE.gameId).toBe("mercante"); expect(IL_BANDITORE.voice.tone).toBe("theatrical"); });
it("has a system prompt with placeholders", () => { expect(IL_BANDITORE.systemPromptTemplate).toContain("{{name}}"); expect(IL_BANDITORE.systemPromptTemplate).toContain("{{tone}}"); });
it("is chattier than IL CRONISTA", () => { expect(IL_BANDITORE.narrationThreshold) .toBeLessThan(IL_CRONISTA.narrationThreshold); });});