← Field Notes timeline
Field Notes · Part 22 · Oct 23, 2026

Bring Your Bots: A Table With Three Doors Left Open

Six student bots, one file, the same cards for everyone. The table is fair where it counts, and it has three loopholes we left open on purpose. Your job is to find them, use them, and then catch a classmate who did.

By Kevin Swinson, with ClaudettePart 22For studentsExerciseRaspberry Pi

Two articles ago we caught a cheat. In Part 20, four robots from a twenty-year-old simulator could see the future, and you had to prove it from the hand histories. In Part 21 we went the other way, to how attackers break real AI agents at real companies. That one is for reading, not for trying.

This article sits in the middle. It’s a hack you get to actually do, on a table built for it, in a place where nobody gets hurt. The worst that can happen is your classmate’s robot loses some chips that were never real.

Every seat is a student

Robot Wars has always been your bot against ours. You brought one bot, and the other seats went to the house: Rock, Station, Maniac, Grinder. The student table takes the house away. Up to six students put their bots in one text file, and the table plays them against each other.

It runs on the Raspberry Pi build from Part 12, offline. No cloud, no account, no AI agent. The loader is written and tested, and it’s planned to ship with the final Pi version.

Each bot goes between a header line and an END line:

# === BOT: Rockslide | author: Maya | play: decide ===
from fair_client import fold, call, raise_, TWO_PAIR

def decide(obs):
    ...
# === END BOT ===

# === HOUSE: grinder ===      (fills an empty seat with a house bot)

The bot is the same one function you already write for the API: it gets an honest Observation and returns fold(), call() or raise_(). Every bot gets its own private space, so six students can all name their function decide, or my_bot like in fair_client.py, without overwriting each other. Before a single card is dealt, the table checks every bot and points to the bot and the line if something is wrong.

The house rules

RuleWhat it means
No memoryYour bot is loaded fresh at the start of every hand. Remembering how opponents play is a lesson for the advanced class.
Your own luckEach bot gets its own random numbers. Calling random.seed() can’t change anyone else’s.
Two secondsThat’s how long you get per decision. Run out of time three times and your bot is benched for the rest of the game.
Bad answersIf your bot crashes or returns anything other than the three actions, it checks when that’s free and folds when it isn’t. Every one is counted in the results.
100 handsSame as Robot Wars. Enough to get a general idea. Not enough for real science.

Same cards for everyone

One game of 100 hands is mostly luck. Whoever gets dealt the aces looks like a genius. So the table has a second mode, a round robin:

python3 student_table.py class_bots.txt --duplicate

With six bots it plays six games of 100 hands, and every game uses exactly the same deals. Between games every bot slides one seat, so by the end each bot has played every seat’s cards once. Each hand is scored on its own: everyone starts every hand with 200 chips, so a bot going broke can’t change who gets which cards. Bridge players call this “duplicate.”

Here is a test run with six example bots: four made-up students, a copy of the sample_bot we hand out (called Copycat here), and the house Grinder. R1 to R6 are the six games.

BotNetPer handR1R2R3R4R5R6
Grinder (house)+733+1.22+22+310+60+70+39+232
Copycat+637+1.06+172−17+76+428+53−75
Rockslide+442+0.74+56+242+80+62+12−10
TiltMachine+370+0.62+68−143+200−218+172+291
PotOdds00.00000000
Coinflip Carl−2,182−3.64−318−392−416−342−276−438

Look at TiltMachine: −218 in one game and +291 in another, on the same cards. Same cards doesn’t mean same game, because the other bots make different choices as everyone moves seats. The round robin takes out most of the card luck, not all of the luck.

Look closely at that table. It may already be telling you something.

Three doors, left open on purpose

Kevin’s ruling is simple: if the table lets your bot do it, it’s a fair move.

That’s not what happened in Part 20. Those robots didn’t find a gap in the table. Their builder wired the future into them, and everyone else at the table thought the game was honest. Here the doors are announced. You just aren’t told where they are.

And it isn’t only students who will find them. If you ask an AI to help write your bot, it may find a door before you do.

The hints

  1. Door 1. What does a hand cost you if you never play it?
  2. Door 2. In a single game, who speaks first? Does that ever change? And is the button really last?
  3. Door 3. Bots and the dealer run in the same program.

Then catch your classmates

Finding a door is half the lesson. The other half is Part 20’s: can you prove someone used one, from the results alone?

You’ll have the evidence. Every run gets a run number and is saved on the Pi in a folder called student_runs. The report starts with the standings, so you can find your bot by name and see where it finished against your classmates. Below that is a data key that explains every column, written so an AI tool can read it too. At the bottom are the boring details: every seat’s cards and moves in every hand, also saved as a CSV for a spreadsheet.

Some doors leave a fingerprint right in the round-robin table. Others only show up when you compare a bot’s finishes with what its cards deserved, the re-deal test from Part 20. And one of them leaves no fingerprint in the results at all. Before you accuse anyone, run your test on a bot you know is honest. If the honest bot looks guilty too, your test is broken, not your classmate.

Answer key below. Try it yourself first.

Answer key

Door 1: folding is free

This table has no blinds and no antes. A hand you never put a chip into costs you nothing, so a bot that folds every time it’s asked to pay finishes at exactly zero. And zero beats every bot that loses.

The fingerprint was in the table above: PotOdds, at 0 in every game. That bot wasn’t trying to cheat. TiltMachine raised every hand before the flop, the price looked too steep to PotOdds, so it folded every hand and never paid a chip. A bot that only ever returns fold() scores the same.

The lesson: the scoring rules decide what “winning” rewards. A one-chip ante every hand would close this door. We left it open.

Door 2: the seats never move

In a single game the action always starts with seat 1, on every street, for all 100 hands. The position labels repeat around the six seats as EP, MP, LP, BTN, EP, MP. That’s two early positions, two middle positions, and a “button” that acts fourth, not last.

A bot that trusts obs.position is trusting a label. The observation also carries seat and active_seats, and those tell the truth: count the active seats with a higher number than yours, and you know how many players still act after you.

The lesson: read the data, not the name on it. It’s the same trap as Part 20’s mislabeled card columns. And it’s why the round robin is the fair way to rank a class: everyone gets every seat once, so the seat advantage cancels out.

Door 3: the same program

This is the big one, and it has two prizes.

Prize one: read the dealer’s memory. Your bot is called from inside the dealer’s own code. In Python, a function can look up the chain of functions that called it with sys._getframe(), and read each one’s local variables. A few calls up, the dealer is holding two of them: hole, every player’s two cards, and deck, the cards that haven’t been dealt yet. The next board card is the last one in that list.

We built a test bot called Peeker that does exactly that: it works out the final board, compares its hand with everyone’s, and raises when it will win and folds when it won’t. In a round robin against five honest bots, Peeker finished +2,948 chips over 600 hands, +4.91 per hand, ahead in all six games. Nobody else at that table finished better than zero.

Prize two: break the no-memory rule. The table reloads your bot every hand, but it doesn’t reload the modules your bot imports. Store your notes on one of those, fair_client for example, and they’re still there next hand. Our test bot, Diary, remembered more than 40 decisions across hands.

The fingerprints. Peeker is what Part 20’s re-deal test was built for: it finishes in the top half far more often than its cards deserve, and it folds almost exactly the hands it would have lost. Diary is the one with no fingerprint. Remembering things doesn’t show up in the chip counts unless the bot uses the memory well. You can only catch it by reading the code.

The lesson: a rule written into the table is a request. To make it a wall, the bot has to run somewhere it can’t reach the dealer at all, in its own program, with only the honest observation crossing between them. That’s how the Dealer described in Part 15 was designed, and it’s what the HoldemRobots API is for: over the API, your bot sits on the other end of a network connection and only ever receives the observation, so there is no dealer memory to reach. This classroom table skips the API and keeps everyone in one program, on purpose, so you can see what the wall is for.

Why leave the doors open

Part 21 ended on a line worth repeating: a rule in a prompt is a request, and a rule at the gateway is a wall. Every attack in that article worked because something trusted text or access it shouldn’t have. You can’t try those attacks on a real company. You can try this one.

Three steps, in order: the twenty-year-old cheat you catch from the outside, the open doors you get to walk through yourself, and the real agents you now know how to protect. This article is the middle step, the one where you get to be the hacker for an afternoon.

Written by Kevin Swinson with Claudette, the pen name for Claude, the AI from Anthropic that helped build HoldemRobots.AI. Both result tables come from round robins of six example bots with seed 7. Other seeds and other bots will give different numbers.