Thulaa Bot Coach
The bots keep score automatically. I'm still the one coaching them.
The measurement rig behind Thulaa's card bots: a nightly report on how they're doing against real players plus an offline self-play arena, so I can hand-tune the search. It keeps score; I coach.
Not an autonomous coach — and I won't pretend it is. The Thulaa bots are pure MCTS over hand-authored heuristics: no ML, nothing that self-tunes. What runs on its own is the measurement — a nightly report and a self-play arena I read to decide the next tweak. I'm the optimizer in the loop (pair-programming with Claude). Calling it an RL trainer would be a lie; it's a tight measure-then-tune loop with a human in the middle.
// architecture
Two things run on their own; the tuning doesn't. A scheduled Cloud Function reads the game_sessions collection every night and emails me a bot-performance report — average finish position, thulaas per bot, bot-vs-human win rate — with plain-English difficulty suggestions. Alongside it, an offline self-play arena plays four MCTS bots against each other over a batch of games and prints win-rates and blunders. I read both, hand-edit the MCTS heuristic weights (or the live difficulty config in Firestore), re-run the arena locally to check, and redeploy. The bots themselves run MCTS both in-app (Dart) and server-side (Cloud Functions) so a game continues when a host backgrounds the app.
// how it works
- 1
Bots decide by search
Every bot turn runs Monte-Carlo Tree Search (ISMCTS with determinization) over a hand-authored heuristic weight table, with a minimax solver for the endgame. Difficulty just scales the search budget and adds noise — no ML anywhere in the bot path.
- 2
Games get logged
Finished games land in the game_sessions collection — trick history, finish order, winners, end reason.
- 3
Nightly report
A scheduled Cloud Function (9pm London) reads those sessions and emails a bot-performance breakdown plus suggestions like 'bots winning 70%+ — consider easing difficulty'. It changes nothing on its own.
- 4
Self-play arena
Offline, I run a harness that plays four bots against each other over dozens of games and prints win-rates, blunders (leading into a known void) and degenerate games.
- 5
I tune by hand
I read the report and the arena output, hand-edit the heuristic weights (or the live Firestore difficulty config), re-run the arena to confirm, and redeploy. The human closes the loop.
// tool calls
MCTSEngine.findBestMovepicks a bot's card via ISMCTS + heuristic rollouts (client Dart + server TS mirror)onBotTurn (Firestore trigger)runs server-side bot moves in a Cloud Function so games survive the host backgrounding the appdailyAnalyticsReport (cron 9pm)reads game_sessions, computes win-rate/finish position, emails the report + difficulty suggestionsbot_arena.tsoffline self-play — 4 bots × 50 games, prints win-rate and blunder countsconfig/bot_difficultylive-overridable difficulty knobs (sims, exploration, mistake rate) without a redeploy
// nightly report
// stack
// infrastructure
- ▸Bots: MCTS in-app (Dart) + server-side (Cloud Functions)
- ▸Telemetry: game_sessions (multi-region Firestore)
- ▸Report: nightly Cloud Function → email
- ▸Tuning: manual — human in the loop
- ▸Models: none (pure search + heuristics)
// why it exists
The bots have to feel like a fun human opponent — beatable, never a pushover. This is the honest way I keep them there: measure automatically, tune by hand. No reinforcement learning, just a tight read-and-adjust loop.