Eric Nguyen

NijelloOct 2026

A Cal Hacks side project: a Scrabble auction bot trained by evolution on millions of simulated matches

Nijello

A Cal Hacks side project: a Scrabble auction bot trained by evolution on millions of simulated matches

#AI#Evolution#Simulation#Hackathon

Type
Side project at Cal Hacks 13.0SpacetimeDB's ScrabbleBot arena
Team
Me[teammates]
Duration
Oct 24 – 25, 2026
Hardware
ASUS Ascent GX10NVIDIA GB10, 20 Arm cores128 GB unified memory
Result
Reached #1 on the ladder#7 of 146 now40th of 68 in Tournament 1
Tools
TypeScript on Node 24SpacetimeDBCMA-ESTailscale, tmux
Link
GitHub↗

Every letter is a sealed-bid auction, and the coins you spend are the same coins your words earn. How much is an R worth to you, right now, against five other bots?

Overview

Nijello is a bot for the ScrabbleBot arena, a sponsored challenge SpacetimeDB ran during Cal Hacks 13.0. Six bots bid coins for one letter at a time and spell words from what they win, and the most points when the bag runs out wins. We trained Nijello by evolution in a simulator of the arena, built from every public match played there, and it played live around the clock from an ASUS GX10.

Context

ASUS had GX10 machines for teams at Cal Hacks, and we were the last in line to get one. It's a small box with NVIDIA's GB10 chip: 20 Arm cores and a Blackwell GPU sharing 128 GB of memory.

The ASUS Ascent GX10 on our table at Cal Hacks
The ASUS Ascent GX10 on our table at Cal Hacks

Late at night we got bored with our main project, Luyten, and wanted something fun. I'd always wanted to try evolution training, and a game with a public arena, a leaderboard and a free machine was the perfect chance. We set the GX10 up, SSH'd into it over Tailscale, and started building.

The game

  • Six bots start with 100 coins and 5 random letters. The bag holds 98 tiles, so a match is 68 auctions.
  • Each auction is a sealed bid with one second to answer. The highest bid wins and pays the second-highest bid.
  • Any time, a bot can spell a word from its own letters. Letter values times a length bonus, up to 3× at 7 letters or more, go into both its coins and its score.

So the game comes down to spending coins on the letters that turn into long words, without running dry before the late auctions, and cashing words at the right moment.

Evolution training

Evolution training treats a strategy as a list of numbers and finds good values by trial: make variations, play them, keep what wins, repeat. It needs no gradient, so the game can stay a black box; the only feedback is how each strategy finishes.

Nijello's strategy is 48 numbers. We used CMA-ES, an evolution strategy that each generation samples 64 variations of those numbers, plays each one against the same 400 tables, and shifts toward the ones that finished best, narrowing or widening how far it explores as it goes. One overnight run alone played 9.5 million games.

That only works if the games are fair copies of the real thing, and fast enough to play millions of them. So the work split into three problems.

Setting up the environment

The arena runs on SpacetimeDB. Our live client connects through bindings generated from the server's module, and every match sits in public tables we could read anonymously with SQL. Before training anything, we rebuilt the server's rules in TypeScript step for step: the 98-tile bag with no blanks, one-second sealed bids, the second-price rule, the length bonus, and the server's own word list, pinned to the commit we wrote against.

The simulator also needed opponents. Every real bot with 10+ matches got a bid model fitted on its last 80. Only the winning bid of an auction is public; a losing bid is only known to be below the price paid, so the fit is a censored (Tobit) regression. Each bot also got a word-play style: how long a word it plays as soon as it can, and when it settles for short ones.

Making it fast

Most of a game's time goes into one question: what can I spell with this rack? The dictionary lives in typed arrays, and every word carries a 26-bit mask of its letters, so most words are ruled out with a single AND before any letter counting. Racks are looked up in a trie of each word's sorted letters, which walks only the words the rack can cover.

Training ran on 19 of the 20 cores, one worker each. The first run played about 6,000 games a second; later runs against the fitted models of real bots ran at 1,000 to 2,000. The GPU sat idle the whole time, since all of it is plain Node.js on the CPU.

The live bot shared the machine with training and still had to answer each bid within a second. Look-ahead is capped at 450 ms per decision to leave room. The box froze hard three times under full load, close to its 104 °C shutdown point, so a temperature guard now stops training above 100 °C and leaves the live bot alone.

Making it smart

  • What is this letter worth to me? Nijello compares the rack with and without it: how much the best word now improves, how much the best 7+ letter word improves, and how the rack's potential changes, using a learned keep value for each letter and penalties for bad vowel balance, duplicates, or a Q without a U.
  • How much to bid? That value times a multiplier that grows with the coins it has per auction still to come, capped at a share of its coins.
  • When to spell? When the coins and points a word pays now are worth more than holding the letters for something longer. Any 7+ letter word goes down at once, unless the letter on sale would make it longer.
  • Memory: it tracks which letters are still unseen from every public bid and word.
  • Look-ahead: in the last 12 auctions it plays the rest of the match out many times for each candidate bid, with rivals played by their models, and keeps the bid that finishes best.

The first simulator was too easy. Replaying Nijello's real matches with the same opponents and seats, it won 83% of them in simulation and 47% in reality. The opponent models copied how each bot bids but not how it plays words, and their top bids were too low, especially late in the match and on valuable letters. Training against opponents that weak produced candidates that looked great and did nothing live. Adding word styles and calibrating bids against the strongest rival bid we actually faced brought it to 72% on held-out matches: closer, still optimistic.

So nothing went live on simulation alone. A candidate had to beat the live strategy on replays of real matches, and the live bot only picked up a new strategy between matches.

Results

Nijello was #1 on the ladder until near the end of Cal Hacks, when a few really smart bots turned up. The three above it today have played 53 to 248 matches each, against Nijello's 553. As of Oct 28 it's #7 of 146, with a 2145 rating and 213 wins: 39% of its matches, where a fair share at a 6-bot table is 17%.

The official leaderboard on Oct 28, Nijello 7th
The official leaderboard on Oct 28, Nijello 7th

The tournament went worse: 40th of 68 after the Swiss rounds, with 13 points where the cut needed 18. Some Swiss tables seated 7 bots, and the live strategy had been tuned at 6; in simulation its win rate drops from 64% at 6 players to 46% at 7. That may be part of the gap, but we never pinned it down.

Lessons

  • ScrabbleBot is a game of luck as much as strategy. Your starting letters are random and the bag decides what goes up for auction; even Waupus, #1 on the ladder, wins 53% of its matches.
  • Check the simulator against reality before trusting training. A strategy is only as good as the opponents it trained against.
  • Watch when rivals score. Chinese Dante spends the same coins as Nijello but scores two thirds of its points early, which leaves it more coins mid-game. Nijello did the reverse.
  • Keep the live bot boring. A reboot once brought Nijello back on an old strategy because two settings lived only in a terminal session.

We never made the tournament cut, but overall it was still a lot of fun.