A browser-first AI poker benchmark. Seat Jev and OpenAI-compatible models at the same no-limit Texas Hold'em table, watch every card and decision as a spectator, and let the tournament run autonomously until one model wins.
See the codeA browser-first AI poker benchmark. Seat Jev and OpenAI-compatible models at the same no-limit Texas Hold'em table, watch every card and decision as a spectator, and let the tournament run autonomously until one model wins.
| Live app | https://pokertools-arena.github.io/ |
| Source | https://github.com/pokertools-arena/pokertools-arena.github.io |
| Package | npx pokertools-arena |
| Runtime | Node.js ≥ 24 for tooling; any modern browser for the app |
Current release: 0.18.1. The npm package now includes the shared
.envparser required by its launcher, with a packed-artifact smoke test preventing futurenpx pokertools-arenamodule-resolution failures.
https://github.com/user-attachments/assets/64d63da2-d904-4b63-a327-c5fa816e6be9
.env bootstrapText-model benchmarks are usually static question sets. Poker is a better stress test: adversarial, sequential, incomplete-information and unforgiving of bad reasoning. pokertools-arena turns that into an observable, reproducible benchmark.
Models only select from arena-generated legal action families and sizes, and every chosen move is re-validated by the engine before it is applied.
| Area | What you get |
|---|---|
| Table | 2–10 seats, no-limit Hold'em tournaments, rising blinds, antes, time banks, elimination and podium flow |
| Connections | Generic OpenAI-compatible base URL, OpenRouter preset, TypeSafe System One preset |
| Protocols | Tool call, strict JSON Schema, prompt JSON, Jev Decisions, native Jev |
| Fairness | One canonical DecisionState per decision, fail-closed hole-card masking, deterministic legal actions |
| Hierarchy | Two-stage family → size decisions shared by every model and adapter |
| Observability | Live decision panel, streamed model reasoning, typed telemetry, latency/error counters, JSONL export |
| Replay | Per-decision snapshots with hero cards, board, stacks, public history, legal menu |
| Sharing | Local 1080×1350 PNG replay cards, table-only WebM recording, native share sheet |
| Benchmarks | In-app sanity suite plus offline and real-API diagnostics with a paired fixed-state corpus |
Prerequisites: Node.js ≥ 24, a modern browser with Web Crypto (Chromium for table-only recording), and an OpenAI-compatible endpoint.
npx pokertools-arena
The CLI starts a tiny local static server and opens the browser. It exists only to avoid file:// restrictions. Poker state, orchestration, model calls, logs and API keys stay in the browser.
| Flag | Default | Purpose |
|---|---|---|
--port <n> | 4173 | Port to serve on (auto-increments if busy). |
--host <addr> | 127.0.0.1 | Interface to bind. |
--chrome | off | Open Google Chrome specifically. |
--no-open | off | Do not launch a browser. |
--no-env | off | Ignore .env bootstrap. |
--autostart | off | Start the configured tournament after launch. |
--starting-stack <n> | 3000 | Override the chip stack for .env seats. |
--max-decisions <n> | unlimited | Stop after this many model decisions. |
--help, -h | — | Print usage. |
npm install
npm run build
Then open dist/pokertools-arena.html. The build bundles the PokerTools browser engine locally; no runtime CDN is used.
The canonical deployment is https://pokertools-arena.github.io/. Enable Settings → Pages → Source: GitHub Actions once; .github/workflows/pages.yml builds and deploys dist/ on every push to main.
The table is the player editor. A fresh launch shows empty seats around the felt.
Defaults and validation limits live in src/config/arena-config.js. A host page may override any value before loading app.js by assigning a partial object to window.__POKERTOOLS_ARENA_CONFIG__; the arena deep-merges it with the documented defaults.
.env bootstrapThe launcher reads .env from the current working directory (falling back to the package root) and injects only supported fields into page memory. Disable with --no-env. A real user .env is never moved, printed or archived.
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY="sk-or-v1-..."
OPENAI_PLAYER1=google/gemma-4-26b-a4b-it
OPENAI_PLAYER2=qwen/qwen3.8-flash
OPENAI_PLAYER3=typesafe/jev-1.13
OPENAI_STARTING_STACK=3000
OPENAI_AUTOSTART=0
OPENAI_MAX_DECISIONS=0
| Variable | Notes |
|---|---|
OPENAI_BASE_URL | Defaults to https://api.openai.com/v1; OpenRouter URLs are auto-detected. |
OPENAI_API_KEY | Optional for local/unauthenticated endpoints. |
OPENAI_PLAYER1 … OPENAI_PLAYER10 | Each value seats one model; Jev models auto-route to Decisions on OpenRouter. |
OPENAI_STARTING_STACK | Chip stack per seat (minimum 100, default 3000). |
OPENAI_AUTOSTART | Set to 1, true, yes, or on to start automatically. |
OPENAI_MAX_DECISIONS | Optional tournament decision limit (0 means unlimited). |
See .env.example.
Two tournament-wide settings keep every model on equal footing:
heroHand classification from @pokertools/evaluator.heroHand; each model infers hand strength from raw cards.They are separate benchmark tracks. Public reports never aggregate them into a single model score.
Decision architecture is also tournament-wide. hierarchical (default) asks for an action family, then a validated size for BET/RAISE. flat is retained only as a diagnostic baseline. Representation is canonical_json, compact_json or markdown; all seats receive identical semantics.
Every seat receives the same canonical information policy: its own hole cards, the public board/pot/stacks/positions/current bets, the same deterministic legal actions, current-hand public history, last eight public hands, and identical deterministic public opponent statistics. Opponent hole cards, other models' reasoning, prior Jev probabilities and provider metadata are never exposed.
Player masking is fail-closed. All-in players count as remaining until a completed hand eliminates them.
The full contract, action-family rules, sizing buckets, shared clock and fairness assertions are documented in docs/architecture/CONTEXT.md.
The benchmark methodology is built around a paired fixed-state corpus:
experimentSeed;fragmentation_flip_rate measures how often the broad family changes when only the number of same-family size choices changes;pokerDecisions, familyModelCalls, sizingModelCalls, totalModelCalls, httpRequests, httpRetries, …) and one machine-readable summary is the single source of truth for every report.A hierarchical aggressive poker decision is 1 poker decision, 2 model calls and 2+ HTTP requests if retries occur. Aggression is not a goal; the goal is symmetric representation, reproducibility, correctness and interpretable statistics.
Real tournament A/B is end-to-end behavioral validation, not a controlled architecture comparison, because actions change future states.
npm test # build + release checks + offline unit + integration
npm run test:unit # corpus, hierarchy/fairness, methodology, sizing, paired, archive, report
npm run test:integration # @pokertools/engine/browser + launcher .env bootstrap
npm run test:diagnostics:dry
npm run test:real # real API paired corpus + diagnostics (uses .env)
npm run test:tournament-ab # real flat-vs-hierarchical tournament (uses .env)
npm run report:diagnostics # build logs/release-<version>/summary.json + summary.md
npm run release:archive # source archive without secrets/logs/dist
npm test is fully offline apart from dependency installation. Real-API diagnostics are manual or secret-gated in CI. See tests/README.md.
pokertools-arena/
├── README.md · LICENSE · package.json · build.mjs
├── bin/pokertools-arena.mjs local static launcher + .env bootstrap
├── src/ browser application source
│ ├── index.html · app.js · styles.css
│ ├── config/arena-config.js defaults, limits, presets, host overrides
│ ├── lib/decision-core.js canonical DOM-free decision core
│ ├── benchmark/scenarios.js in-app sanity scenarios
│ ├── env/arena-env.js .env placeholder injected by the launcher
│ ├── shims/crypto.cjs Web Crypto shim for the PokerTools browser entry
│ └── assets/ favicon and social image
├── tools/
│ ├── diagnostics/ corpus, harness, stats, counters, paired, report
│ └── release/archive.mjs source archive builder
├── tests/
│ ├── unit/ · integration/ · real/ · analysis/
│ └── release-check.mjs
├── docs/
│ ├── architecture/CONTEXT.md
│ ├── releases/RELEASE_NOTES.md
│ ├── diagnostics/SUMMARY.md + summary.json
│ ├── verification/0.3.x.md
│ └── legal/THIRD_PARTY_NOTICES.md
└── .github/workflows/ CI, GitHub Pages, npm publish, manual real diagnostics
Generated and gitignored: dist/, pokertools-arena.html, logs/, node_modules/.
docs/architecture/CONTEXT.mddocs/releases/RELEASE_NOTES.mddocs/diagnostics/SUMMARY.mddocs/diagnostics/summary.jsontests/README.mddocs/legal/THIRD_PARTY_NOTICES.md.env payload only to loopback requests whenever it contains a configured API key.MIT — see LICENSE.
50 commits
JavaScript
82.1%
HTML
10.0%
CSS
7.9%
A browser-first AI poker benchmark. Seat Jev and OpenAI-compatible models at the same no-limit Texas Hold'em table, watch every card and decision as a spectator, and let the tournament run autonomously until one model wins.
See the codeA browser-first AI poker benchmark. Seat Jev and OpenAI-compatible models at the same no-limit Texas Hold'em table, watch every card and decision as a spectator, and let the tournament run autonomously until one model wins.
| Live app | https://pokertools-arena.github.io/ |
| Source | https://github.com/pokertools-arena/pokertools-arena.github.io |
| Package | npx pokertools-arena |
| Runtime | Node.js ≥ 24 for tooling; any modern browser for the app |
Current release: 0.18.1. The npm package now includes the shared
.envparser required by its launcher, with a packed-artifact smoke test preventing futurenpx pokertools-arenamodule-resolution failures.
https://github.com/user-attachments/assets/64d63da2-d904-4b63-a327-c5fa816e6be9
.env bootstrapText-model benchmarks are usually static question sets. Poker is a better stress test: adversarial, sequential, incomplete-information and unforgiving of bad reasoning. pokertools-arena turns that into an observable, reproducible benchmark.
Models only select from arena-generated legal action families and sizes, and every chosen move is re-validated by the engine before it is applied.
| Area | What you get |
|---|---|
| Table | 2–10 seats, no-limit Hold'em tournaments, rising blinds, antes, time banks, elimination and podium flow |
| Connections | Generic OpenAI-compatible base URL, OpenRouter preset, TypeSafe System One preset |
| Protocols | Tool call, strict JSON Schema, prompt JSON, Jev Decisions, native Jev |
| Fairness | One canonical DecisionState per decision, fail-closed hole-card masking, deterministic legal actions |
| Hierarchy | Two-stage family → size decisions shared by every model and adapter |
| Observability | Live decision panel, streamed model reasoning, typed telemetry, latency/error counters, JSONL export |
| Replay | Per-decision snapshots with hero cards, board, stacks, public history, legal menu |
| Sharing | Local 1080×1350 PNG replay cards, table-only WebM recording, native share sheet |
| Benchmarks | In-app sanity suite plus offline and real-API diagnostics with a paired fixed-state corpus |
Prerequisites: Node.js ≥ 24, a modern browser with Web Crypto (Chromium for table-only recording), and an OpenAI-compatible endpoint.
npx pokertools-arena
The CLI starts a tiny local static server and opens the browser. It exists only to avoid file:// restrictions. Poker state, orchestration, model calls, logs and API keys stay in the browser.
| Flag | Default | Purpose |
|---|---|---|
--port <n> | 4173 | Port to serve on (auto-increments if busy). |
--host <addr> | 127.0.0.1 | Interface to bind. |
--chrome | off | Open Google Chrome specifically. |
--no-open | off | Do not launch a browser. |
--no-env | off | Ignore .env bootstrap. |
--autostart | off | Start the configured tournament after launch. |
--starting-stack <n> | 3000 | Override the chip stack for .env seats. |
--max-decisions <n> | unlimited | Stop after this many model decisions. |
--help, -h | — | Print usage. |
npm install
npm run build
Then open dist/pokertools-arena.html. The build bundles the PokerTools browser engine locally; no runtime CDN is used.
The canonical deployment is https://pokertools-arena.github.io/. Enable Settings → Pages → Source: GitHub Actions once; .github/workflows/pages.yml builds and deploys dist/ on every push to main.
The table is the player editor. A fresh launch shows empty seats around the felt.
Defaults and validation limits live in src/config/arena-config.js. A host page may override any value before loading app.js by assigning a partial object to window.__POKERTOOLS_ARENA_CONFIG__; the arena deep-merges it with the documented defaults.
.env bootstrapThe launcher reads .env from the current working directory (falling back to the package root) and injects only supported fields into page memory. Disable with --no-env. A real user .env is never moved, printed or archived.
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY="sk-or-v1-..."
OPENAI_PLAYER1=google/gemma-4-26b-a4b-it
OPENAI_PLAYER2=qwen/qwen3.8-flash
OPENAI_PLAYER3=typesafe/jev-1.13
OPENAI_STARTING_STACK=3000
OPENAI_AUTOSTART=0
OPENAI_MAX_DECISIONS=0
| Variable | Notes |
|---|---|
OPENAI_BASE_URL | Defaults to https://api.openai.com/v1; OpenRouter URLs are auto-detected. |
OPENAI_API_KEY | Optional for local/unauthenticated endpoints. |
OPENAI_PLAYER1 … OPENAI_PLAYER10 | Each value seats one model; Jev models auto-route to Decisions on OpenRouter. |
OPENAI_STARTING_STACK | Chip stack per seat (minimum 100, default 3000). |
OPENAI_AUTOSTART | Set to 1, true, yes, or on to start automatically. |
OPENAI_MAX_DECISIONS | Optional tournament decision limit (0 means unlimited). |
See .env.example.
Two tournament-wide settings keep every model on equal footing:
heroHand classification from @pokertools/evaluator.heroHand; each model infers hand strength from raw cards.They are separate benchmark tracks. Public reports never aggregate them into a single model score.
Decision architecture is also tournament-wide. hierarchical (default) asks for an action family, then a validated size for BET/RAISE. flat is retained only as a diagnostic baseline. Representation is canonical_json, compact_json or markdown; all seats receive identical semantics.
Every seat receives the same canonical information policy: its own hole cards, the public board/pot/stacks/positions/current bets, the same deterministic legal actions, current-hand public history, last eight public hands, and identical deterministic public opponent statistics. Opponent hole cards, other models' reasoning, prior Jev probabilities and provider metadata are never exposed.
Player masking is fail-closed. All-in players count as remaining until a completed hand eliminates them.
The full contract, action-family rules, sizing buckets, shared clock and fairness assertions are documented in docs/architecture/CONTEXT.md.
The benchmark methodology is built around a paired fixed-state corpus:
experimentSeed;fragmentation_flip_rate measures how often the broad family changes when only the number of same-family size choices changes;pokerDecisions, familyModelCalls, sizingModelCalls, totalModelCalls, httpRequests, httpRetries, …) and one machine-readable summary is the single source of truth for every report.A hierarchical aggressive poker decision is 1 poker decision, 2 model calls and 2+ HTTP requests if retries occur. Aggression is not a goal; the goal is symmetric representation, reproducibility, correctness and interpretable statistics.
Real tournament A/B is end-to-end behavioral validation, not a controlled architecture comparison, because actions change future states.
npm test # build + release checks + offline unit + integration
npm run test:unit # corpus, hierarchy/fairness, methodology, sizing, paired, archive, report
npm run test:integration # @pokertools/engine/browser + launcher .env bootstrap
npm run test:diagnostics:dry
npm run test:real # real API paired corpus + diagnostics (uses .env)
npm run test:tournament-ab # real flat-vs-hierarchical tournament (uses .env)
npm run report:diagnostics # build logs/release-<version>/summary.json + summary.md
npm run release:archive # source archive without secrets/logs/dist
npm test is fully offline apart from dependency installation. Real-API diagnostics are manual or secret-gated in CI. See tests/README.md.
pokertools-arena/
├── README.md · LICENSE · package.json · build.mjs
├── bin/pokertools-arena.mjs local static launcher + .env bootstrap
├── src/ browser application source
│ ├── index.html · app.js · styles.css
│ ├── config/arena-config.js defaults, limits, presets, host overrides
│ ├── lib/decision-core.js canonical DOM-free decision core
│ ├── benchmark/scenarios.js in-app sanity scenarios
│ ├── env/arena-env.js .env placeholder injected by the launcher
│ ├── shims/crypto.cjs Web Crypto shim for the PokerTools browser entry
│ └── assets/ favicon and social image
├── tools/
│ ├── diagnostics/ corpus, harness, stats, counters, paired, report
│ └── release/archive.mjs source archive builder
├── tests/
│ ├── unit/ · integration/ · real/ · analysis/
│ └── release-check.mjs
├── docs/
│ ├── architecture/CONTEXT.md
│ ├── releases/RELEASE_NOTES.md
│ ├── diagnostics/SUMMARY.md + summary.json
│ ├── verification/0.3.x.md
│ └── legal/THIRD_PARTY_NOTICES.md
└── .github/workflows/ CI, GitHub Pages, npm publish, manual real diagnostics
Generated and gitignored: dist/, pokertools-arena.html, logs/, node_modules/.
docs/architecture/CONTEXT.mddocs/releases/RELEASE_NOTES.mddocs/diagnostics/SUMMARY.mddocs/diagnostics/summary.jsontests/README.mddocs/legal/THIRD_PARTY_NOTICES.md.env payload only to loopback requests whenever it contains a configured API key.MIT — see LICENSE.
50 commits
JavaScript
82.1%
HTML
10.0%
CSS
7.9%