pokertools-arena 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/bin/pokertools-arena.mjs +197 -0
- package/dist/.nojekyll +0 -0
- package/dist/app.js +118427 -0
- package/dist/arena-env.js +2 -0
- package/dist/favicon.svg +4 -0
- package/dist/index.html +382 -0
- package/dist/og-image.png +0 -0
- package/dist/og-image.svg +190 -0
- package/dist/pokertools-arena.html +121106 -0
- package/dist/styles.css +2289 -0
- package/docs/architecture/CONTEXT.md +202 -0
- package/docs/diagnostics/SUMMARY.md +102 -0
- package/docs/diagnostics/summary.json +337 -0
- package/docs/legal/THIRD_PARTY_NOTICES.md +15 -0
- package/docs/releases/RELEASE_NOTES.md +171 -0
- package/docs/verification/0.3.x.md +250 -0
- package/package.json +77 -0
- package/src/app.js +2458 -0
- package/src/assets/favicon.svg +4 -0
- package/src/assets/og-image.png +0 -0
- package/src/assets/og-image.svg +190 -0
- package/src/benchmark/scenarios.js +108 -0
- package/src/env/arena-env.js +2 -0
- package/src/index.html +382 -0
- package/src/lib/decision-core.js +1288 -0
- package/src/shims/crypto.cjs +19 -0
- package/src/styles.css +2289 -0
package/.env.example
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Optional local/CDP real-model bootstrap. Never commit real keys.
|
|
2
|
+
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
3
|
+
OPENAI_API_KEY="sk-or-v1-..."
|
|
4
|
+
OPENAI_PLAYER1=google/gemma-4-26b-a4b-it
|
|
5
|
+
OPENAI_PLAYER2=qwen/qwen3.8-flash
|
|
6
|
+
OPENAI_PLAYER3=typesafe/jev-1.13
|
|
7
|
+
# OPENAI_PLAYER4=...
|
|
8
|
+
OPENAI_STARTING_STACK=10000
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Art
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# pokertools-arena
|
|
2
|
+
|
|
3
|
+
**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.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/pokertools-arena/pokertools-arena.github.io/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/pokertools-arena/pokertools-arena.github.io/actions/workflows/pages.yml)
|
|
7
|
+
[](https://www.npmjs.com/package/pokertools-arena)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](./package.json)
|
|
10
|
+
|
|
11
|
+
[](https://pokertools-arena.github.io/)
|
|
12
|
+
|
|
13
|
+
| | |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| **Live app** | <https://pokertools-arena.github.io/> |
|
|
16
|
+
| **Source** | <https://github.com/pokertools-arena/pokertools-arena.github.io> |
|
|
17
|
+
| **Package** | `npx pokertools-arena` |
|
|
18
|
+
| **Runtime** | Node.js ≥ 24 for tooling; any modern browser for the app |
|
|
19
|
+
|
|
20
|
+
> **Current release: 0.4.0.** This is the methodology and reproducibility release: a paired fixed-state corpus, separated family/sizing correctness, fragmentation-invariance metrics with confidence intervals, standardized counters, and a reorganized repository with one source of truth per report.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Contents
|
|
25
|
+
|
|
26
|
+
- [Why pokertools-arena](#why-pokertools-arena)
|
|
27
|
+
- [Features](#features)
|
|
28
|
+
- [Quick start](#quick-start)
|
|
29
|
+
- [Configure the table](#configure-the-table)
|
|
30
|
+
- [`.env` bootstrap](#env-bootstrap)
|
|
31
|
+
- [Benchmark modes](#benchmark-modes)
|
|
32
|
+
- [Fairness](#fairness)
|
|
33
|
+
- [Methodology](#methodology)
|
|
34
|
+
- [Testing](#testing)
|
|
35
|
+
- [Repository layout](#repository-layout)
|
|
36
|
+
- [Documentation](#documentation)
|
|
37
|
+
- [Privacy and security](#privacy-and-security)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why pokertools-arena
|
|
43
|
+
|
|
44
|
+
Text-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.
|
|
45
|
+
|
|
46
|
+
- **Same information for every seat.** Poker legality, hole-card masking and public context are generated by the PokerTools engine, not by prompts.
|
|
47
|
+
- **Any OpenAI-compatible endpoint.** Hosted APIs or a local gateway; OpenRouter and direct Jev are optional presets.
|
|
48
|
+
- **Nothing to deploy.** The benchmark is fully client-side. There is no application server, database or backend API.
|
|
49
|
+
- **Everything is inspectable.** Watch live, replay any decision, export logs and replay PNGs.
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
| Area | What you get |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| **Table** | 2–10 seats, no-limit Hold'em tournaments, rising blinds, antes, time banks, elimination and podium flow |
|
|
58
|
+
| **Connections** | Generic OpenAI-compatible base URL, OpenRouter preset, TypeSafe System One preset |
|
|
59
|
+
| **Protocols** | Tool call, strict JSON Schema, prompt JSON, Jev Decisions, native Jev |
|
|
60
|
+
| **Fairness** | One canonical `DecisionState` per decision, fail-closed hole-card masking, deterministic legal actions |
|
|
61
|
+
| **Hierarchy** | Two-stage family → size decisions shared by every model and adapter |
|
|
62
|
+
| **Observability** | Live decision panel, typed telemetry, latency/error counters, JSONL export |
|
|
63
|
+
| **Replay** | Per-decision snapshots with hero cards, board, stacks, public history, legal menu |
|
|
64
|
+
| **Sharing** | Local 1080×1350 PNG replay cards, table-only WebM recording, native share sheet |
|
|
65
|
+
| **Benchmarks** | In-app sanity suite plus offline and real-API diagnostics with a paired fixed-state corpus |
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
Prerequisites: **Node.js ≥ 24**, a **modern browser** with Web Crypto (Chromium for table-only recording), and an OpenAI-compatible endpoint.
|
|
70
|
+
|
|
71
|
+
### Option A — npm launcher (recommended)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx pokertools-arena
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
| Flag | Default | Purpose |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `--port <n>` | `4173` | Port to serve on (auto-increments if busy). |
|
|
82
|
+
| `--host <addr>` | `127.0.0.1` | Interface to bind. |
|
|
83
|
+
| `--chrome` | off | Open Google Chrome specifically. |
|
|
84
|
+
| `--no-open` | off | Do not launch a browser. |
|
|
85
|
+
| `--no-env` | off | Ignore `.env` bootstrap. |
|
|
86
|
+
| `--help`, `-h` | — | Print usage. |
|
|
87
|
+
|
|
88
|
+
### Option B — single HTML file
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install
|
|
92
|
+
npm run build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then open `dist/pokertools-arena.html`. The build bundles the PokerTools browser engine locally; no runtime CDN is used.
|
|
96
|
+
|
|
97
|
+
### Option C — GitHub Pages
|
|
98
|
+
|
|
99
|
+
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`.
|
|
100
|
+
|
|
101
|
+
## Configure the table
|
|
102
|
+
|
|
103
|
+
The table **is** the player editor. A fresh launch shows empty seats around the felt.
|
|
104
|
+
|
|
105
|
+
1. Click a seat to add or edit a model.
|
|
106
|
+
2. Choose a connection, enter a model ID and pick a protocol.
|
|
107
|
+
3. Configure **Settings → Connections** (base URLs and keys) and **Settings → Tournament** (stack, blinds, clock, benchmark mode, architecture, representation).
|
|
108
|
+
4. Press **Start**. Only configured seats enter the tournament, and seat editing locks while it runs.
|
|
109
|
+
|
|
110
|
+
## `.env` bootstrap
|
|
111
|
+
|
|
112
|
+
The 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.
|
|
113
|
+
|
|
114
|
+
```dotenv
|
|
115
|
+
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
116
|
+
OPENAI_API_KEY="sk-or-v1-..."
|
|
117
|
+
OPENAI_PLAYER1=google/gemma-4-26b-a4b-it
|
|
118
|
+
OPENAI_PLAYER2=qwen/qwen3.8-flash
|
|
119
|
+
OPENAI_PLAYER3=typesafe/jev-1.13
|
|
120
|
+
OPENAI_STARTING_STACK=10000
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Variable | Notes |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `OPENAI_BASE_URL` | Defaults to `https://api.openai.com/v1`; OpenRouter URLs are auto-detected. |
|
|
126
|
+
| `OPENAI_API_KEY` | Optional for local/unauthenticated endpoints. |
|
|
127
|
+
| `OPENAI_PLAYER1` … `OPENAI_PLAYER10` | Each value seats one model; Jev models auto-route to Decisions on OpenRouter. |
|
|
128
|
+
| `OPENAI_STARTING_STACK` | Chip stack per seat (minimum 100, default 10000). |
|
|
129
|
+
|
|
130
|
+
See [`.env.example`](./.env.example).
|
|
131
|
+
|
|
132
|
+
## Benchmark modes
|
|
133
|
+
|
|
134
|
+
Two tournament-wide settings keep every model on equal footing:
|
|
135
|
+
|
|
136
|
+
- **Strategy** (default): every seat receives a deterministic `heroHand` classification from `@pokertools/evaluator`.
|
|
137
|
+
- **Raw cognition**: no `heroHand`; each model infers hand strength from raw cards.
|
|
138
|
+
|
|
139
|
+
They are **separate benchmark tracks**. Public reports never aggregate them into a single model score.
|
|
140
|
+
|
|
141
|
+
**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.
|
|
142
|
+
|
|
143
|
+
## Fairness
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
Player masking is **fail-closed**. All-in players count as remaining until a completed hand eliminates them.
|
|
148
|
+
|
|
149
|
+
The full contract, action-family rules, sizing buckets, shared clock and fairness assertions are documented in [`docs/architecture/CONTEXT.md`](./docs/architecture/CONTEXT.md).
|
|
150
|
+
|
|
151
|
+
## Methodology
|
|
152
|
+
|
|
153
|
+
The benchmark methodology is built around a **paired fixed-state corpus**:
|
|
154
|
+
|
|
155
|
+
- the exact same immutable state is used for flat and hierarchical, for every model, mode and representation;
|
|
156
|
+
- execution is deterministically interleaved with a recorded `experimentSeed`;
|
|
157
|
+
- **family accuracy** and **sizing accuracy (conditional on a correct family)** are reported separately from **final-action accuracy**;
|
|
158
|
+
- behavioral proportions carry sample size and Wilson 95% confidence intervals;
|
|
159
|
+
- **`fragmentation_flip_rate`** measures how often the broad family changes when only the number of same-family size choices changes;
|
|
160
|
+
- counters are standardized (`pokerDecisions`, `familyModelCalls`, `sizingModelCalls`, `totalModelCalls`, `httpRequests`, `httpRetries`, …) and one machine-readable summary is the single source of truth for every report.
|
|
161
|
+
|
|
162
|
+
> 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.
|
|
163
|
+
|
|
164
|
+
Real tournament A/B is **end-to-end behavioral validation**, not a controlled architecture comparison, because actions change future states.
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm test # build + release checks + offline unit + integration
|
|
170
|
+
npm run test:unit # corpus, hierarchy/fairness, methodology, sizing, paired, archive, report
|
|
171
|
+
npm run test:integration # @pokertools/engine/browser + launcher .env bootstrap
|
|
172
|
+
npm run test:diagnostics:dry
|
|
173
|
+
npm run test:real # real API paired corpus + diagnostics (uses .env)
|
|
174
|
+
npm run test:tournament-ab # real flat-vs-hierarchical tournament (uses .env)
|
|
175
|
+
npm run report:diagnostics # build logs/release-<version>/summary.json + summary.md
|
|
176
|
+
npm run release:archive # source archive without secrets/logs/dist
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`npm test` is fully offline apart from dependency installation. Real-API diagnostics are manual or secret-gated in CI. See [`tests/README.md`](./tests/README.md).
|
|
180
|
+
|
|
181
|
+
## Repository layout
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
pokertools-arena/
|
|
185
|
+
├── README.md · LICENSE · package.json · build.mjs
|
|
186
|
+
├── bin/pokertools-arena.mjs local static launcher + .env bootstrap
|
|
187
|
+
├── src/ browser application source
|
|
188
|
+
│ ├── index.html · app.js · styles.css
|
|
189
|
+
│ ├── lib/decision-core.js canonical DOM-free decision core
|
|
190
|
+
│ ├── benchmark/scenarios.js in-app sanity scenarios
|
|
191
|
+
│ ├── env/arena-env.js .env placeholder injected by the launcher
|
|
192
|
+
│ ├── shims/crypto.cjs Web Crypto shim for the PokerTools browser entry
|
|
193
|
+
│ └── assets/ favicon and social image
|
|
194
|
+
├── tools/
|
|
195
|
+
│ ├── diagnostics/ corpus, harness, stats, counters, paired, report
|
|
196
|
+
│ └── release/archive.mjs source archive builder
|
|
197
|
+
├── tests/
|
|
198
|
+
│ ├── unit/ · integration/ · real/ · analysis/
|
|
199
|
+
│ └── release-check.mjs
|
|
200
|
+
├── docs/
|
|
201
|
+
│ ├── architecture/CONTEXT.md
|
|
202
|
+
│ ├── releases/RELEASE_NOTES.md
|
|
203
|
+
│ ├── diagnostics/SUMMARY.md + summary.json
|
|
204
|
+
│ ├── verification/0.3.x.md
|
|
205
|
+
│ └── legal/THIRD_PARTY_NOTICES.md
|
|
206
|
+
└── .github/workflows/ CI, GitHub Pages, npm publish, manual real diagnostics
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Generated and gitignored: `dist/`, `pokertools-arena.html`, `logs/`, `node_modules/`.
|
|
210
|
+
|
|
211
|
+
## Documentation
|
|
212
|
+
|
|
213
|
+
- Architecture and invariants → [`docs/architecture/CONTEXT.md`](./docs/architecture/CONTEXT.md)
|
|
214
|
+
- Chronological release history → [`docs/releases/RELEASE_NOTES.md`](./docs/releases/RELEASE_NOTES.md)
|
|
215
|
+
- Benchmark methodology and results → [`docs/diagnostics/SUMMARY.md`](./docs/diagnostics/SUMMARY.md)
|
|
216
|
+
- Machine-readable release summary → [`docs/diagnostics/summary.json`](./docs/diagnostics/summary.json)
|
|
217
|
+
- Test suites → [`tests/README.md`](./tests/README.md)
|
|
218
|
+
- Third-party notices → [`docs/legal/THIRD_PARTY_NOTICES.md`](./docs/legal/THIRD_PARTY_NOTICES.md)
|
|
219
|
+
|
|
220
|
+
## Privacy and security
|
|
221
|
+
|
|
222
|
+
- API keys live only in current page memory and are excluded from saved setup state, tournament snapshots, exported logs and release archives.
|
|
223
|
+
- The launcher serves the `.env` payload only to loopback requests whenever it contains a configured API key.
|
|
224
|
+
- Models never receive hidden information and never construct engine commands.
|
|
225
|
+
- Replay images and recordings are produced locally; nothing is uploaded.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT — see [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { extname, dirname, join, normalize, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
|
+
const root = join(packageRoot, 'dist');
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const valueAfter = (flag, fallback) => {
|
|
12
|
+
const index = args.indexOf(flag);
|
|
13
|
+
return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
|
|
14
|
+
};
|
|
15
|
+
const host = valueAfter('--host', '127.0.0.1');
|
|
16
|
+
const requestedPortRaw = valueAfter('--port', '4173');
|
|
17
|
+
const requestedPort = requestedPortRaw === '0' ? 0 : (Number(requestedPortRaw) || 4173);
|
|
18
|
+
const shouldOpen = !args.includes('--no-open');
|
|
19
|
+
const help = args.includes('--help') || args.includes('-h');
|
|
20
|
+
const useEnv = !args.includes('--no-env');
|
|
21
|
+
const useChrome = args.includes('--chrome');
|
|
22
|
+
const envOverrides = {
|
|
23
|
+
OPENAI_STARTING_STACK: valueAfter('--starting-stack', null),
|
|
24
|
+
OPENAI_MAX_DECISIONS: valueAfter('--max-decisions', null),
|
|
25
|
+
OPENAI_AUTOSTART: args.includes('--autostart') ? '1' : null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function parseDotEnv(text) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const rawLine of String(text || '').split(/\r?\n/)) {
|
|
31
|
+
let line = rawLine.trim();
|
|
32
|
+
if (!line || line.startsWith('#')) continue;
|
|
33
|
+
if (line.startsWith('export ')) line = line.slice(7).trim();
|
|
34
|
+
const eq = line.indexOf('=');
|
|
35
|
+
if (eq <= 0) continue;
|
|
36
|
+
const key = line.slice(0, eq).trim();
|
|
37
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
38
|
+
let value = line.slice(eq + 1).trim();
|
|
39
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
40
|
+
const quote = value[0];
|
|
41
|
+
value = value.slice(1, -1);
|
|
42
|
+
if (quote === '"') value = value.replace(/\n/g, '\n').replace(/\r/g, '\r').replace(/\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
43
|
+
} else {
|
|
44
|
+
value = value.replace(/\s+#.*$/, '').trim();
|
|
45
|
+
}
|
|
46
|
+
out[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadArenaEnv() {
|
|
52
|
+
if (!useEnv) return { source: null, values: {} };
|
|
53
|
+
const candidates = [resolve(process.cwd(), '.env'), join(packageRoot, '.env')];
|
|
54
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
55
|
+
try {
|
|
56
|
+
const values = parseDotEnv(await readFile(candidate, 'utf8'));
|
|
57
|
+
return { source: candidate, values };
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
return { source: null, values: {} };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function envBootstrap(values, overrides = {}) {
|
|
64
|
+
const baseUrl = String(values.OPENAI_BASE_URL || process.env.OPENAI_BASE_URL || '').trim();
|
|
65
|
+
const apiKey = String(values.OPENAI_API_KEY || process.env.OPENAI_API_KEY || '').trim();
|
|
66
|
+
const models = [];
|
|
67
|
+
for (let i = 1; i <= 10; i++) {
|
|
68
|
+
const model = String(values[`OPENAI_PLAYER${i}`] || process.env[`OPENAI_PLAYER${i}`] || '').trim();
|
|
69
|
+
if (model) models.push({ index: i, model });
|
|
70
|
+
}
|
|
71
|
+
if (!baseUrl && !apiKey && !models.length) return null;
|
|
72
|
+
const resolvedBase = baseUrl || 'https://api.openai.com/v1';
|
|
73
|
+
const isOpenRouter = /(^|\.)openrouter\.ai(?=\/|$)/i.test(new URL(resolvedBase).hostname);
|
|
74
|
+
const connectionId = 'env-connection';
|
|
75
|
+
const truthy = value => /^(1|true|yes|on)$/i.test(String(value ?? '').trim());
|
|
76
|
+
const flagOrEnv = flag => {
|
|
77
|
+
if (overrides[flag] !== undefined && overrides[flag] !== null && overrides[flag] !== '') return overrides[flag];
|
|
78
|
+
return values[flag] || process.env[flag];
|
|
79
|
+
};
|
|
80
|
+
const autostart = truthy(flagOrEnv('OPENAI_AUTOSTART'));
|
|
81
|
+
const maxDecisions = Math.max(0, Math.round(Number(flagOrEnv('OPENAI_MAX_DECISIONS') || 0) || 0));
|
|
82
|
+
const startingStack = Math.max(100, Math.round(Number(flagOrEnv('OPENAI_STARTING_STACK') || 10_000) || 10_000));
|
|
83
|
+
return {
|
|
84
|
+
source: '.env',
|
|
85
|
+
settings: {
|
|
86
|
+
// Default chip stack for every model seat. Override with --starting-stack
|
|
87
|
+
// or OPENAI_STARTING_STACK.
|
|
88
|
+
startingStack,
|
|
89
|
+
// Opt-in launcher demo/benchmark controls. Autostart is off unless the
|
|
90
|
+
// launcher explicitly enables it; maxDecisions hard-stops the tournament.
|
|
91
|
+
autostart,
|
|
92
|
+
maxDecisions,
|
|
93
|
+
},
|
|
94
|
+
connections: [{
|
|
95
|
+
id: connectionId,
|
|
96
|
+
name: isOpenRouter ? 'OpenRouter' : 'OpenAI-compatible',
|
|
97
|
+
kind: isOpenRouter ? 'openrouter' : 'openai',
|
|
98
|
+
baseUrl: resolvedBase,
|
|
99
|
+
apiKey,
|
|
100
|
+
headers: '',
|
|
101
|
+
}],
|
|
102
|
+
players: models.map(({ index, model }, order) => ({
|
|
103
|
+
lobbySeat: Math.max(0, index - 1),
|
|
104
|
+
name: `Player ${index}`,
|
|
105
|
+
connectionId,
|
|
106
|
+
model,
|
|
107
|
+
protocol: isOpenRouter && /(^|\/)typesafe\/jev-/i.test(model.replace(/^~/, '')) ? 'jev_decisions' : 'tool',
|
|
108
|
+
provider: '',
|
|
109
|
+
})),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const loadedEnv = await loadArenaEnv();
|
|
114
|
+
const arenaEnv = envBootstrap(loadedEnv.values, envOverrides);
|
|
115
|
+
|
|
116
|
+
if (help) {
|
|
117
|
+
console.log(`pokertools-arena\n\nUsage:\n npx pokertools-arena [--port 4173] [--host 127.0.0.1] [--chrome] [--no-open] [--no-env] [--autostart] [--starting-stack N] [--max-decisions N]\n\nThe launcher serves the prebuilt static benchmark locally and opens it in your browser so you can watch the tournament. --chrome opens Google Chrome specifically. By default it also reads .env from the current directory and preloads OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_PLAYER1..10 into page memory. --autostart starts the tournament once seats are loaded, and --max-decisions hard-stops it after N poker decisions. --starting-stack and --max-decisions override .env values. Use --no-env to disable the .env bootstrap.`);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mime = {
|
|
122
|
+
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
|
|
123
|
+
'.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function isLoopbackRequest(req) {
|
|
127
|
+
const address = String(req.socket?.remoteAddress || '');
|
|
128
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function safePath(urlPath) {
|
|
132
|
+
const clean = decodeURIComponent((urlPath || '/').split('?')[0]);
|
|
133
|
+
const target = clean === '/' ? '/index.html' : clean;
|
|
134
|
+
const resolved = normalize(join(root, target));
|
|
135
|
+
return resolved.startsWith(root) ? resolved : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const server = createServer(async (req, res) => {
|
|
139
|
+
try {
|
|
140
|
+
const urlPath = (req.url || '/').split('?')[0];
|
|
141
|
+
if (urlPath === '/arena-env.js') {
|
|
142
|
+
const exposedEnv = arenaEnv && (isLoopbackRequest(req) || !arenaEnv.connections?.some(c => c.apiKey)) ? arenaEnv : null;
|
|
143
|
+
const payload = `window.__POKERTOOLS_ENV__ = ${JSON.stringify(exposedEnv)};\n`;
|
|
144
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
145
|
+
res.end(payload);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const file = safePath(req.url);
|
|
149
|
+
if (!file) throw new Error('Invalid path');
|
|
150
|
+
const info = await stat(file);
|
|
151
|
+
if (!info.isFile()) throw new Error('Not a file');
|
|
152
|
+
const body = await readFile(file);
|
|
153
|
+
res.writeHead(200, {
|
|
154
|
+
'Content-Type': mime[extname(file)] || 'application/octet-stream',
|
|
155
|
+
'Cache-Control': 'no-store',
|
|
156
|
+
'Cross-Origin-Opener-Policy': 'same-origin-allow-popups'
|
|
157
|
+
});
|
|
158
|
+
res.end(body);
|
|
159
|
+
} catch {
|
|
160
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
161
|
+
res.end('Not found');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function openBrowser(url) {
|
|
166
|
+
const platform = process.platform;
|
|
167
|
+
let command, commandArgs;
|
|
168
|
+
if (useChrome) {
|
|
169
|
+
if (platform === 'darwin') { command = 'open'; commandArgs = ['-a', 'Google Chrome', url]; }
|
|
170
|
+
else if (platform === 'win32') { command = 'cmd'; commandArgs = ['/c', 'start', '', 'chrome', url]; }
|
|
171
|
+
else { command = 'google-chrome'; commandArgs = [url]; }
|
|
172
|
+
} else if (platform === 'darwin') { command = 'open'; commandArgs = [url]; }
|
|
173
|
+
else if (platform === 'win32') { command = 'cmd'; commandArgs = ['/c', 'start', '', url]; }
|
|
174
|
+
else { command = 'xdg-open'; commandArgs = [url]; }
|
|
175
|
+
try { spawn(command, commandArgs, { detached: true, stdio: 'ignore' }).unref(); } catch {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function listen(port) {
|
|
179
|
+
server.once('error', error => {
|
|
180
|
+
if (error.code === 'EADDRINUSE' && port < requestedPort + 20) return listen(port + 1);
|
|
181
|
+
console.error(`pokertools-arena: ${error.message}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
});
|
|
184
|
+
server.listen(port, host, () => {
|
|
185
|
+
const address = server.address();
|
|
186
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
187
|
+
const url = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${actualPort}/`;
|
|
188
|
+
console.log(`pokertools-arena running at ${url}`);
|
|
189
|
+
if (arenaEnv) console.log(`Loaded .env bootstrap${loadedEnv.source ? ` from ${loadedEnv.source}` : ''}: ${arenaEnv.players.length} player(s), ${arenaEnv.connections[0].baseUrl}`);
|
|
190
|
+
else if (useEnv) console.log('No supported .env bootstrap values found; using browser-saved/manual setup.');
|
|
191
|
+
else console.log('.env bootstrap disabled by --no-env.');
|
|
192
|
+
console.log('Press Ctrl+C to stop the local launcher.');
|
|
193
|
+
if (shouldOpen) openBrowser(url);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
listen(requestedPort);
|
package/dist/.nojekyll
ADDED
|
File without changes
|