← Blog
Cashing a WSOP Tournament with PLOScope. Part 2: The Tech

Cashing a WSOP Tournament with PLOScope. Part 2: The Tech

pokerploscopepythonreactarchitecturedockerside-project

In Part 1, I told the story of using a homegrown app to cash a WSOP tournament. Now let’s crack it open.

PLOScope is a Pot Limit Omaha double board bomb pot equity calculator and solver. It takes a hand, a position, and board textures, then runs thousands of simulations to tell you exactly where you stand across two simultaneous community boards.

The full stack is on GitHub if you want to dig in yourself.

The Problem PLOScope Solves

In standard Texas Hold’em, equity calculators are everywhere. But PLO double board bomb pots are a different beast:

  • 4 hole cards instead of 2. the combinatorial space explodes
  • Two boards running simultaneously. you need aggregate equity across both
  • No preflop action. bomb pots skip straight to the flop, so positional and range analysis is different
  • Scoop potential. winning both boards changes the math significantly

Off-the-shelf tools didn’t handle this format. I needed to build my own.

Architecture Overview

PLOScope is a microservices platform with six core components:

graph TB
    User[User Browser] --> Traefik[Traefik Reverse Proxy]
    Traefik --> Frontend[React Frontend :3000]
    Traefik --> Backend[Flask API :5001]

    Backend --> DB[(PostgreSQL)]
    Backend --> Redis[(Redis Cache)]
    Backend --> RabbitMQ[(RabbitMQ)]
    Backend --> gRPC[gRPC Server :50051]

    RabbitMQ --> Celery[Celery Workers]
    Celery --> Core[Core Equity Engine]
    Celery --> DB
    Celery --> Redis

    Backend -.->|WebSocket| User

    style Core fill:#f9a825,stroke:#f57f17,color:#000
    style Backend fill:#1565c0,stroke:#0d47a1,color:#fff
    style Frontend fill:#2e7d32,stroke:#1b5e20,color:#fff

The yellow box. the Core equity engine. is where the actual poker math lives. Everything else is plumbing to get hands in and results out.

The Core Engine

The equity engine is a Python library built on Treys for hand evaluation, with NumPy for fast array operations. Here’s how a simulation runs:

sequenceDiagram
    participant U as User
    participant API as Flask API
    participant Q as RabbitMQ
    participant W as Celery Worker
    participant E as Equity Engine
    participant WS as WebSocket

    U->>API: Submit spot (hands + boards)
    API->>Q: Enqueue job
    API->>U: Job ID + status: pending
    Q->>W: Pick up job
    W->>E: Run equity calculation
    E->>E: Monte Carlo simulation (N iterations)
    E-->>W: Results (equity per hand per board)
    W->>WS: Push progress updates
    WS-->>U: Real-time progress
    W->>API: Store results
    WS-->>U: Final results

The equity calculation itself works like this:

  1. Parse the input. player hands (4 cards each), known board cards (if any), dead cards
  2. Enumerate or sample. for small card spaces, enumerate all possible runouts; for larger ones, Monte Carlo sampling
  3. Evaluate each runout. for every possible combination of remaining community cards, evaluate each player’s best 5-card hand on each board using Treys
  4. Aggregate. calculate win/tie/loss percentages per player per board, then combine for total equity including scoop probability

The key insight for double board: your total equity isn’t just (board1_equity + board2_equity) / 2. Scoop probability. winning both boards. is disproportionately valuable because you take the entire pot instead of splitting. PLOScope tracks this separately.

The Tech Stack

Backend: Python + Flask + gRPC

The API server runs Flask for REST endpoints and WebSocket connections (via Flask-SocketIO), with a separate gRPC server for high-performance internal communication:

  • Flask 3.x. REST API, auth, job management
  • SQLAlchemy 2.x. ORM for PostgreSQL
  • Flask-SocketIO. real-time updates to the browser
  • gRPC. internal RPC between services
  • Celery 5.x. distributed task queue for compute-heavy jobs

I chose Flask over FastAPI because I started this as a quick prototype and Flask’s ecosystem (extensions, SocketIO integration) got me moving faster. Would I pick FastAPI for a greenfield rewrite? Probably. But Flask did the job.

Frontend: React + TypeScript

The UI is built with React 18 and TypeScript. Nothing exotic. React Bootstrap for components, Webpack for bundling, Socket.IO client for real-time solver progress.

The most interesting frontend challenge was the hand input interface. Representing 4 hole cards per player, two separate boards, and the ability to specify known/unknown cards in an intuitive way took several iterations. Playing cards are surprisingly tricky UI.

Infrastructure

graph LR
    subgraph "Data Layer"
        PG[(PostgreSQL 15)]
        RD[(Redis 7)]
        RMQ[(RabbitMQ 3.13)]
    end

    subgraph "Compute Layer"
        BE[Flask API]
        GRPC[gRPC Server]
        CW1[Celery Worker 1]
        CW2[Celery Worker 2]
        CWN[Celery Worker N]
    end

    subgraph "Edge"
        TK[Traefik v3]
        FE[React App]
    end

    TK --> FE
    TK --> BE
    BE --> PG
    BE --> RD
    BE --> RMQ
    GRPC --> PG
    RMQ --> CW1
    RMQ --> CW2
    RMQ --> CWN
    CW1 --> RD
    CW2 --> RD

Everything runs in Docker. The docker-compose.yml defines the full stack. database, cache, message broker, API, workers, and reverse proxy. Spin up the whole platform with docker compose up -d.

Key infrastructure decisions:

  • RabbitMQ over Redis for queuing. Celery supports both, but RabbitMQ gives me proper dead letter queues, message persistence, and better visibility into job state. For compute-heavy tasks that can take minutes, I wanted reliability over simplicity.
  • Traefik for routing. auto-discovery via Docker labels, built-in SSL, and a dashboard. No nginx config files to maintain.
  • Separate gRPC server. the equity engine is CPU-bound. Keeping it behind gRPC lets me scale compute independently from the REST API.

The Solver: Counterfactual Regret Minimization

Beyond raw equity calculation, PLOScope includes a GTO (Game Theory Optimal) solver using Counterfactual Regret Minimization (CFR). This is the same algorithmic family that powers commercial poker solvers.

The short version: CFR plays the game against itself millions of times, tracking the “regret” of not having chosen each possible action. Over iterations, it converges on a strategy where no player can improve by deviating. the Nash equilibrium.

For double board bomb pots, the game tree is enormous. Four cards per player, two boards, multiple betting streets. The solver uses:

  • Abstraction. grouping similar hands into “buckets” to reduce the game tree
  • Monte Carlo CFR. sampling game tree paths instead of traversing the full tree
  • Early termination. stopping when strategy changes fall below a threshold

This is the computationally expensive part. solver jobs go through the Celery queue and can run for minutes or longer depending on the complexity of the spot.

Data Model

The core tables are straightforward:

erDiagram
    USERS ||--o{ JOBS : submits
    USERS ||--o{ SPOTS : saves
    USERS ||--o{ HAND_HISTORIES : uploads

    USERS {
        uuid id PK
        string email
        string username
        string subscription_tier
    }

    JOBS {
        uuid id PK
        uuid user_id FK
        string job_type
        string status
        json input_data
        json result_data
    }

    SPOTS {
        uuid id PK
        uuid user_id FK
        string name
        json board_top
        json board_bottom
        json hands
    }

    HAND_HISTORIES {
        uuid id PK
        uuid user_id FK
        string filename
        text raw_content
        json parsed_data
    }

Jobs store both input and output as JSON. the schema for a “spot” is complex enough that a rigid relational model would fight me more than help. PostgreSQL’s JSONB handles this well.

What I’d Do Differently

PLOScope was built under a three-week deadline with a tournament at the finish line. That shaped some decisions I’d revisit:

  • The frontend needs work. It’s functional but not polished. A card selection UI deserves more design attention than I gave it.
  • The solver could be faster. Python + NumPy is decent for equity calculation, but a Rust or C++ core for the CFR solver would unlock much better performance.
  • Better hand history parsing. I started building import support for major poker site formats but only got one working before Vegas.

Current Status

PLOScope is open source under the PolyForm Noncommercial License. The orchestration repo ties together all the microservices. I’ve moved on to other projects, but the codebase is there for anyone who wants to pick it up or learn from the architecture.

The project taught me something I keep relearning: the best side projects solve a problem you actually have. I didn’t set out to build a poker platform. I set out to cash a tournament. The platform was just the tool that got me there.


Read Part 1 for the full tournament story. the road trip, the preparation, and the hand that mattered.