pokertools-arena 0.8.0 → 0.9.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 +1 -1
- package/README.md +6 -3
- package/bin/pokertools-arena.mjs +1 -1
- package/dist/app.js +122 -50
- package/dist/index.html +18 -18
- package/dist/pokertools-arena.html +598 -3048
- package/dist/styles.css +489 -3011
- package/docs/diagnostics/SUMMARY.md +1 -2
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +20 -1
- package/package.json +1 -1
- package/src/app.js +45 -68
- package/src/config/arena-config.js +86 -0
- package/src/index.html +18 -18
- package/src/styles.css +489 -3011
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| **Package** | `npx pokertools-arena` |
|
|
18
18
|
| **Runtime** | Node.js ≥ 24 for tooling; any modern browser for the app |
|
|
19
19
|
|
|
20
|
-
> **Current release: 0.
|
|
20
|
+
> **Current release: 0.9.0.** A responsive interface and configuration release: the table, inspector, logs, decision history, and dialogs now adapt cleanly from phones to wide displays. Tournament defaults are centralized, with 30 seconds per action and 3,000 starting chips.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -107,6 +107,8 @@ The table **is** the player editor. A fresh launch shows empty seats around the
|
|
|
107
107
|
3. Configure **Settings → Connections** (base URLs and keys) and **Settings → Tournament** (stack, blinds, clock, benchmark mode, architecture, representation).
|
|
108
108
|
4. Press **Start**. Only configured seats enter the tournament, and seat editing locks while it runs.
|
|
109
109
|
|
|
110
|
+
Defaults and validation limits live in [`src/config/arena-config.js`](./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.
|
|
111
|
+
|
|
110
112
|
## `.env` bootstrap
|
|
111
113
|
|
|
112
114
|
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.
|
|
@@ -117,7 +119,7 @@ OPENAI_API_KEY="sk-or-v1-..."
|
|
|
117
119
|
OPENAI_PLAYER1=google/gemma-4-26b-a4b-it
|
|
118
120
|
OPENAI_PLAYER2=qwen/qwen3.8-flash
|
|
119
121
|
OPENAI_PLAYER3=typesafe/jev-1.13
|
|
120
|
-
OPENAI_STARTING_STACK=
|
|
122
|
+
OPENAI_STARTING_STACK=3000
|
|
121
123
|
```
|
|
122
124
|
|
|
123
125
|
| Variable | Notes |
|
|
@@ -125,7 +127,7 @@ OPENAI_STARTING_STACK=10000
|
|
|
125
127
|
| `OPENAI_BASE_URL` | Defaults to `https://api.openai.com/v1`; OpenRouter URLs are auto-detected. |
|
|
126
128
|
| `OPENAI_API_KEY` | Optional for local/unauthenticated endpoints. |
|
|
127
129
|
| `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
|
|
130
|
+
| `OPENAI_STARTING_STACK` | Chip stack per seat (minimum 100, default 3000). |
|
|
129
131
|
|
|
130
132
|
See [`.env.example`](./.env.example).
|
|
131
133
|
|
|
@@ -186,6 +188,7 @@ pokertools-arena/
|
|
|
186
188
|
├── bin/pokertools-arena.mjs local static launcher + .env bootstrap
|
|
187
189
|
├── src/ browser application source
|
|
188
190
|
│ ├── index.html · app.js · styles.css
|
|
191
|
+
│ ├── config/arena-config.js defaults, limits, presets, host overrides
|
|
189
192
|
│ ├── lib/decision-core.js canonical DOM-free decision core
|
|
190
193
|
│ ├── benchmark/scenarios.js in-app sanity scenarios
|
|
191
194
|
│ ├── env/arena-env.js .env placeholder injected by the launcher
|
package/bin/pokertools-arena.mjs
CHANGED
|
@@ -79,7 +79,7 @@ function envBootstrap(values, overrides = {}) {
|
|
|
79
79
|
};
|
|
80
80
|
const autostart = truthy(flagOrEnv('OPENAI_AUTOSTART'));
|
|
81
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') ||
|
|
82
|
+
const startingStack = Math.max(100, Math.round(Number(flagOrEnv('OPENAI_STARTING_STACK') || 3_000) || 3_000));
|
|
83
83
|
return {
|
|
84
84
|
source: '.env',
|
|
85
85
|
settings: {
|
package/dist/app.js
CHANGED
|
@@ -114139,6 +114139,89 @@ var DECISION_SANITY_SCENARIOS = Object.freeze([
|
|
|
114139
114139
|
}
|
|
114140
114140
|
]);
|
|
114141
114141
|
|
|
114142
|
+
// src/config/arena-config.js
|
|
114143
|
+
var BASE_CONFIG = {
|
|
114144
|
+
table: {
|
|
114145
|
+
minPlayers: 2,
|
|
114146
|
+
maxPlayers: 10,
|
|
114147
|
+
startingStack: 3e3,
|
|
114148
|
+
smallBlind: 25,
|
|
114149
|
+
bigBlind: 50,
|
|
114150
|
+
ante: 5,
|
|
114151
|
+
handsPerLevel: 8,
|
|
114152
|
+
blindMultiplier: 1.5
|
|
114153
|
+
},
|
|
114154
|
+
timing: {
|
|
114155
|
+
actionSeconds: 30,
|
|
114156
|
+
timeBankSeconds: 30,
|
|
114157
|
+
lowTimeSeconds: 5,
|
|
114158
|
+
lowTimeFraction: 0.25,
|
|
114159
|
+
betweenActionsMs: 250,
|
|
114160
|
+
betweenHandsMs: 700
|
|
114161
|
+
},
|
|
114162
|
+
limits: {
|
|
114163
|
+
startingStack: { min: 100 },
|
|
114164
|
+
smallBlind: { min: 1 },
|
|
114165
|
+
bigBlind: { min: 2 },
|
|
114166
|
+
ante: { min: 0 },
|
|
114167
|
+
handsPerLevel: { min: 1 },
|
|
114168
|
+
blindMultiplier: { min: 1.1, max: 3, step: 0.1 },
|
|
114169
|
+
actionSeconds: { min: 1, max: 120 },
|
|
114170
|
+
timeBankSeconds: { min: 0, max: 600 },
|
|
114171
|
+
lowTimeSeconds: { min: 0, max: 600 },
|
|
114172
|
+
lowTimeFraction: { min: 0, max: 1, step: 0.05 },
|
|
114173
|
+
betweenActionsMs: { min: 0, max: 5e3 },
|
|
114174
|
+
betweenHandsMs: { min: 0, max: 1e4 }
|
|
114175
|
+
},
|
|
114176
|
+
connections: {
|
|
114177
|
+
defaultKind: "openai",
|
|
114178
|
+
presets: {
|
|
114179
|
+
openai: "https://api.openai.com/v1",
|
|
114180
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
114181
|
+
typesafe: "https://api.typesafe.ai/v1/systemone"
|
|
114182
|
+
}
|
|
114183
|
+
},
|
|
114184
|
+
history: {
|
|
114185
|
+
recentDecisions: 12,
|
|
114186
|
+
snapshotEvents: 300,
|
|
114187
|
+
persistedEvents: 100
|
|
114188
|
+
}
|
|
114189
|
+
};
|
|
114190
|
+
function mergeConfig(base, override) {
|
|
114191
|
+
if (!override || typeof override !== "object" || Array.isArray(override)) return { ...base };
|
|
114192
|
+
const merged = { ...base };
|
|
114193
|
+
for (const [key, value] of Object.entries(override)) {
|
|
114194
|
+
if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
|
|
114195
|
+
merged[key] = mergeConfig(base[key], value);
|
|
114196
|
+
} else if (value !== void 0) {
|
|
114197
|
+
merged[key] = value;
|
|
114198
|
+
}
|
|
114199
|
+
}
|
|
114200
|
+
return merged;
|
|
114201
|
+
}
|
|
114202
|
+
var ARENA_CONFIG = Object.freeze(mergeConfig(BASE_CONFIG, globalThis.__POKERTOOLS_ARENA_CONFIG__));
|
|
114203
|
+
var TABLE_DEFAULTS = Object.freeze(ARENA_CONFIG.table);
|
|
114204
|
+
var TIMING_DEFAULTS = Object.freeze(ARENA_CONFIG.timing);
|
|
114205
|
+
var CONFIG_LIMITS = Object.freeze(ARENA_CONFIG.limits);
|
|
114206
|
+
var CONNECTION_PRESETS = Object.freeze(ARENA_CONFIG.connections.presets);
|
|
114207
|
+
var HISTORY_CONFIG = Object.freeze(ARENA_CONFIG.history);
|
|
114208
|
+
var FORM_DEFAULTS = Object.freeze({ ...TABLE_DEFAULTS, ...TIMING_DEFAULTS });
|
|
114209
|
+
function applyConfiguredFormDefaults(form) {
|
|
114210
|
+
for (const [name, value] of Object.entries(FORM_DEFAULTS)) {
|
|
114211
|
+
const input = form?.elements?.namedItem(name);
|
|
114212
|
+
if (!input) continue;
|
|
114213
|
+
input.value = String(value);
|
|
114214
|
+
const limits = CONFIG_LIMITS[name] || {};
|
|
114215
|
+
for (const attribute of ["min", "max", "step"]) {
|
|
114216
|
+
if (limits[attribute] != null) input.setAttribute(attribute, String(limits[attribute]));
|
|
114217
|
+
}
|
|
114218
|
+
}
|
|
114219
|
+
}
|
|
114220
|
+
function defaultConnections() {
|
|
114221
|
+
const kind = ARENA_CONFIG.connections.defaultKind;
|
|
114222
|
+
return [{ name: "API", kind, baseUrl: CONNECTION_PRESETS[kind], apiKey: "", headers: "" }];
|
|
114223
|
+
}
|
|
114224
|
+
|
|
114142
114225
|
// src/lib/decision-core.js
|
|
114143
114226
|
var import_evaluator = __toESM(require_dist2(), 1);
|
|
114144
114227
|
var ACTION = Object.freeze({
|
|
@@ -115561,7 +115644,6 @@ var els = {
|
|
|
115561
115644
|
handValue: $("#handValue"),
|
|
115562
115645
|
levelValue: $("#levelValue"),
|
|
115563
115646
|
streetLabel: $("#streetLabel"),
|
|
115564
|
-
winnerBanner: $("#winnerBanner"),
|
|
115565
115647
|
decisionPanelTitle: $("#decisionPanelTitle"),
|
|
115566
115648
|
decisionEmpty: $("#decisionEmpty"),
|
|
115567
115649
|
decisionCard: $("#decisionCard"),
|
|
@@ -115582,8 +115664,10 @@ var els = {
|
|
|
115582
115664
|
decisionLabelPosition: $("#decisionLabelPosition"),
|
|
115583
115665
|
decisionLabelOptions: $("#decisionLabelOptions"),
|
|
115584
115666
|
decisionFeed: $("#decisionFeed"),
|
|
115667
|
+
decisionFeedSummary: $("#decisionFeedSummary"),
|
|
115585
115668
|
eventLog: $("#eventLog"),
|
|
115586
115669
|
logSummary: $("#logSummary"),
|
|
115670
|
+
logTabCount: $("#logTabCount"),
|
|
115587
115671
|
logSearch: $("#logSearch"),
|
|
115588
115672
|
logFilter: $("#logFilter"),
|
|
115589
115673
|
logClear: $("#logClear"),
|
|
@@ -115653,7 +115737,7 @@ var audioContext = null;
|
|
|
115653
115737
|
var soundMasterGain = null;
|
|
115654
115738
|
var lastVisualState = null;
|
|
115655
115739
|
var lastProcessedEventId = null;
|
|
115656
|
-
var MAX_LOBBY_SEATS =
|
|
115740
|
+
var MAX_LOBBY_SEATS = TABLE_DEFAULTS.maxPlayers;
|
|
115657
115741
|
var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
115658
115742
|
var editingSeatIndex = null;
|
|
115659
115743
|
var seatNameAuto = false;
|
|
@@ -115674,14 +115758,6 @@ var activeDecisionClockId = null;
|
|
|
115674
115758
|
var tableRecording = null;
|
|
115675
115759
|
var currentReplayEvent = null;
|
|
115676
115760
|
var renderMemo = { status: "", table: "", decision: "", feed: "", events: "", stats: "" };
|
|
115677
|
-
var TIMING_DEFAULTS = Object.freeze({
|
|
115678
|
-
actionSeconds: 20,
|
|
115679
|
-
timeBankSeconds: 30,
|
|
115680
|
-
lowTimeSeconds: 5,
|
|
115681
|
-
lowTimeFraction: 0.25,
|
|
115682
|
-
betweenActionsMs: 250,
|
|
115683
|
-
betweenHandsMs: 700
|
|
115684
|
-
});
|
|
115685
115761
|
function animationsAllowed() {
|
|
115686
115762
|
return document.visibilityState === "visible" && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
115687
115763
|
}
|
|
@@ -115932,10 +116008,6 @@ function processVisualEffects(s) {
|
|
|
115932
116008
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
115933
116009
|
lastVisualState = s;
|
|
115934
116010
|
}
|
|
115935
|
-
var defaultConnections = [
|
|
115936
|
-
{ name: "API", kind: "openai", baseUrl: "https://api.openai.com/v1", apiKey: "", headers: "" }
|
|
115937
|
-
];
|
|
115938
|
-
var defaultPlayers = [];
|
|
115939
116011
|
function injectedEnvironmentConfig() {
|
|
115940
116012
|
const raw = globalThis.__POKERTOOLS_ENV__;
|
|
115941
116013
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -115943,7 +116015,7 @@ function injectedEnvironmentConfig() {
|
|
|
115943
116015
|
const players = Array.isArray(raw.players) ? raw.players.filter((p) => p && p.model) : [];
|
|
115944
116016
|
if (!connections.length && !players.length) return null;
|
|
115945
116017
|
const settings = raw.settings && typeof raw.settings === "object" ? {
|
|
115946
|
-
startingStack: Math.max(
|
|
116018
|
+
startingStack: Math.max(CONFIG_LIMITS.startingStack.min, Math.round(Number(raw.settings.startingStack) || TABLE_DEFAULTS.startingStack)),
|
|
115947
116019
|
autostart: Boolean(raw.settings.autostart),
|
|
115948
116020
|
maxDecisions: Math.max(0, Math.round(Number(raw.settings.maxDecisions) || 0))
|
|
115949
116021
|
} : null;
|
|
@@ -115953,7 +116025,7 @@ function injectedEnvironmentConfig() {
|
|
|
115953
116025
|
id: String(c.id || `env-connection-${index + 1}`),
|
|
115954
116026
|
name: String(c.name || "API"),
|
|
115955
116027
|
kind: ["openai", "openrouter", "typesafe"].includes(c.kind) ? c.kind : "openai",
|
|
115956
|
-
baseUrl: String(c.baseUrl ||
|
|
116028
|
+
baseUrl: String(c.baseUrl || CONNECTION_PRESETS.openai),
|
|
115957
116029
|
apiKey: String(c.apiKey || ""),
|
|
115958
116030
|
headers: String(c.headers || "")
|
|
115959
116031
|
})),
|
|
@@ -116198,7 +116270,7 @@ function buildBlindStructure(config) {
|
|
|
116198
116270
|
let sb = Math.max(1, Math.round(config.smallBlind));
|
|
116199
116271
|
let bb = Math.max(sb * 2, Math.round(config.bigBlind));
|
|
116200
116272
|
let ante = Math.max(0, Math.round(config.ante || 0));
|
|
116201
|
-
const multiplier = clamp(Number(config.blindMultiplier) ||
|
|
116273
|
+
const multiplier = clamp(Number(config.blindMultiplier) || TABLE_DEFAULTS.blindMultiplier, CONFIG_LIMITS.blindMultiplier.min, CONFIG_LIMITS.blindMultiplier.max);
|
|
116202
116274
|
const safe = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
116203
116275
|
for (let i = 0; i < 60; i++) {
|
|
116204
116276
|
if (!safe(sb) || !safe(bb) || !safe(ante)) break;
|
|
@@ -116215,7 +116287,7 @@ function buildBlindStructure(config) {
|
|
|
116215
116287
|
return levels;
|
|
116216
116288
|
}
|
|
116217
116289
|
function normalizeConfig(input = {}) {
|
|
116218
|
-
const players = Array.isArray(input.players) ? input.players.slice(0,
|
|
116290
|
+
const players = Array.isArray(input.players) ? input.players.slice(0, MAX_LOBBY_SEATS) : [];
|
|
116219
116291
|
const connections = Array.isArray(input.connections) ? input.connections : [];
|
|
116220
116292
|
if (players.length < 2) throw new Error("At least 2 players are required");
|
|
116221
116293
|
if (!connections.length) throw new Error("Add at least one API connection");
|
|
@@ -116229,24 +116301,24 @@ function normalizeConfig(input = {}) {
|
|
|
116229
116301
|
parseHeaders(c.headers);
|
|
116230
116302
|
return { ...c, baseUrl: normalizeBaseUrl(c.baseUrl) };
|
|
116231
116303
|
});
|
|
116232
|
-
const smallBlind = Math.max(
|
|
116233
|
-
const bigBlind = Math.max(
|
|
116304
|
+
const smallBlind = Math.max(CONFIG_LIMITS.smallBlind.min, Math.round(Number(input.smallBlind) || TABLE_DEFAULTS.smallBlind));
|
|
116305
|
+
const bigBlind = Math.max(CONFIG_LIMITS.bigBlind.min, Math.round(Number(input.bigBlind) || TABLE_DEFAULTS.bigBlind));
|
|
116234
116306
|
if (bigBlind < smallBlind * 2) throw new Error("Big blind must be at least twice the small blind");
|
|
116235
116307
|
return {
|
|
116236
116308
|
id: id("tournament"),
|
|
116237
116309
|
name: "pokertools-arena",
|
|
116238
|
-
startingStack: Math.max(
|
|
116310
|
+
startingStack: Math.max(CONFIG_LIMITS.startingStack.min, Math.round(Number(input.startingStack) || TABLE_DEFAULTS.startingStack)),
|
|
116239
116311
|
smallBlind,
|
|
116240
116312
|
bigBlind,
|
|
116241
|
-
ante: Math.max(
|
|
116242
|
-
handsPerLevel: Math.max(
|
|
116243
|
-
blindMultiplier: clamp(Number(input.blindMultiplier) ||
|
|
116244
|
-
actionSeconds: clamp(Number(input.actionSeconds) || TIMING_DEFAULTS.actionSeconds,
|
|
116245
|
-
timeBankSeconds: clamp(Number(input.timeBankSeconds) || TIMING_DEFAULTS.timeBankSeconds,
|
|
116246
|
-
lowTimeSeconds: clamp(Number.isFinite(Number(input.lowTimeSeconds)) ? Number(input.lowTimeSeconds) : TIMING_DEFAULTS.lowTimeSeconds,
|
|
116247
|
-
lowTimeFraction: clamp(Number.isFinite(Number(input.lowTimeFraction)) ? Number(input.lowTimeFraction) : TIMING_DEFAULTS.lowTimeFraction,
|
|
116248
|
-
betweenActionsMs: clamp(Number(input.betweenActionsMs) || TIMING_DEFAULTS.betweenActionsMs,
|
|
116249
|
-
betweenHandsMs: clamp(Number(input.betweenHandsMs) || TIMING_DEFAULTS.betweenHandsMs,
|
|
116313
|
+
ante: Math.max(CONFIG_LIMITS.ante.min, Math.round(Number(input.ante) || TABLE_DEFAULTS.ante)),
|
|
116314
|
+
handsPerLevel: Math.max(CONFIG_LIMITS.handsPerLevel.min, Math.round(Number(input.handsPerLevel) || TABLE_DEFAULTS.handsPerLevel)),
|
|
116315
|
+
blindMultiplier: clamp(Number(input.blindMultiplier) || TABLE_DEFAULTS.blindMultiplier, CONFIG_LIMITS.blindMultiplier.min, CONFIG_LIMITS.blindMultiplier.max),
|
|
116316
|
+
actionSeconds: clamp(Number(input.actionSeconds) || TIMING_DEFAULTS.actionSeconds, CONFIG_LIMITS.actionSeconds.min, CONFIG_LIMITS.actionSeconds.max),
|
|
116317
|
+
timeBankSeconds: clamp(Number(input.timeBankSeconds) || TIMING_DEFAULTS.timeBankSeconds, CONFIG_LIMITS.timeBankSeconds.min, CONFIG_LIMITS.timeBankSeconds.max),
|
|
116318
|
+
lowTimeSeconds: clamp(Number.isFinite(Number(input.lowTimeSeconds)) ? Number(input.lowTimeSeconds) : TIMING_DEFAULTS.lowTimeSeconds, CONFIG_LIMITS.lowTimeSeconds.min, CONFIG_LIMITS.lowTimeSeconds.max),
|
|
116319
|
+
lowTimeFraction: clamp(Number.isFinite(Number(input.lowTimeFraction)) ? Number(input.lowTimeFraction) : TIMING_DEFAULTS.lowTimeFraction, CONFIG_LIMITS.lowTimeFraction.min, CONFIG_LIMITS.lowTimeFraction.max),
|
|
116320
|
+
betweenActionsMs: clamp(Number(input.betweenActionsMs) || TIMING_DEFAULTS.betweenActionsMs, CONFIG_LIMITS.betweenActionsMs.min, CONFIG_LIMITS.betweenActionsMs.max),
|
|
116321
|
+
betweenHandsMs: clamp(Number(input.betweenHandsMs) || TIMING_DEFAULTS.betweenHandsMs, CONFIG_LIMITS.betweenHandsMs.min, CONFIG_LIMITS.betweenHandsMs.max),
|
|
116250
116322
|
connections: cleanConnections,
|
|
116251
116323
|
benchmarkMode: input.benchmarkMode === BENCHMARK_MODES.RAW ? BENCHMARK_MODES.RAW : DEFAULT_BENCHMARK_MODE,
|
|
116252
116324
|
decisionArchitecture: input.decisionArchitecture === DECISION_ARCHITECTURES.FLAT ? DECISION_ARCHITECTURES.FLAT : DEFAULT_DECISION_ARCHITECTURE,
|
|
@@ -116420,7 +116492,7 @@ var TournamentDirector = class {
|
|
|
116420
116492
|
stats: this.stats,
|
|
116421
116493
|
publicPlayerStats,
|
|
116422
116494
|
timeBanks: this.timeBanks,
|
|
116423
|
-
events: this.events.slice(-
|
|
116495
|
+
events: this.events.slice(-HISTORY_CONFIG.snapshotEvents)
|
|
116424
116496
|
};
|
|
116425
116497
|
}
|
|
116426
116498
|
broadcast() {
|
|
@@ -116442,7 +116514,7 @@ var TournamentDirector = class {
|
|
|
116442
116514
|
persistLightweight() {
|
|
116443
116515
|
try {
|
|
116444
116516
|
const state = this.snapshot();
|
|
116445
|
-
const lightweight = { ...state, events: state.events.slice(-
|
|
116517
|
+
const lightweight = { ...state, events: state.events.slice(-HISTORY_CONFIG.persistedEvents), currentDecision: null };
|
|
116446
116518
|
storageSet("pokertoolsArenaLastState", JSON.stringify(lightweight));
|
|
116447
116519
|
} catch {
|
|
116448
116520
|
}
|
|
@@ -116831,16 +116903,12 @@ function refreshSeatConnectionSelect(selected = null) {
|
|
|
116831
116903
|
applySeatProtocolRules();
|
|
116832
116904
|
}
|
|
116833
116905
|
function connectionPresetUrl(kind) {
|
|
116834
|
-
|
|
116835
|
-
if (kind === "typesafe") return "https://api.typesafe.ai/v1/systemone";
|
|
116836
|
-
return "https://api.openai.com/v1";
|
|
116906
|
+
return CONNECTION_PRESETS[kind] || CONNECTION_PRESETS.openai;
|
|
116837
116907
|
}
|
|
116838
116908
|
function shouldReplacePresetUrl(value) {
|
|
116839
116909
|
const normalized = normalizeBaseUrl(value);
|
|
116840
116910
|
return !normalized || [
|
|
116841
|
-
|
|
116842
|
-
"https://openrouter.ai/api/v1",
|
|
116843
|
-
"https://api.typesafe.ai/v1/systemone"
|
|
116911
|
+
...Object.values(CONNECTION_PRESETS)
|
|
116844
116912
|
].includes(normalized);
|
|
116845
116913
|
}
|
|
116846
116914
|
function addConnectionRow(connection = {}) {
|
|
@@ -116864,9 +116932,9 @@ function addConnectionRow(connection = {}) {
|
|
|
116864
116932
|
$("[data-field=kind]", row).addEventListener("change", () => {
|
|
116865
116933
|
const kind = $("[data-field=kind]", row).value;
|
|
116866
116934
|
const url = $("[data-field=baseUrl]", row);
|
|
116867
|
-
if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value =
|
|
116868
|
-
if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value =
|
|
116869
|
-
if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value =
|
|
116935
|
+
if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value = CONNECTION_PRESETS.typesafe;
|
|
116936
|
+
if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openrouter;
|
|
116937
|
+
if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openai;
|
|
116870
116938
|
refreshSeatConnectionSelect();
|
|
116871
116939
|
});
|
|
116872
116940
|
$(".remove-connection", row).addEventListener("click", () => {
|
|
@@ -117089,7 +117157,7 @@ function saveSeatAssignments() {
|
|
|
117089
117157
|
}
|
|
117090
117158
|
function restoreSeatAssignments(rows = []) {
|
|
117091
117159
|
seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
117092
|
-
const sources = rows
|
|
117160
|
+
const sources = rows;
|
|
117093
117161
|
sources.slice(0, MAX_LOBBY_SEATS).forEach((player, index) => {
|
|
117094
117162
|
const lobbySeat = clamp(Math.round(Number(player.lobbySeat ?? index)), 0, MAX_LOBBY_SEATS - 1);
|
|
117095
117163
|
let connectionId = player.connectionId || "";
|
|
@@ -117159,13 +117227,14 @@ async function primeModelCapabilities() {
|
|
|
117159
117227
|
}));
|
|
117160
117228
|
}
|
|
117161
117229
|
function restoreSetup() {
|
|
117230
|
+
applyConfiguredFormDefaults(els.setupForm);
|
|
117162
117231
|
let saved = null;
|
|
117163
117232
|
try {
|
|
117164
117233
|
saved = JSON.parse(storageGet("pokertoolsArenaBrowserConfig"));
|
|
117165
117234
|
} catch {
|
|
117166
117235
|
}
|
|
117167
117236
|
const injected = injectedEnvironmentConfig();
|
|
117168
|
-
const conns = injected?.connections?.length ? injected.connections : saved?.connections?.length ? saved.connections : defaultConnections;
|
|
117237
|
+
const conns = injected?.connections?.length ? injected.connections : saved?.connections?.length ? saved.connections : defaultConnections();
|
|
117169
117238
|
conns.forEach(addConnectionRow);
|
|
117170
117239
|
restoreSeatAssignments(injected?.players?.length ? injected.players : saved?.players?.length ? saved.players : []);
|
|
117171
117240
|
capabilityPrime = primeModelCapabilities();
|
|
@@ -117191,7 +117260,7 @@ function restoreSetup() {
|
|
|
117191
117260
|
function renderLobbyIfVisible() {
|
|
117192
117261
|
if (lobbyVisible && (!director || !["RUNNING", "PAUSED"].includes(director.status))) renderTable(currentState || { status: "IDLE", events: [] });
|
|
117193
117262
|
}
|
|
117194
|
-
var TABLE_MIN_PLAYERS =
|
|
117263
|
+
var TABLE_MIN_PLAYERS = TABLE_DEFAULTS.minPlayers;
|
|
117195
117264
|
function visualSeatAngle(index, count) {
|
|
117196
117265
|
const safeCount = Math.max(TABLE_MIN_PLAYERS, Math.min(MAX_LOBBY_SEATS, Math.round(Number(count) || TABLE_MIN_PLAYERS)));
|
|
117197
117266
|
const safeIndex = (Math.round(Number(index) || 0) % safeCount + safeCount) % safeCount;
|
|
@@ -117334,7 +117403,6 @@ function renderLobbyTable() {
|
|
|
117334
117403
|
els.handValue.textContent = "\u2014";
|
|
117335
117404
|
els.levelValue.textContent = "\u2014";
|
|
117336
117405
|
els.streetLabel.textContent = "LOBBY";
|
|
117337
|
-
els.winnerBanner.classList.add("hidden");
|
|
117338
117406
|
}
|
|
117339
117407
|
function renderStatus(s) {
|
|
117340
117408
|
const status = s?.status || "IDLE";
|
|
@@ -117405,7 +117473,6 @@ function renderTable(s) {
|
|
|
117405
117473
|
els.levelValue.title = `Level ${Number(table.blindLevel ?? 0) + 1}`;
|
|
117406
117474
|
els.streetLabel.textContent = s.status === "FINISHED" && s.winner ? `WINNER \xB7 ${displayModelName(s.winner.model || "")}` : table.street || "\u2014";
|
|
117407
117475
|
els.streetLabel.classList.toggle("winner-street", s.status === "FINISHED" && Boolean(s.winner));
|
|
117408
|
-
els.winnerBanner.classList.add("hidden");
|
|
117409
117476
|
}
|
|
117410
117477
|
function clearTurnRings(keep = null) {
|
|
117411
117478
|
for (const el of $$(".seat.turn-active")) if (el !== keep) el.classList.remove("turn-active");
|
|
@@ -117644,7 +117711,7 @@ function renderDecision(s) {
|
|
|
117644
117711
|
}
|
|
117645
117712
|
function renderFeed(s) {
|
|
117646
117713
|
const feedArchive = eventArchive(s);
|
|
117647
|
-
const events = feedArchive.filter((e) => e.type === "DECISION").slice(-
|
|
117714
|
+
const events = feedArchive.filter((e) => e.type === "DECISION").slice(-HISTORY_CONFIG.recentDecisions).reverse();
|
|
117648
117715
|
const explanations = new Map(feedArchive.filter((e) => e.type === "SPECTATOR_EXPLANATION").map((e) => [e.decisionId, e.text]));
|
|
117649
117716
|
els.decisionFeed.innerHTML = events.length ? events.map((e) => {
|
|
117650
117717
|
const infra = [];
|
|
@@ -117656,6 +117723,7 @@ function renderFeed(s) {
|
|
|
117656
117723
|
const reason = explanations.get(e.decisionId) || e.publicReason || (e.decisionMeta?.family ? "Typed decision (no text rationale)" : "No public rationale");
|
|
117657
117724
|
return `<button type="button" class="decision-item decision-history-item ${e.error ? "error" : ""}" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))} decision"><div class="decision-item-head"><strong title="${escapeHtml(e.configuredModel || e.resolvedModel || "")}">${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))}</strong><span class="decision-item-action">${escapeHtml(e.action?.description || "\u2014")}</span></div><div class="decision-reason">${escapeHtml(reason)}</div>${telemetry ? `<div class="decision-item-telemetry">${telemetry}</div>` : ""}<div class="decision-meta mono">${escapeHtml(e.connection || "")} \xB7 ${escapeHtml(shortModel(e.resolvedModel || e.configuredModel || ""))} \xB7 ${e.primaryDecisionLatencyMs || e.latencyMs || 0}ms${infra.length ? ` \xB7 ${escapeHtml(infra.join(" \xB7 "))}` : ""}${e.error ? ` \xB7 ${escapeHtml(e.error)}` : ""}<span class="decision-replay-hint">View hand \u2197</span></div></button>`;
|
|
117658
117725
|
}).join("") : '<div class="empty-state">No decisions yet.</div>';
|
|
117726
|
+
if (els.decisionFeedSummary) els.decisionFeedSummary.textContent = events.length ? `${events.length} most recent` : "Newest first";
|
|
117659
117727
|
}
|
|
117660
117728
|
function eventArchive(s = currentState) {
|
|
117661
117729
|
if (director?.events?.length) return director.events;
|
|
@@ -117707,6 +117775,7 @@ function eventMatchesView(event) {
|
|
|
117707
117775
|
function renderEvents(s) {
|
|
117708
117776
|
const archive = eventArchive(s);
|
|
117709
117777
|
const events = archive.filter(eventMatchesView).reverse();
|
|
117778
|
+
if (els.logTabCount) els.logTabCount.textContent = archive.length > 999 ? "999+" : String(archive.length);
|
|
117710
117779
|
if (els.logSummary) {
|
|
117711
117780
|
const suffix = events.length === archive.length ? "full run" : `${events.length} shown`;
|
|
117712
117781
|
els.logSummary.textContent = `${archive.length.toLocaleString()} event${archive.length === 1 ? "" : "s"} \xB7 ${suffix}`;
|
|
@@ -117721,7 +117790,7 @@ function renderEvents(s) {
|
|
|
117721
117790
|
const attrs = replayable ? ` type="button" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(detail || "decision")}"` : "";
|
|
117722
117791
|
return `<${tag}${attrs} class="event-row event-${category}${e.error || e.type?.includes?.("ERROR") ? " has-error" : ""}">
|
|
117723
117792
|
<span class="event-rail" aria-hidden="true"></span>
|
|
117724
|
-
<span class="event-main"><span class="event-row-head"><span class="event-type">${escapeHtml(e.type)}</span><time class="event-time mono" datetime="${new Date(e.at).toISOString()}">${escapeHtml(time)}</time></span>${detail ? `<span class="event-detail">${escapeHtml(detail)}</span>` : ""}${meta ? `<span class="event-meta mono">${escapeHtml(meta)}</span>` : ""}</span>
|
|
117793
|
+
<span class="event-main"><span class="event-row-head"><span class="event-type">${escapeHtml(e.type.replaceAll("_", " "))}</span><time class="event-time mono" datetime="${new Date(e.at).toISOString()}">${escapeHtml(time)}</time></span>${detail ? `<span class="event-detail">${escapeHtml(detail)}</span>` : ""}${meta ? `<span class="event-meta mono">${escapeHtml(meta)}</span>` : ""}</span>
|
|
117725
117794
|
${replayable ? '<span class="event-open" aria-hidden="true">\u2197</span>' : ""}
|
|
117726
117795
|
</${tag}>`;
|
|
117727
117796
|
}).join("") || '<div class="empty-state log-empty">No events match these filters.</div>';
|
|
@@ -117784,6 +117853,10 @@ function statsRenderSignature(s) {
|
|
|
117784
117853
|
}
|
|
117785
117854
|
function render(s, { force = false } = {}) {
|
|
117786
117855
|
currentState = s;
|
|
117856
|
+
if (els.logTabCount) {
|
|
117857
|
+
const eventCount = eventArchive(s).length;
|
|
117858
|
+
els.logTabCount.textContent = eventCount > 999 ? "999+" : String(eventCount);
|
|
117859
|
+
}
|
|
117787
117860
|
const statusSig = statusRenderSignature(s);
|
|
117788
117861
|
if (force || renderMemo.status !== statusSig) {
|
|
117789
117862
|
renderMemo.status = statusSig;
|
|
@@ -118880,7 +118953,7 @@ els.clearTestsBtn.addEventListener("click", () => {
|
|
|
118880
118953
|
});
|
|
118881
118954
|
els.setupBtn.addEventListener("click", openSetup);
|
|
118882
118955
|
els.closeSetup.addEventListener("click", () => els.setupDialog.close());
|
|
118883
|
-
els.addConnectionBtn.addEventListener("click", () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind:
|
|
118956
|
+
els.addConnectionBtn.addEventListener("click", () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: ARENA_CONFIG.connections.defaultKind, baseUrl: connectionPresetUrl(ARENA_CONFIG.connections.defaultKind) }));
|
|
118884
118957
|
els.pauseBtn.addEventListener("click", () => {
|
|
118885
118958
|
if (!director) return;
|
|
118886
118959
|
currentState?.status === "PAUSED" ? director.resume() : director.pause();
|
|
@@ -119084,7 +119157,6 @@ loadPokerTools().catch((err) => {
|
|
|
119084
119157
|
els.setupError.classList.remove("hidden");
|
|
119085
119158
|
openSetup({ preserveError: true });
|
|
119086
119159
|
});
|
|
119087
|
-
if (!storageGet("pokertoolsArenaBrowserSeen")) storageSet("pokertoolsArenaBrowserSeen", "1");
|
|
119088
119160
|
var resizeTimer = null;
|
|
119089
119161
|
function relayoutForViewport() {
|
|
119090
119162
|
clearTimeout(resizeTimer);
|
package/dist/index.html
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
</script>
|
|
55
55
|
|
|
56
|
-
<link rel="stylesheet" href="./styles.css?v=0.
|
|
56
|
+
<link rel="stylesheet" href="./styles.css?v=0.9.0" />
|
|
57
57
|
</head>
|
|
58
58
|
<body>
|
|
59
59
|
<div class="app-shell">
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
<div class="brandbar" aria-label="pokertools-arena">
|
|
64
64
|
<div class="brand-mark">♠</div>
|
|
65
65
|
<div class="brand-copy">
|
|
66
|
-
<
|
|
66
|
+
<h1 class="brand-title">pokertools-arena</h1>
|
|
67
67
|
</div>
|
|
68
68
|
</div>
|
|
69
69
|
<a id="repoLink" class="repo-link" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg></a>
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
<div class="tabs" role="tablist" aria-label="Tournament inspector">
|
|
81
81
|
<button id="tabButton-live" class="tab active" data-tab="live" type="button" role="tab" aria-selected="true" aria-controls="tab-live">Live</button>
|
|
82
|
-
<button id="tabButton-log" class="tab" data-tab="log" type="button" role="tab" aria-selected="false" aria-controls="tab-log" tabindex="-1">Log</button>
|
|
82
|
+
<button id="tabButton-log" class="tab" data-tab="log" type="button" role="tab" aria-selected="false" aria-controls="tab-log" tabindex="-1">Log <span id="logTabCount" class="tab-count">0</span></button>
|
|
83
83
|
<button id="tabButton-stats" class="tab" data-tab="stats" type="button" role="tab" aria-selected="false" aria-controls="tab-stats" tabindex="-1">Stats</button>
|
|
84
84
|
</div>
|
|
85
85
|
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
<div id="legalActions" class="legal-actions"></div>
|
|
103
103
|
</div>
|
|
104
104
|
</section>
|
|
105
|
-
<section class="panel-block decision-feed-panel"><div class="panel-title">
|
|
105
|
+
<section class="panel-block decision-feed-panel"><div class="panel-title-row"><div class="panel-title">Decision history</div><div id="decisionFeedSummary" class="panel-caption">Newest first</div></div><div id="decisionFeed" class="decision-feed"></div></section>
|
|
106
106
|
</div>
|
|
107
107
|
|
|
108
108
|
<div id="tab-log" class="tab-panel" role="tabpanel" aria-labelledby="tabButton-log" hidden>
|
|
@@ -132,7 +132,6 @@
|
|
|
132
132
|
</div>
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
|
-
<div id="winnerBanner" class="winner-banner hidden" role="status" aria-live="polite"></div>
|
|
136
135
|
<div id="pokerTable" class="poker-table">
|
|
137
136
|
<div class="felt-ring"></div>
|
|
138
137
|
<div id="fxLayer" class="fx-layer" aria-hidden="true"></div>
|
|
@@ -183,6 +182,7 @@
|
|
|
183
182
|
</div>
|
|
184
183
|
|
|
185
184
|
<div class="dialog-body setup-body">
|
|
185
|
+
<p class="dialog-intro">Configure providers, tournament pacing, and benchmark behavior. Changes are saved locally without API keys.</p>
|
|
186
186
|
<section class="settings-section">
|
|
187
187
|
<div class="section-head compact-section-head">
|
|
188
188
|
<div class="section-title-row"><h3>Connections</h3><button class="info-button" type="button" aria-label="About API connections" data-tip="Use any browser-accessible OpenAI-compatible API. Base URL is always editable. OpenRouter is an optional preset that also enables Jev Decisions routing.">i</button></div>
|
|
@@ -197,18 +197,18 @@
|
|
|
197
197
|
<div id="seatSummary" class="seat-summary">0 / 10 seated</div>
|
|
198
198
|
</div>
|
|
199
199
|
<div class="field-grid tournament-fields">
|
|
200
|
-
<label>Starting stack<input name="startingStack" type="number"
|
|
201
|
-
<label>Small blind<input name="smallBlind" type="number"
|
|
202
|
-
<label>Big blind<input name="bigBlind" type="number"
|
|
203
|
-
<label>Ante<input name="ante" type="number"
|
|
204
|
-
<label>Hands / level<input name="handsPerLevel" type="number"
|
|
205
|
-
<label>Blind multiplier<input name="blindMultiplier" type="number"
|
|
206
|
-
<label>Action time (s)<input name="actionSeconds" type="number"
|
|
207
|
-
<label>Time bank (s)<input name="timeBankSeconds" type="number"
|
|
208
|
-
<label>Low-time warning (s)<input name="lowTimeSeconds" type="number"
|
|
209
|
-
<label>Low-time ring fraction<input name="lowTimeFraction" type="number"
|
|
210
|
-
<label>Action delay (ms)<input name="betweenActionsMs" type="number"
|
|
211
|
-
<label>Hand delay (ms)<input name="betweenHandsMs" type="number"
|
|
200
|
+
<label>Starting stack<input name="startingStack" type="number" /></label>
|
|
201
|
+
<label>Small blind<input name="smallBlind" type="number" /></label>
|
|
202
|
+
<label>Big blind<input name="bigBlind" type="number" /></label>
|
|
203
|
+
<label>Ante<input name="ante" type="number" /></label>
|
|
204
|
+
<label>Hands / level<input name="handsPerLevel" type="number" /></label>
|
|
205
|
+
<label>Blind multiplier<input name="blindMultiplier" type="number" /></label>
|
|
206
|
+
<label>Action time (s)<input name="actionSeconds" type="number" /></label>
|
|
207
|
+
<label>Time bank (s)<input name="timeBankSeconds" type="number" /></label>
|
|
208
|
+
<label>Low-time warning (s)<input name="lowTimeSeconds" type="number" /></label>
|
|
209
|
+
<label>Low-time ring fraction<input name="lowTimeFraction" type="number" /></label>
|
|
210
|
+
<label>Action delay (ms)<input name="betweenActionsMs" type="number" /></label>
|
|
211
|
+
<label>Hand delay (ms)<input name="betweenHandsMs" type="number" /></label>
|
|
212
212
|
</div>
|
|
213
213
|
</section>
|
|
214
214
|
|
|
@@ -391,6 +391,6 @@
|
|
|
391
391
|
</template>
|
|
392
392
|
|
|
393
393
|
<script src="./arena-env.js"></script>
|
|
394
|
-
<script type="module" src="./app.js?v=0.
|
|
394
|
+
<script type="module" src="./app.js?v=0.9.0"></script>
|
|
395
395
|
</body>
|
|
396
396
|
</html>
|