pokertools-arena 0.17.1 → 0.18.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 CHANGED
@@ -6,3 +6,5 @@ OPENAI_PLAYER2=qwen/qwen3.8-flash
6
6
  OPENAI_PLAYER3=typesafe/jev-1.13
7
7
  # OPENAI_PLAYER4=...
8
8
  OPENAI_STARTING_STACK=3000
9
+ # OPENAI_AUTOSTART=1
10
+ # OPENAI_MAX_DECISIONS=50
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.17.1.** A mid-hand all-in no longer aborts the tournament: the decision-context player count now tracks non-eliminated seats instead of stacks, and any context failure falls back to a deterministic safe action so the game continues.
20
+ > **Current release: 0.18.0.** This maintenance release centralizes environment parsing and benchmark constants, makes release artifacts reproducible, fixes archive and cache-version drift, and improves keyboard and screen-reader behavior.
21
21
 
22
22
  ---
23
23
 
@@ -83,6 +83,9 @@ The CLI starts a tiny local static server and opens the browser. It exists only
83
83
  | `--chrome` | off | Open Google Chrome specifically. |
84
84
  | `--no-open` | off | Do not launch a browser. |
85
85
  | `--no-env` | off | Ignore `.env` bootstrap. |
86
+ | `--autostart` | off | Start the configured tournament after launch. |
87
+ | `--starting-stack <n>` | `3000` | Override the chip stack for `.env` seats. |
88
+ | `--max-decisions <n>` | unlimited | Stop after this many model decisions. |
86
89
  | `--help`, `-h` | — | Print usage. |
87
90
 
88
91
  ### Option B — single HTML file
@@ -120,6 +123,8 @@ OPENAI_PLAYER1=google/gemma-4-26b-a4b-it
120
123
  OPENAI_PLAYER2=qwen/qwen3.8-flash
121
124
  OPENAI_PLAYER3=typesafe/jev-1.13
122
125
  OPENAI_STARTING_STACK=3000
126
+ OPENAI_AUTOSTART=0
127
+ OPENAI_MAX_DECISIONS=0
123
128
  ```
124
129
 
125
130
  | Variable | Notes |
@@ -128,6 +133,8 @@ OPENAI_STARTING_STACK=3000
128
133
  | `OPENAI_API_KEY` | Optional for local/unauthenticated endpoints. |
129
134
  | `OPENAI_PLAYER1` … `OPENAI_PLAYER10` | Each value seats one model; Jev models auto-route to Decisions on OpenRouter. |
130
135
  | `OPENAI_STARTING_STACK` | Chip stack per seat (minimum 100, default 3000). |
136
+ | `OPENAI_AUTOSTART` | Set to `1`, `true`, `yes`, or `on` to start automatically. |
137
+ | `OPENAI_MAX_DECISIONS` | Optional tournament decision limit (`0` means unlimited). |
131
138
 
132
139
  See [`.env.example`](./.env.example).
133
140
 
@@ -4,6 +4,7 @@ import { readFile, stat } from 'node:fs/promises';
4
4
  import { extname, dirname, join, normalize, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { spawn } from 'node:child_process';
7
+ import { parseDotEnv } from '../tools/shared/dotenv.mjs';
7
8
 
8
9
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
9
10
  const root = join(packageRoot, 'dist');
@@ -25,29 +26,6 @@ const envOverrides = {
25
26
  OPENAI_AUTOSTART: args.includes('--autostart') ? '1' : null,
26
27
  };
27
28
 
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
29
  async function loadArenaEnv() {
52
30
  if (!useEnv) return { source: null, values: {} };
53
31
  const candidates = [resolve(process.cwd(), '.env'), join(packageRoot, '.env')];
package/dist/app.js CHANGED
@@ -114000,237 +114000,6 @@ var require_browser = __commonJS({
114000
114000
  // src/app.js
114001
114001
  var import_browser = __toESM(require_browser(), 1);
114002
114002
 
114003
- // src/benchmark/scenarios.js
114004
- var POLICY = Object.freeze({
114005
- private: "hero hole cards only",
114006
- public: "board, pot, blinds, stacks, positions, current-hand actions, recent public hand history, public player statistics, and legal actions",
114007
- excluded: "opponent hole cards, other agents reasoning, model outputs, API/provider metadata, hidden deck state, and future cards"
114008
- });
114009
- var OBJECTIVE = "Choose exactly one legal action that best maximizes tournament chip EV from the supplied state. Use only the information in this state and only an actionId present in legalActions.";
114010
- function action(id2, type, description, amount = null) {
114011
- return { id: id2, type, amount, description };
114012
- }
114013
- function withBetting(state) {
114014
- const heroBet = Number(state.hero?.currentBet) || 0;
114015
- const highestBet = Math.max(heroBet, ...(state.opponents ?? []).map((o) => Number(o.currentBet) || 0));
114016
- const toCall = Math.max(0, highestBet - heroBet);
114017
- const stack = Number(state.hero?.stack) || 0;
114018
- return {
114019
- ...state,
114020
- betting: {
114021
- highestBet,
114022
- heroCurrentBet: heroBet,
114023
- toCall,
114024
- effectiveCall: Math.min(stack, toCall),
114025
- stackBehind: stack,
114026
- facingAllInCall: stack > 0 && stack <= toCall
114027
- }
114028
- };
114029
- }
114030
- function baseState(overrides = {}) {
114031
- return withBetting({
114032
- contextVersion: 3,
114033
- informationPolicy: POLICY,
114034
- objective: OBJECTIVE,
114035
- game: "No-Limit Texas Holdem tournament",
114036
- memoryPolicy: {
114037
- currentHand: "all public model actions in the current hand before this decision",
114038
- recentHands: "last 8 completed public hands",
114039
- publicPlayerStats: "deterministic aggregates from completed hands before the current hand; identical public dataset for every seat"
114040
- },
114041
- tournament: { handNumber: 50, blindLevel: 4, playersRemaining: 2, startingPlayers: 2 },
114042
- blinds: { smallBlind: 50, bigBlind: 100, ante: 0 },
114043
- hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BTN/SB", stack: 1e4, stackBB: 100, cards: ["As", "Kd"], currentBet: 0 },
114044
- board: [],
114045
- street: "PREFLOP",
114046
- pot: 150,
114047
- buttonSeat: 1,
114048
- actionHistory: [],
114049
- recentHands: [],
114050
- publicPlayerStats: [
114051
- { playerId: "benchmark-hero", playerName: "Benchmark Hero", sampleHands: 40, vpipPct: 0.3, pfrPct: 0.22, aggressionPct: 0.41, foldPct: 0.28, callPct: 0.24, checkPct: 0.18, wins: 10 },
114052
- { playerId: "benchmark-villain", playerName: "Benchmark Villain", sampleHands: 40, vpipPct: 0.31, pfrPct: 0.21, aggressionPct: 0.4, foldPct: 0.29, callPct: 0.24, checkPct: 0.17, wins: 9 }
114053
- ],
114054
- opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 1e4, stackBB: 100, currentBet: 100, status: "ACTIVE", cards: [] }],
114055
- legalActions: [],
114056
- ...overrides
114057
- });
114058
- }
114059
- var DECISION_SANITY_SCENARIOS = Object.freeze([
114060
- {
114061
- id: "free-check-dominates-fold",
114062
- title: "Free check dominates folding",
114063
- category: "Dominance",
114064
- expectedTypes: ["CHECK"],
114065
- note: "With no bet to call, checking preserves every future outcome while folding gives up immediately.",
114066
- state: baseState({ street: "FLOP", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 5e3, stackBB: 50, cards: ["7c", "2d"], currentBet: 0 }, board: ["As", "Kd", "Qh"], pot: 300, opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 5e3, stackBB: 50, currentBet: 0, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CHECK", "Check")] })
114067
- },
114068
- {
114069
- id: "board-royal-call",
114070
- title: "Board royal flush: call a small bet",
114071
- category: "Guaranteed tie",
114072
- expectedTypes: ["CALL"],
114073
- note: "The board itself is a royal flush, so folding gives up a guaranteed share of the pot.",
114074
- state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 4900, stackBB: 49, cards: ["2c", "3d"], currentBet: 0 }, board: ["As", "Ks", "Qs", "Js", "Ts"], pot: 1100, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 100, description: "Bet 100 (1 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 4900, stackBB: 49, currentBet: 100, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 100")] })
114075
- },
114076
- {
114077
- id: "royal-nuts-call",
114078
- title: "Royal flush facing a shove",
114079
- category: "Nuts",
114080
- expectedTypes: ["CALL"],
114081
- note: "Hero holds the unbeatable royal flush and must not fold to an all-in bet.",
114082
- state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 4e3, stackBB: 40, cards: ["As", "Ks"], currentBet: 0 }, board: ["Qs", "Js", "Ts", "2d", "3c"], pot: 6e3, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 4e3, description: "All-in 4000 (40 BB)" }, potBefore: 2e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 4e3, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 4000")] })
114083
- },
114084
- {
114085
- id: "straight-flush-call",
114086
- title: "Straight flush facing a river bet",
114087
- category: "Nuts",
114088
- expectedTypes: ["CALL"],
114089
- note: "Hero has a nine-high straight flush; folding to the available call is an obvious sanity failure.",
114090
- state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 7e3, stackBB: 70, cards: ["9s", "8s"], currentBet: 0 }, board: ["7s", "6s", "5s", "Kd", "2c"], pot: 3500, opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 5500, stackBB: 55, currentBet: 1500, status: "ACTIVE", cards: [] }], actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 1500, description: "Bet 1500 (15 BB)" }, potBefore: 2e3 }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 1500")] })
114091
- },
114092
- {
114093
- id: "aces-vs-short-shove",
114094
- title: "Pocket aces versus a short-stack shove",
114095
- category: "Preflop",
114096
- expectedTypes: ["CALL"],
114097
- note: "With only fold or call available, folding AA to a short all-in is a basic strategic failure.",
114098
- state: baseState({ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9600, stackBB: 96, cards: ["As", "Ah"], currentBet: 100 }, pot: 2550, actionHistory: [{ street: "PREFLOP", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "RAISE", amount: 2500, description: "All-in to 2500 (25 BB)" }, potBefore: 150 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 2500, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 2400")] })
114099
- },
114100
- {
114101
- id: "seven-deuce-vs-shove",
114102
- title: "Seven-deuce offsuit versus a 100 BB shove",
114103
- category: "Preflop",
114104
- expectedTypes: ["FOLD"],
114105
- note: "The weakest starting hand facing an enormous shove with little dead money should fold.",
114106
- state: baseState({ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9900, stackBB: 99, cards: ["7c", "2d"], currentBet: 100 }, pot: 10150, actionHistory: [{ street: "PREFLOP", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "RAISE", amount: 1e4, description: "All-in to 10000 (100 BB)" }, potBefore: 150 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 1e4, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 9900")] })
114107
- },
114108
- {
114109
- id: "river-air-overbet",
114110
- title: "Seven-high versus a 10\xD7 pot river shove",
114111
- category: "River discipline",
114112
- expectedTypes: ["FOLD"],
114113
- note: "Hero has seven-high with no meaningful showdown value and faces an extreme overbet.",
114114
- state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 1e4, stackBB: 100, cards: ["7d", "2d"], currentBet: 0 }, board: ["Ac", "Kh", "Qh", "9s", "3c"], pot: 1100, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 1e4, description: "All-in 10000 (100 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 1e4, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 10000")] })
114115
- },
114116
- {
114117
- id: "nut-flush-tiny-bet",
114118
- title: "Nut flush versus a tiny river bet",
114119
- category: "Pot odds",
114120
- expectedTypes: ["CALL"],
114121
- note: "Hero has the nut flush and faces a tiny bet; folding is an obvious sanity failure.",
114122
- state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9900, stackBB: 99, cards: ["As", "8s"], currentBet: 0 }, board: ["Ks", "Qs", "4s", "2h", "3d"], pot: 1100, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 100, description: "Bet 100 (1 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 9900, stackBB: 99, currentBet: 100, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 100")] })
114123
- },
114124
- {
114125
- id: "kings-vs-open",
114126
- title: "Pocket kings versus a normal open",
114127
- category: "Preflop",
114128
- expectedTypes: ["CALL", "RAISE"],
114129
- note: "Continuing with KK is expected; the suite accepts either calling or raising and rejects folding.",
114130
- state: baseState({ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9900, stackBB: 99, cards: ["Kc", "Kd"], currentBet: 100 }, pot: 450, actionHistory: [{ street: "PREFLOP", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "RAISE", amount: 300, description: "Raise to 300 (3 BB)" }, potBefore: 150 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 9700, stackBB: 97, currentBet: 300, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 200"), action("A2", "RAISE", "Raise to 900 (9 BB)", 900), action("A3", "RAISE", "Raise to 1200 (12 BB)", 1200)] })
114131
- },
114132
- {
114133
- id: "top-pair-tiny-river-bet",
114134
- title: "Top pair versus a 5% pot river bet",
114135
- category: "Pot odds",
114136
- expectedTypes: ["CALL"],
114137
- note: "Top pair facing a tiny river bet gets overwhelming pot odds; folding is a basic sanity failure.",
114138
- state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9950, stackBB: 99.5, cards: ["Ah", "Jd"], currentBet: 0 }, board: ["As", "8c", "6d", "4h", "2c"], pot: 1050, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 50, description: "Bet 50 (0.5 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 9950, stackBB: 99.5, currentBet: 50, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 50")] })
114139
- }
114140
- ]);
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: 2
114153
- },
114154
- timing: {
114155
- actionSeconds: 30,
114156
- timeBankSeconds: 30,
114157
- lowTimeSeconds: 5,
114158
- lowTimeFraction: 0.25,
114159
- betweenActionsMs: 250,
114160
- betweenHandsMs: 2e3
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
- siteUrl: "https://pokertools-arena.github.io/",
114179
- siteName: "pokertools-arena",
114180
- presets: {
114181
- openai: "https://api.openai.com/v1",
114182
- openrouter: "https://openrouter.ai/api/v1",
114183
- typesafe: "https://api.typesafe.ai/v1/systemone"
114184
- }
114185
- },
114186
- history: {
114187
- recentDecisions: 12,
114188
- snapshotEvents: 300,
114189
- persistedEvents: 100
114190
- }
114191
- };
114192
- function mergeConfig(base, override) {
114193
- if (!override || typeof override !== "object" || Array.isArray(override)) return { ...base };
114194
- const merged = { ...base };
114195
- for (const [key, value] of Object.entries(override)) {
114196
- if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
114197
- merged[key] = mergeConfig(base[key], value);
114198
- } else if (value !== void 0) {
114199
- merged[key] = value;
114200
- }
114201
- }
114202
- return merged;
114203
- }
114204
- var ARENA_CONFIG = Object.freeze(mergeConfig(BASE_CONFIG, globalThis.__POKERTOOLS_ARENA_CONFIG__));
114205
- var TABLE_DEFAULTS = Object.freeze(ARENA_CONFIG.table);
114206
- var TIMING_DEFAULTS = Object.freeze(ARENA_CONFIG.timing);
114207
- var CONFIG_LIMITS = Object.freeze(ARENA_CONFIG.limits);
114208
- var CONNECTION_PRESETS = Object.freeze(ARENA_CONFIG.connections.presets);
114209
- var HISTORY_CONFIG = Object.freeze(ARENA_CONFIG.history);
114210
- var FORM_DEFAULTS = Object.freeze({ ...TABLE_DEFAULTS, ...TIMING_DEFAULTS });
114211
- function applyConfiguredFormDefaults(form) {
114212
- for (const [name, value] of Object.entries(FORM_DEFAULTS)) {
114213
- const input = form?.elements?.namedItem(name);
114214
- if (!input) continue;
114215
- input.value = String(value);
114216
- const limits = CONFIG_LIMITS[name] || {};
114217
- for (const attribute of ["min", "max", "step"]) {
114218
- if (limits[attribute] != null) input.setAttribute(attribute, String(limits[attribute]));
114219
- }
114220
- }
114221
- }
114222
- function defaultConnections() {
114223
- const kind = ARENA_CONFIG.connections.defaultKind;
114224
- return [{ name: "API", kind, baseUrl: CONNECTION_PRESETS[kind], apiKey: "", headers: defaultExtraHeaders(kind) }];
114225
- }
114226
- function defaultExtraHeaders(kind) {
114227
- if (kind !== "openrouter") return "";
114228
- return JSON.stringify({
114229
- "HTTP-Referer": ARENA_CONFIG.connections.siteUrl,
114230
- "X-OpenRouter-Title": ARENA_CONFIG.connections.siteName
114231
- }, null, 2);
114232
- }
114233
-
114234
114003
  // src/lib/decision-core.js
114235
114004
  var import_evaluator = __toESM(require_dist2(), 1);
114236
114005
  var ACTION = Object.freeze({
@@ -115794,6 +115563,231 @@ async function decideHierarchical({
115794
115563
  }
115795
115564
  }
115796
115565
 
115566
+ // src/benchmark/scenarios.js
115567
+ function action(id2, type, description, amount = null) {
115568
+ return { id: id2, type, amount, description };
115569
+ }
115570
+ function withBetting(state) {
115571
+ const heroBet = Number(state.hero?.currentBet) || 0;
115572
+ const highestBet = Math.max(heroBet, ...(state.opponents ?? []).map((o) => Number(o.currentBet) || 0));
115573
+ const toCall = Math.max(0, highestBet - heroBet);
115574
+ const stack = Number(state.hero?.stack) || 0;
115575
+ return {
115576
+ ...state,
115577
+ betting: {
115578
+ highestBet,
115579
+ heroCurrentBet: heroBet,
115580
+ toCall,
115581
+ effectiveCall: Math.min(stack, toCall),
115582
+ stackBehind: stack,
115583
+ facingAllInCall: stack > 0 && stack <= toCall
115584
+ }
115585
+ };
115586
+ }
115587
+ function baseState(overrides = {}) {
115588
+ return withBetting({
115589
+ contextVersion: DECISION_CONTEXT_VERSION,
115590
+ informationPolicy: INFORMATION_POLICY,
115591
+ objective: DECISION_OBJECTIVE,
115592
+ game: "No-Limit Texas Holdem tournament",
115593
+ memoryPolicy: {
115594
+ currentHand: "all public model actions in the current hand before this decision",
115595
+ recentHands: "last 8 completed public hands",
115596
+ publicPlayerStats: "deterministic aggregates from completed hands before the current hand; identical public dataset for every seat"
115597
+ },
115598
+ tournament: { handNumber: 50, blindLevel: 4, playersRemaining: 2, startingPlayers: 2 },
115599
+ blinds: { smallBlind: 50, bigBlind: 100, ante: 0 },
115600
+ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BTN/SB", stack: 1e4, stackBB: 100, cards: ["As", "Kd"], currentBet: 0 },
115601
+ board: [],
115602
+ street: "PREFLOP",
115603
+ pot: 150,
115604
+ buttonSeat: 1,
115605
+ actionHistory: [],
115606
+ recentHands: [],
115607
+ publicPlayerStats: [
115608
+ { playerId: "benchmark-hero", playerName: "Benchmark Hero", sampleHands: 40, vpipPct: 0.3, pfrPct: 0.22, aggressionPct: 0.41, foldPct: 0.28, callPct: 0.24, checkPct: 0.18, wins: 10 },
115609
+ { playerId: "benchmark-villain", playerName: "Benchmark Villain", sampleHands: 40, vpipPct: 0.31, pfrPct: 0.21, aggressionPct: 0.4, foldPct: 0.29, callPct: 0.24, checkPct: 0.17, wins: 9 }
115610
+ ],
115611
+ opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 1e4, stackBB: 100, currentBet: 100, status: "ACTIVE", cards: [] }],
115612
+ legalActions: [],
115613
+ ...overrides
115614
+ });
115615
+ }
115616
+ var DECISION_SANITY_SCENARIOS = Object.freeze([
115617
+ {
115618
+ id: "free-check-dominates-fold",
115619
+ title: "Free check dominates folding",
115620
+ category: "Dominance",
115621
+ expectedTypes: ["CHECK"],
115622
+ note: "With no bet to call, checking preserves every future outcome while folding gives up immediately.",
115623
+ state: baseState({ street: "FLOP", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 5e3, stackBB: 50, cards: ["7c", "2d"], currentBet: 0 }, board: ["As", "Kd", "Qh"], pot: 300, opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 5e3, stackBB: 50, currentBet: 0, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CHECK", "Check")] })
115624
+ },
115625
+ {
115626
+ id: "board-royal-call",
115627
+ title: "Board royal flush: call a small bet",
115628
+ category: "Guaranteed tie",
115629
+ expectedTypes: ["CALL"],
115630
+ note: "The board itself is a royal flush, so folding gives up a guaranteed share of the pot.",
115631
+ state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 4900, stackBB: 49, cards: ["2c", "3d"], currentBet: 0 }, board: ["As", "Ks", "Qs", "Js", "Ts"], pot: 1100, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 100, description: "Bet 100 (1 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 4900, stackBB: 49, currentBet: 100, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 100")] })
115632
+ },
115633
+ {
115634
+ id: "royal-nuts-call",
115635
+ title: "Royal flush facing a shove",
115636
+ category: "Nuts",
115637
+ expectedTypes: ["CALL"],
115638
+ note: "Hero holds the unbeatable royal flush and must not fold to an all-in bet.",
115639
+ state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 4e3, stackBB: 40, cards: ["As", "Ks"], currentBet: 0 }, board: ["Qs", "Js", "Ts", "2d", "3c"], pot: 6e3, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 4e3, description: "All-in 4000 (40 BB)" }, potBefore: 2e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 4e3, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 4000")] })
115640
+ },
115641
+ {
115642
+ id: "straight-flush-call",
115643
+ title: "Straight flush facing a river bet",
115644
+ category: "Nuts",
115645
+ expectedTypes: ["CALL"],
115646
+ note: "Hero has a nine-high straight flush; folding to the available call is an obvious sanity failure.",
115647
+ state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 7e3, stackBB: 70, cards: ["9s", "8s"], currentBet: 0 }, board: ["7s", "6s", "5s", "Kd", "2c"], pot: 3500, opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 5500, stackBB: 55, currentBet: 1500, status: "ACTIVE", cards: [] }], actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 1500, description: "Bet 1500 (15 BB)" }, potBefore: 2e3 }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 1500")] })
115648
+ },
115649
+ {
115650
+ id: "aces-vs-short-shove",
115651
+ title: "Pocket aces versus a short-stack shove",
115652
+ category: "Preflop",
115653
+ expectedTypes: ["CALL"],
115654
+ note: "With only fold or call available, folding AA to a short all-in is a basic strategic failure.",
115655
+ state: baseState({ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9600, stackBB: 96, cards: ["As", "Ah"], currentBet: 100 }, pot: 2550, actionHistory: [{ street: "PREFLOP", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "RAISE", amount: 2500, description: "All-in to 2500 (25 BB)" }, potBefore: 150 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 2500, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 2400")] })
115656
+ },
115657
+ {
115658
+ id: "seven-deuce-vs-shove",
115659
+ title: "Seven-deuce offsuit versus a 100 BB shove",
115660
+ category: "Preflop",
115661
+ expectedTypes: ["FOLD"],
115662
+ note: "The weakest starting hand facing an enormous shove with little dead money should fold.",
115663
+ state: baseState({ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9900, stackBB: 99, cards: ["7c", "2d"], currentBet: 100 }, pot: 10150, actionHistory: [{ street: "PREFLOP", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "RAISE", amount: 1e4, description: "All-in to 10000 (100 BB)" }, potBefore: 150 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 1e4, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 9900")] })
115664
+ },
115665
+ {
115666
+ id: "river-air-overbet",
115667
+ title: "Seven-high versus a 10\xD7 pot river shove",
115668
+ category: "River discipline",
115669
+ expectedTypes: ["FOLD"],
115670
+ note: "Hero has seven-high with no meaningful showdown value and faces an extreme overbet.",
115671
+ state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 1e4, stackBB: 100, cards: ["7d", "2d"], currentBet: 0 }, board: ["Ac", "Kh", "Qh", "9s", "3c"], pot: 1100, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 1e4, description: "All-in 10000 (100 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 0, stackBB: 0, currentBet: 1e4, status: "ALL_IN", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 10000")] })
115672
+ },
115673
+ {
115674
+ id: "nut-flush-tiny-bet",
115675
+ title: "Nut flush versus a tiny river bet",
115676
+ category: "Pot odds",
115677
+ expectedTypes: ["CALL"],
115678
+ note: "Hero has the nut flush and faces a tiny bet; folding is an obvious sanity failure.",
115679
+ state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9900, stackBB: 99, cards: ["As", "8s"], currentBet: 0 }, board: ["Ks", "Qs", "4s", "2h", "3d"], pot: 1100, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 100, description: "Bet 100 (1 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 9900, stackBB: 99, currentBet: 100, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 100")] })
115680
+ },
115681
+ {
115682
+ id: "kings-vs-open",
115683
+ title: "Pocket kings versus a normal open",
115684
+ category: "Preflop",
115685
+ expectedTypes: ["CALL", "RAISE"],
115686
+ note: "Continuing with KK is expected; the suite accepts either calling or raising and rejects folding.",
115687
+ state: baseState({ hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9900, stackBB: 99, cards: ["Kc", "Kd"], currentBet: 100 }, pot: 450, actionHistory: [{ street: "PREFLOP", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "RAISE", amount: 300, description: "Raise to 300 (3 BB)" }, potBefore: 150 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 9700, stackBB: 97, currentBet: 300, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 200"), action("A2", "RAISE", "Raise to 900 (9 BB)", 900), action("A3", "RAISE", "Raise to 1200 (12 BB)", 1200)] })
115688
+ },
115689
+ {
115690
+ id: "top-pair-tiny-river-bet",
115691
+ title: "Top pair versus a 5% pot river bet",
115692
+ category: "Pot odds",
115693
+ expectedTypes: ["CALL"],
115694
+ note: "Top pair facing a tiny river bet gets overwhelming pot odds; folding is a basic sanity failure.",
115695
+ state: baseState({ street: "RIVER", hero: { id: "benchmark-hero", name: "Benchmark Hero", seat: 1, position: "BB", stack: 9950, stackBB: 99.5, cards: ["Ah", "Jd"], currentBet: 0 }, board: ["As", "8c", "6d", "4h", "2c"], pot: 1050, actionHistory: [{ street: "RIVER", playerName: "Benchmark Villain", position: "BTN/SB", action: { type: "BET", amount: 50, description: "Bet 50 (0.5 BB)" }, potBefore: 1e3 }], opponents: [{ seat: 2, id: "benchmark-villain", name: "Benchmark Villain", stack: 9950, stackBB: 99.5, currentBet: 50, status: "ACTIVE", cards: [] }], legalActions: [action("A0", "FOLD", "Fold"), action("A1", "CALL", "Call 50")] })
115696
+ }
115697
+ ]);
115698
+
115699
+ // src/config/arena-config.js
115700
+ var BASE_CONFIG = {
115701
+ table: {
115702
+ minPlayers: 2,
115703
+ maxPlayers: 10,
115704
+ startingStack: 3e3,
115705
+ smallBlind: 25,
115706
+ bigBlind: 50,
115707
+ ante: 5,
115708
+ handsPerLevel: 8,
115709
+ blindMultiplier: 2
115710
+ },
115711
+ timing: {
115712
+ actionSeconds: 30,
115713
+ timeBankSeconds: 30,
115714
+ lowTimeSeconds: 5,
115715
+ lowTimeFraction: 0.25,
115716
+ betweenActionsMs: 250,
115717
+ betweenHandsMs: 2e3
115718
+ },
115719
+ limits: {
115720
+ startingStack: { min: 100 },
115721
+ smallBlind: { min: 1 },
115722
+ bigBlind: { min: 2 },
115723
+ ante: { min: 0 },
115724
+ handsPerLevel: { min: 1 },
115725
+ blindMultiplier: { min: 1.1, max: 3, step: 0.1 },
115726
+ actionSeconds: { min: 1, max: 120 },
115727
+ timeBankSeconds: { min: 0, max: 600 },
115728
+ lowTimeSeconds: { min: 0, max: 600 },
115729
+ lowTimeFraction: { min: 0, max: 1, step: 0.05 },
115730
+ betweenActionsMs: { min: 0, max: 5e3 },
115731
+ betweenHandsMs: { min: 0, max: 1e4 }
115732
+ },
115733
+ connections: {
115734
+ defaultKind: "openai",
115735
+ siteUrl: "https://pokertools-arena.github.io/",
115736
+ siteName: "pokertools-arena",
115737
+ presets: {
115738
+ openai: "https://api.openai.com/v1",
115739
+ openrouter: "https://openrouter.ai/api/v1",
115740
+ typesafe: "https://api.typesafe.ai/v1/systemone"
115741
+ }
115742
+ },
115743
+ history: {
115744
+ recentDecisions: 12,
115745
+ snapshotEvents: 300,
115746
+ persistedEvents: 100
115747
+ }
115748
+ };
115749
+ function mergeConfig(base, override) {
115750
+ if (!override || typeof override !== "object" || Array.isArray(override)) return { ...base };
115751
+ const merged = { ...base };
115752
+ for (const [key, value] of Object.entries(override)) {
115753
+ if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
115754
+ merged[key] = mergeConfig(base[key], value);
115755
+ } else if (value !== void 0) {
115756
+ merged[key] = value;
115757
+ }
115758
+ }
115759
+ return merged;
115760
+ }
115761
+ var ARENA_CONFIG = Object.freeze(mergeConfig(BASE_CONFIG, globalThis.__POKERTOOLS_ARENA_CONFIG__));
115762
+ var TABLE_DEFAULTS = Object.freeze(ARENA_CONFIG.table);
115763
+ var TIMING_DEFAULTS = Object.freeze(ARENA_CONFIG.timing);
115764
+ var CONFIG_LIMITS = Object.freeze(ARENA_CONFIG.limits);
115765
+ var CONNECTION_PRESETS = Object.freeze(ARENA_CONFIG.connections.presets);
115766
+ var HISTORY_CONFIG = Object.freeze(ARENA_CONFIG.history);
115767
+ var FORM_DEFAULTS = Object.freeze({ ...TABLE_DEFAULTS, ...TIMING_DEFAULTS });
115768
+ function applyConfiguredFormDefaults(form) {
115769
+ for (const [name, value] of Object.entries(FORM_DEFAULTS)) {
115770
+ const input = form?.elements?.namedItem(name);
115771
+ if (!input) continue;
115772
+ input.value = String(value);
115773
+ const limits = CONFIG_LIMITS[name] || {};
115774
+ for (const attribute of ["min", "max", "step"]) {
115775
+ if (limits[attribute] != null) input.setAttribute(attribute, String(limits[attribute]));
115776
+ }
115777
+ }
115778
+ }
115779
+ function defaultConnections() {
115780
+ const kind = ARENA_CONFIG.connections.defaultKind;
115781
+ return [{ name: "API", kind, baseUrl: CONNECTION_PRESETS[kind], apiKey: "", headers: defaultExtraHeaders(kind) }];
115782
+ }
115783
+ function defaultExtraHeaders(kind) {
115784
+ if (kind !== "openrouter") return "";
115785
+ return JSON.stringify({
115786
+ "HTTP-Referer": ARENA_CONFIG.connections.siteUrl,
115787
+ "X-OpenRouter-Title": ARENA_CONFIG.connections.siteName
115788
+ }, null, 2);
115789
+ }
115790
+
115797
115791
  // src/assets/card-deal.mp3
115798
115792
  var card_deal_default = "data:audio/mpeg;base64,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onWk/7WjRpaNbM8siaAVORzuRee5FH9wZbLzyz7MyRz84iRgkAly1NTnLBUZI1stX/NeHVFTqn1zzbZJcCsBI/UmTMTXtPb1pz/+c5xIlrzLV29aaizHVTScFb/S//uyZNQA1G53PYNJMJBm7bfBJKMQElme3M1gwGG/uNrEwwm6XeZk0jjVPrafqVFZ5uUuYzzPmMbMYUaVHlZOYxcuYzqAic0zqyG6lvZAIBZ0DAT5WUKJK0p2+YxW/NNQy6mNo+jUM+Y2gYVQMBCWQHTPhpVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVYBAJFtyALEh6pQgm3TcX+zfs0AjnS9fnS//VAIVRJa1cMBUBEqzHf2aqX8aELpbS0iIEnImlUNItWjlSvKWl1mlUMULMZSl6Ras1GMalJZrkKGMf8RE0VWSwJNSRSQs1eERNeJBYNJocltCESkRN7ZWntItSRJil0ZKs+MYqzjlocISXlia3DcuuhQkpI74Qkv2XVjOoUyl/l/5jS3K5nmMBGMUpW/9jpVZaRU3mtZq+Pslv4bWv/4yhVWYoGx0qwdB8HQCwjdknQHQLhKAqIpJv8rXsSsrANg+tba/yjmJNVYZsm/2v2WrmlWG21FRzTS0UPgWOZqKFmZpWv5gI4C5f/DAWIeWAg3H0JinFnGROm0LgFANLjDDUfC2rAC4ybZelI5XycrVlyuooE3NTVny9mkt61mEMOhsAomAOFIehKUAbGYNSoB4gj6OJqIR+IqQRkMdSqakpGSTwSjsRSmTSsbEk8Eo7ElOTS8bElQSl4kpyaXlRi4ZPklsmoRaOURkvElsSUhOLpqSlYkrxJQi0cqUyokrzGi5tbAqKr5jQ+hPUzxy+c0XNrWo2VgqBItE6jaNUdpGEnSNROWRhIujUTlm4SenIlJGqJWc6JyRqiV08tTyyRyy//uyZOyA9HtpKzkDS/J3rlWSCQLcHTXkZi0w1cjeA8AB//gMMJGo1TJG4SsiYFHLIwSPInJHabhKyMBJnNTseb6od+moOvNBokKCSbKgbKmYHpFB1ipsnR5DqZoSYBsTLNmD8qgbUKIxkTL4mxIWuZu67ypMQU1FNC4wqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
115799
115793
 
@@ -116810,6 +116804,7 @@ var TournamentDirector = class {
116810
116804
  async recoverHand(triggerError) {
116811
116805
  this.currentDecision = null;
116812
116806
  if (this.handComplete()) {
116807
+ this.completeHand();
116813
116808
  this.broadcast();
116814
116809
  return;
116815
116810
  }
@@ -117322,14 +117317,6 @@ function addConnectionRow(connection = {}) {
117322
117317
  if (event.target.value !== "openrouter" && headersInput.value.trim() === defaultOpenRouterHeaders) headersInput.value = "";
117323
117318
  refreshSeatConnectionSelect();
117324
117319
  });
117325
- $("[data-field=kind]", row).addEventListener("change", () => {
117326
- const kind = $("[data-field=kind]", row).value;
117327
- const url = $("[data-field=baseUrl]", row);
117328
- if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value = CONNECTION_PRESETS.typesafe;
117329
- if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openrouter;
117330
- if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openai;
117331
- refreshSeatConnectionSelect();
117332
- });
117333
117320
  $(".remove-connection", row).addEventListener("click", () => {
117334
117321
  if (els.connectionsEditor.children.length <= 1) return;
117335
117322
  const removedId = row.dataset.id;
@@ -119476,6 +119463,7 @@ var choicePickers = /* @__PURE__ */ new Map();
119476
119463
  function syncChoicePicker(select) {
119477
119464
  const picker = choicePickers.get(select);
119478
119465
  if (!picker) return;
119466
+ const focusedIndex = document.activeElement?.closest?.(".choice-option")?.dataset.optionIndex;
119479
119467
  picker.replaceChildren(...[...select.options].map((option, index) => {
119480
119468
  const button = document.createElement("button");
119481
119469
  button.type = "button";
@@ -119484,11 +119472,14 @@ function syncChoicePicker(select) {
119484
119472
  button.dataset.optionIndex = String(index);
119485
119473
  button.textContent = option.textContent;
119486
119474
  button.disabled = select.disabled || option.disabled;
119487
- button.setAttribute("aria-checked", String(option.value === select.value));
119475
+ const checked = option.value === select.value;
119476
+ button.setAttribute("aria-checked", String(checked));
119477
+ button.tabIndex = checked ? 0 : -1;
119488
119478
  return button;
119489
119479
  }));
119490
119480
  picker.dataset.optionCount = String(select.options.length);
119491
119481
  picker.setAttribute("aria-disabled", String(select.disabled));
119482
+ if (focusedIndex != null) picker.querySelector(`[data-option-index="${focusedIndex}"]`)?.focus();
119492
119483
  }
119493
119484
  function syncChoicePickers() {
119494
119485
  for (const select of choicePickers.keys()) syncChoicePicker(select);
@@ -119501,6 +119492,8 @@ function enhanceSelect(select) {
119501
119492
  picker.role = "radiogroup";
119502
119493
  picker.setAttribute("aria-label", select.getAttribute("aria-label") || `Choose ${id2.replace(/[-_]/g, " ")}`);
119503
119494
  select.classList.add("native-select-enhanced");
119495
+ select.tabIndex = -1;
119496
+ select.setAttribute("aria-hidden", "true");
119504
119497
  select.insertAdjacentElement("afterend", picker);
119505
119498
  choicePickers.set(select, picker);
119506
119499
  syncChoicePicker(select);