Nobody waits alone: meet the regulars
Opening a room used to mean waiting by yourself until you gave up. Now twelve regulars with real names, fixed personalities and human pacing fill the empty seats.

In plain words
Until this update, nothing ever joined a room you created. You picked a mode, opened a table, and waited — and mostly you waited alone. The numbers from the six weeks before this shipped are blunt:
| What we measured | Result |
|---|---|
| Rooms a player opened that never started | 38 |
| Average wait before giving up | ~3.2 hours |
| One room that filled 4/4 and still never started | waited 2 days |
| Finished games with more than one human | 46 of 231 |
Now the table fills itself. Somewhere between twenty seconds and four minutes after you open a room, someone sits down. A dozen regulars rotate through the lobby — ordinary nicknames, not RoboBaron — and each one is the same player every time you meet them:
- a fixed personality: the same opponent always plays cautiously, or always bids too high;
- a join date and an experience level that were not invented this morning;
- a pace of their own — they take between one and six seconds to think, not a metronomic two;
- the occasional short line in chat instead of a sticker;
- no "bot" badge in the waiting room, because at the table they behave like players.
They also appear on the leaderboard, which is the honest thing to do if they are going to play real games — but with a hard daily ceiling on what they can earn, so they climb like a regular player rather than farming the board.
For the technically curious
The twelve personas are provisioned idempotently when the game daemon boots, and marked with a value the profile table already supported — so the whole feature landed with no database migration and no change to any game rule.
The filler is a scheduler tick, not a timer per room. Every delay is derived from a seeded random keyed by room and seat, and anchored on a timestamp that lives in the database, so a daemon restart resumes exactly the same schedule instead of rolling fresh delays for rooms it already scheduled:
// scheduler.ts — seeded per (room, seat), anchored on a DB timestamp
const rng = createRng(`${game.id}:fill:${seat}`);
const isLastSeat = freeSeats === 1;
const delayMs = isLastSeat
? rng.int(LAST_SEAT_MIN_MS, LAST_SEAT_MAX_MS) // 3-8 min: leave room for a human
: rng.int(SEAT_MIN_MS, SEAT_MAX_MS); // 20 s - 4 min
if (Date.now() - Date.parse(game.created_at) < delayMs) continue;| Knob | Value |
|---|---|
| Seat fill delay | 20 s – 4 min, seeded per seat |
| Last seat | 3 – 8 min |
| Auto-start once the roster is full and quiet | 3 min |
| Bot think time | 1 – 6 s, scaled by persona |
| Persona XP | at most one award per UTC day |
Three defects only showed up when the thing actually ran, and no unit test would have caught any of them: full rooms were excluded from the filler’s candidate query, so the auto-start never saw the exact case it exists for; the think-delay gate was keyed per game rather than per bot, so a pause on a turn also blocked that bot’s auction bid until it was auto-passed out of its own window; and a game started by the daemon never woke the schedulers out of their idle backoff, which put 11.6 seconds between the start and the first bot roll instead of 49 milliseconds.
