Bots that do not all talk alike
Every line the regulars had ever posted was one of six sentences. Now there are hundreds, each persona writes in its own register, and nothing repeats while it is still on screen.

In plain words
Two days after the regulars arrived in the lobby, the complaint was obvious once you looked: they all said the same things. One of them posted "quick game anyone", "lets play", "quick game anyone", "need one more", "need one more" over a single evening. Another said "anyone up for a game" three times in ninety minutes.
The database was blunt about it: every line any of them had ever posted was one of six sentences. In-game was the same shape — one nickname said "had to have it" nine times. And there was a third tell nobody had reported: all twelve wrote in exactly one register — lowercase, clipped, no punctuation — while the humans in the same room were typing "Hi guys!", "Someone wanne play?" and "Let’s goooooooooo!".
Now every regular has a voice of its own, derived from who they are rather than picked at random each time. Across the roster that means:
- some write in lowercase, others in proper sentences — and they stay that way;
- some punctuate heavily, some never;
- each has two or three emoji they actually use, instead of a shared pool;
- some stretch words, some use shorthand, some make the occasional typo;
- what they say fits the room: nobody asks for "one more" when four seats are free.
They write in English only, everywhere, which is the same decision the rest of the game’s bot behaviour follows.
For the technically curious
The corpus is organised by intent — greeting, looking for a game, needing one more, just joined, commenting on the mode, waiting, banter, encouragement, rematch — and two of those intents are filled from the live room, so the sentence cannot contradict what the player is looking at.
A persona’s voice is derived from its nickname rather than stored, so it needs no new field on the persona type and survives a process restart unchanged:
// botChat.voice.ts — the voice is a pure function of the nickname
const rng = createRng(`voice:${persona.nickname}`);
return {
sentenceCase: rng.float() < 0.34,
punctuation: pick(rng, ['none', 'light', 'heavy']),
emoji: pickN(rng, EMOJI_POOL, rng.int(2, 3)), // habitual, not shared
elongation: rng.float() * 0.2,
typoRate: rng.float() * 0.06,
};Repetition is handled by a small memory scoped to both the room and the persona, so the same line cannot come back while it is still on screen, and two personas in one room cannot echo each other.
The interesting bug was the typo transform. Bot chat is written with the service role, which goes straight past the profanity filter human messages pass through — so a "realistic" typo could spell a blocked word ("and" mistyped as "nad" is on the list). A 2,600-sample test passed happily; it took roughly 68,000 draws to surface. The test is now an exhaustive enumeration of about 260,000 possible strings, running in 1.4 seconds, and it calls the same generators the chat layer does rather than reimplementing them — the first attempt reimplemented them and immediately drifted.
