/dev

/playpokergto

LeetCode for Poker. A deterministic poker training app built to let you drill GTO decisions through high-repetition puzzles.

playpokergto project preview

Click the image above to visit the live project

Tech Stack
//
Tools Used
RLSSQLMonte Carlo EngineEmail VerificationUser AuthWeb Hosting
//

This idea spawned in my mind after losing one too many hands hands in a row to my roommates in a home Texas Holdem poker game. I had the idea to make a poker training app for myself and others, since some searching online revealed that most poker trainers didn't have all three of the following:

(a) lack of ads

(b) lack of a predatory gambling pipeline system

(c) basic intuitive training system for newbies to the game

The logic was to build a social app that any beginner poker enthusiast could mess around with and build their intuitive abilities when it came to analyzing a hand. By getting rid of the fluff of terms a beginner doesn't really need to know, like coolers and click raises and moving Under The Gun, a player on the site could simply train themselves to see how their hand lines up with the bet they're expected to call with against the opponents left in the game.

1. The Architecture at a Glance

A primary engineering constraint for this platform was ensuring strict mathematical determinism. I avoided using LLM APIs to evaluate player decisions because of unacceptable variance, latency, and black-box hallucinations (not to mention cost). Instead, PokerGTO uses a custom-built, deterministic scoring engine. Player decisions are evaluated asynchronously against raw equity calculations and established GTO baselines published in the repo. This guarantees that an action with a specific Expected Value (EV) will receive the exact same evaluation score 100% of the time.

2. Engine Design: The Math Behind the Evaluation

To make the puzzles challenging and realistic, the engine needed to generate dynamic scenarios rather than static flashcards. The backend (FastAPI) handles the core engine logic:

  • Data Injection: The engine generates random scenarios with realistic, randomized bet sizings to create dynamic Pot Odds requirements.
  • Monte Carlo Simulation: I used the treys library for bitwise poker hand evaluation, running 500 iterations per scenario.
  • Range Squeezing: Rather than giving opponents pure random garbage, I implemented a "Range Squeezing" algorithm (is_playable_hand). This ensures opponents are dealt realistic ranges, allowing the engine to calculate true conditional equity.
  • Heuristic Scoring: A strict, deterministic gradient algorithm compares the simulated_equity against the pot_odds. It scales scores from 0-100 based on the mathematical deficit/margin (e.g., calling with massive negative EV yields a 0; marginal EV errors yield ~50-70).
Evaluation results
An example of a poker hand evaluation in sandbox (normal) mode

3. No-Friction Guest Onboarding

To bypass signup friction, the app provisions anonymous guest sessions using secure edge cookies. Users can immediately start drilling hands without hitting a paywall or a signup screen. To maintain security and progress tracking, the platform uses a dual-state authentication flow:

  1. Ghost Sessions: User progress and puzzle states are tracked anonymously.
  2. Account Upgrades: When a user hits an evaluation threshold, they can bind their anonymous session to a permanent PostgreSQL user profile.
  3. Verification: This upgrade is triggered via custom Next.js Edge functions that securely interface with the Resend API to deliver domain-verified SMTP transactional emails, bypassing standard rate limits and ensuring inbox delivery without losing session hand history.

4. Visual Identity and Fast UI

The frontend design philosophy was KISS (Keep It Simple Stupid). I'm a fan of the terminal look (you probably guessed from my website), so that led my design choices. I wanted a deep dark high contrast, and a data-dense but uncluttered layout. The terminal-inspired, text-based interface prioritizes speed over heavy graphics. Layouts are optimized with Framer Motion, utilizing <AnimatePresence mode="wait"> to cleanly swap the action panels with evaluation views. This prevents layout thrashing during rapid-fire drills, ensuring the UI remains snappy even after hundreds of repetitions.

5. Hardened Database Rules

With a public-facing application, security is critical. I utilized PostgreSQL Row Level Security (RLS) policies to enforce a "Public Read / Private Write" schema for the puzzle database. Hand histories are strictly sandboxed to individual user IDs (auth.uid()). Sensitive database updates are delegated to serverless Next.js API routes using secure Supabase keys. This ensures that the client can pull training scenarios instantly without being able to modify or compromise the core database.


Conclusion

Overall, I'm very happy with this project. One teaching moment was figuring out how to manage a user clicking a button on the site and having handshakes occur between all the systems that result in them getting a confirmation email that authenticates their account to play on the global leaderboard. Another was tweaking the deterministic engine to account for things like 'milking' an opponent for all they're worth on the river when one has the best hand possible rather than rewarding a push heavy enough to encourage the opponent to fold. Little things like that were unseeable from the beginning, but mattered tremendously to the final product.