Inspiration
Most poker bots in a hackathon get written once and submitted on gut feel. We wanted to build a bot that could get better with outside help, but that would never accept a change just because it sounded smart. Poker made that hard on purpose: one hand result swings by about 25 big blinds, so a change can look great over a few hundred hands and still be worse.
What it does
Goofy Gamblers' Poker Bot is a heads-up Short Deck 5-Card Pot-Limit Omaha agent, plus an offline loop that strives to make it better.
- The bot (
agent.py) estimates its chance of winning and compares that to the price of calling. Preflop it uses a formula we fitted offline. On the flop, turn and river it runs fast Monte Carlo simulations. It also tracks how often the opponent raises and folds during a half and adjusts to that. - Built for the rules: a time governor spends about 2.5 s of the 8 s time bank per half and saves more time for big pots. Any error falls back to a legal check or fold, so a bug can't cost us a match by forfeit.
- The improvement loop: every self-play decision is logged and loaded into Snowflake, where SQL compares how each action actually performed. Gemini (Google AI Studio) reads those results and proposes setting changes. It also writes new sparring opponents (a bluffer, a maniac, a nit, a calling station and a trapper). Every proposal is then tested head-to-head on the same cards, and it is kept only if it wins by a statistically clear margin.
Results for the version we submitted (duplicate format, 1,000 deals per half):
- +7.4 bb/deal vs RandomBot, +7.1 vs CallingStation, +7.6 vs AlwaysPot, +12.6 vs AlwaysHalf. Every 95% confidence interval is above zero.
- Official judge run vs AlwaysPot: +7.03 bb/deal, CI [+5.45, +8.62], 0 errors.
How we built it
- Python + FastAPI with the official starter template. We left the server code untouched and changed only the strategy section.
- Harness: a duplicate-match runner that plays in-process using the real
game.py, so thousands of hands run in minutes without the servers. - Exploration logging: 10% of decisions are chosen at random. Because those random actions don't depend on the hand, comparing their results gives a fair answer to "what if we had played differently here?". Normal play can't answer that, since strong hands and aggressive actions always go together.
- Snowflake: a loader script creates the
POKER_BOTtables and uploads the CSV logs.analysis.sqlthen breaks results down by street, equity bucket and action, only for the hands where the action was random. - Gemini:
gemini_helper.py analyzesends the Snowflake results plus our current settings and asks for specific changes.gemini_helper.py sparringhas Gemini write new opponent bots in different styles. - Guardrails (
autotune.py): Gemini can only change six whitelisted settings, each within set bounds. A change must pass two tests. First a screen, then a confirmation on fresh cards. It needs at least +0.15 bb/deal overall and must not lose clearly to any single opponent. Every proposal and its result is logged.
Challenges we ran into
- Gemini was confidently wrong. Every change Gemini proposed failed the paired test: seven single changes plus one combination. Two rounds' suggestions came out unconfirmed or measurably worse. The best one looked like +0.35 bb/deal, then gave −0.10 when we re-ran it. The causes were things we had to fix ourselves: comparing fold vs. call unfairly, mixing chips with big blinds, using evidence from hands nowhere near the setting being changed, and judging a whole policy from single random moves.
- Noise. At about 25 bb per deal, most real improvements are smaller than the noise. Paired testing on identical cards was the only way to see anything.
- The time bank. Our first full judge run used 6.28 s of the 8 s bank in one half, dangerously close to a forfeit. We measured several budgets and settled on 2.5 s.
- Tooling. Snowflake's built-in assistant was blocked on our account, so we wrote the SQL by hand. In one automated run the Snowflake upload failed, and the loop fell back to analyzing the local logs instead of crashing.
Accomplishments that we're proud of
- A bot that beats all four baseline bots by a clear margin with zero errors in the official judge.
- A self-improving loop that rejects every bad suggestion, including ones that looked good on a first run. "No change adopted" is a measured result.
- Using Gemini where it is strong, generating ideas and opponents. Statistics make the final call, not the language model.
- Catching a near-forfeit time problem with measurement before submission.
What we learned
- Large language models are good at generating ideas and bad at judging them. We treat each suggestion as a hypothesis, not an answer.
- How you collect data decides what it can tell you. Without random exploration, our Snowflake tables would only have confirmed our existing strategy.
- With high variance, the experiment setup (paired seeds, confidence intervals, a minimum effect size, replication) matters more than how clever the strategy is.
- Utilizing Snowflake and Google AI Studios for the first time. Implementing the tools to interact with each other seamlessly, we leveraged each program to its strengths.
What's next for Goofy Gamblers' Poker Bot
- Smarter equity: estimate the opponent's likely range instead of assuming a random hand, which currently overstates our chances.
- Tougher sparring: train and test against stronger Gemini-written opponents and other teams' styles, not just the baseline bots.
- A more reliable pipeline: make the Snowflake step run every time, and try Snowflake's AI features if access opens up.
- Better search: replace one-change-at-a-time proposals with a structured search over the safe settings, still gated by the same paired tests.
Built With
- google-ai-studio
- python
- snowflake