惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Vercel News
Vercel News
Microsoft Azure Blog
Microsoft Azure Blog
爱范儿
爱范儿
N
Netflix TechBlog - Medium
Google DeepMind News
Google DeepMind News
H
Help Net Security
罗磊的独立博客
The Cloudflare Blog
J
Java Code Geeks
博客园 - 叶小钗
I
InfoQ
B
Blog
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
腾讯CDC
月光博客
月光博客
博客园_首页
雷峰网
雷峰网
M
MIT News - Artificial intelligence
博客园 - 【当耐特】
美团技术团队
T
The Blog of Author Tim Ferriss
博客园 - 司徒正美

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How I Built a Pluribus-Style Poker AI From Scratch
Griffin Henning · 2026-06-24 · via DEV Community

Why poker is hard

Chess and Go are perfect information games, both players see everything. The challenge is purely computational.

Poker is an imperfect information game. You can't see your opponent's cards. Every decision has to account for the full range of hands they might hold.

Standard minimax doesn't work. You can't evaluate a position without knowing your opponent's hand.

The solution is Nash equilibrium, a strategy where neither player can improve their EV by unilaterally changing what they do. In poker this is called GTO (Game-Theoretically Optimal). Unexploitable by definition.


Stage 1: Counterfactual Regret Minimization

CFR (Zinkevich et al., 2007) is the algorithm that made strong poker AI possible.

The intuition: Play against yourself repeatedly. After each game, ask: “how much better would I have done if I'd always taken a different action?” That's regret.

Update your strategy: play actions proportional to their cumulative positive regret. Repeat thousands of times. The time-average of your strategies converges to Nash equilibrium.

I implemented this on “Leduc Hold'em” ‚ a 6-card toy game with 216 information sets, the standard poker AI research testbed.

def get_strategy(self, reach_prob: float) -> list[float]:
    pos = [max(r, 0.0) for r in self.regrets]
    total = sum(pos)
    strat = [p / total for p in pos] if total > 0 else [1/n] * n
    # Accumulate average strategy (this is what converges to Nash)
    for i in range(n):
        self.strat_sum[i] += reach_prob * strat[i]
    return strat

After 10,000 iterations (2.2 seconds):

  • King raises 99% ‚ correct, strongest hand
  • Jack plays a mixed strategy‚ correct, must be unpredictable to stay unexploitable

That last point surprises people. GTO isn't about always making the "right" move. It's about being unpredictable enough that no opponent strategy can consistently beat you.


Stage 2: Monte Carlo CFR

Full NLHE has ~10¹⁶⁰ game states. Full tree traversal every iteration is impossible.

MCCFR samples a subset each iteration. I used external sampling:

  • Traverser nodes: explore all actions
  • Opponent nodes: sample one action from their strategy
  • Result: unbiased value estimates, tractable computation
if player == traverser:
    # Explore ALL actions, update regrets
    for action in actions:
        v = traverse(next_state, traverser)
        action_values[action] = v
    update_regrets(action_values)
else:
    # SAMPLE one opponent action
    action = sample_from_strategy(strategy)
    return traverse(next_state, traverser)

On Leduc Hold'em: MCCFR converged to the same equilibrium as vanilla CFR at 1.9x the speed.


Stage 3: Card Abstraction

Even with MCCFR, full NLHE has too many states. Solution: group similar hands into buckets.

The naive approach clusters by average equity, wrong in an important way.

A flopped flush draw and a top pair can have identical average equity but completely different equity distributions over future runouts. The flush draw is either far ahead or far behind. The made hand is consistently ahead. GTO strategy treats these differently.

The right metric: Earth Mover's Distance between equity histograms.

def emd(hist_a: np.ndarray, hist_b: np.ndarray) -> float:
    """Wasserstein-1 distance — captures distribution shape, not just mean."""
    cdf_a = np.cumsum(hist_a)
    cdf_b = np.cumsum(hist_b)
    return float(np.sum(np.abs(cdf_a - cdf_b)))

Abstraction scheme:

  • Preflop: 169 canonical hands ‚Üí 8 equity percentile buckets
  • Flop: EMD clustering ‚Üí 12 clusters
  • Turn: EMD clustering ‚Üí 12 clusters
  • River: exact strength percentile ‚Üí 8 bins

Optimization that halved computation: compute equity histograms for both players simultaneously from the same random rollouts, rather than running separate Monte Carlo samples.


Stage 4: Deep CFR

Still too many information sets for a hash table. Deep CFR (Brown & Sandholm, 2019) replaces the table with neural networks.

Two networks per player:

class AdvantageNetwork(nn.Module):
    """Approximates cumulative counterfactual regret per action."""
    def regret_matching(self, features):
        advantages = self.forward(features)
        pos = F.relu(advantages)
        total = pos.sum(dim=-1, keepdim=True)
        uniform = torch.ones_like(advantages) / n
        return torch.where(total > 1e-6, pos / total, uniform)

class StrategyNetwork(nn.Module):
    """Approximates the average strategy — this converges to Nash."""
    def forward(self, x):
        return F.softmax(super().forward(x), dim=-1)

Feature encoding (373 dimensions):

  • 104: hole cards (2 √ó 52 one-hot)
  • 260: board cards (5 slots √ó 52 one-hot)
  • 4: street one-hot
  • 5: normalized scalars (pot, stacks, to-call, raises)

Reservoir buffers ensure all past iterations are represented in training, preventing catastrophic forgetting without unbounded memory.

The biggest performance win: keeping networks in eval() mode during traversal rather than toggling per inference.

# Before: called eval() on every inference ‚ 61ms/traversal
model.eval()
with torch.no_grad():
    output = model(x)

# After: set eval() once before traversal loop ‚ 10ms/traversal
player.set_inference_mode()  # called once
# ... thousands of traversals ...

One line of code. 6x speedup. Profile before you optimize.


Stage 5: Real-Time Search

The blueprint strategy from Deep CFR knows a lot but uses coarse abstractions. Real-time search fixes this.

At each decision point:

  1. Build a local game tree rooted at the current state
  2. Run MCCFR in that subtree for a fixed iteration budget
  3. At leaf nodes, query the blueprint for value estimates
  4. Return the search-refined strategy
def solve(self, gs: GameState, player: int) -> dict:
    self.nodes = {}  # Fresh local tree per decision
    t_start = time.time()

    while self._iters < self.config.n_iters:
        if (time.time() - t_start) * 1000 >= self.config.time_limit_ms:
            break
        self._traverse(gs, self._iters % 2, depth=1)
        self._iters += 1

    return self._root_node().get_avg_strategy(actions)

Blueprint bootstrapping blends local and blueprint strategies early in search for stability:

blend = min(self._iters / self.config.n_iters, 1.0)
strat = (1 - blend) * blueprint_strat + blend * local_strat

Average decision time: 75ms on CPU.


Results

300 duplicate hand pairs (600 total hands per matchup). Duplicate scoring controls for card luck.

Matchup mBB/hand Significant
Blueprint vs Random +28,403 ‚úì
Search vs Random +28,134 ‚úì
Search vs Blueprint +31,798 ‚úì

Search consistently outperforms blueprint-only play. This is the core empirical claim of Pluribus ‚ validated.


How it compares to Pluribus

This project Pluribus
Players 2 (heads-up) 6
Traversals ~50,000 12.4M
Hardware Single CPU 64-core CPU
Training ~30 min ~8 days

Same architecture. Scale is the difference.


Three things I'd do differently

1. Profile earlier. The 6x traversal speedup from eval mode was sitting there the whole time. I spent days on algorithmic optimizations before finding it.

2. Start with finer bet abstraction. Five bet sizes is enough to demonstrate the technique but too coarse for real strategic depth. Pluribus used 14. The strategy changes meaningfully.

3. Build the evaluation framework first. I ran hundreds of training iterations before having reliable exploitability metrics. Convergence looks different than you expect, EV oscillating near zero is not the same as converging to Nash.


The codebase

GitHub: github.com/griff-ui/poker-ai

Five stages, 40 files, 27 tests passing, full documentation. MIT licensed ‚use it for research, study, or as the foundation for your own solver.

Live demo: griff-ui.github.io/poker-ai

Browser-based hand analyzer, select cards, set game state, see GTO strategy frequencies. No Python required.


References