pokertools-arena 0.4.5 → 0.4.9
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/README.md +1 -1
- package/dist/app.js +73 -33
- package/dist/index.html +20 -14
- package/dist/pokertools-arena.html +178 -47
- package/dist/styles.css +85 -0
- package/docs/diagnostics/SUMMARY.md +1 -1
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +54 -0
- package/package.json +1 -1
- package/src/app.js +101 -29
- package/src/index.html +20 -14
- package/src/styles.css +85 -0
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.4.
|
|
20
|
+
> **Current release: 0.4.9.** A maintenance release on top of the 0.4.0 methodology work: modal tooltips float above overflow, the launcher starts exactly once when its default port is busy, real-API diagnostics skip cleanly when repository secrets are absent, the Decision sanity suite attributes results to the correct model, seat editors show the model-derived name, close buttons use centered SVG icons, decision logs no longer repeat the typed-decision caption, Stop immediately freezes the clock and table effects, seat cards clear their last action at the start of each hand, pots fly back to winners at showdown, the header/table chrome and small-screen board cards are more compact, and the inspector is an accessible tablist with corrected poker statistics.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
package/dist/app.js
CHANGED
|
@@ -115737,6 +115737,7 @@ function animateBoardCards(previousCount, currentCount) {
|
|
|
115737
115737
|
}
|
|
115738
115738
|
function processVisualEffects(s) {
|
|
115739
115739
|
const events = s?.events || [];
|
|
115740
|
+
const previous = lastVisualState;
|
|
115740
115741
|
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
115741
115742
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
115742
115743
|
lastVisualState = s;
|
|
@@ -115748,7 +115749,6 @@ function processVisualEffects(s) {
|
|
|
115748
115749
|
lastVisualState = s;
|
|
115749
115750
|
return;
|
|
115750
115751
|
}
|
|
115751
|
-
const previous = lastVisualState;
|
|
115752
115752
|
if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
|
|
115753
115753
|
const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
|
|
115754
115754
|
if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
|
|
@@ -115774,12 +115774,14 @@ function processVisualEffects(s) {
|
|
|
115774
115774
|
}
|
|
115775
115775
|
showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} \xB7 ${e.action?.description || type}`, type);
|
|
115776
115776
|
}
|
|
115777
|
-
if (e.type === "HAND_END"
|
|
115778
|
-
const before = new Map((previous
|
|
115779
|
-
const
|
|
115777
|
+
if (e.type === "HAND_END") {
|
|
115778
|
+
const before = new Map((previous?.table?.players || []).filter(Boolean).map((p) => [p.id, Number(p.stack || 0)]));
|
|
115779
|
+
const payloadIds = (e.winners || []).map((w) => w?.playerId ?? w?.id ?? w).filter(Boolean);
|
|
115780
|
+
const fromStacks = (s?.table?.players || []).filter((p) => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0))).map((p) => p.id);
|
|
115781
|
+
const winnerIds = [.../* @__PURE__ */ new Set([...payloadIds, ...fromStacks])];
|
|
115780
115782
|
const pot = $(".hud-pot", document);
|
|
115781
|
-
|
|
115782
|
-
if (
|
|
115783
|
+
winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
|
|
115784
|
+
if (winnerIds.length) playTableSound("winner");
|
|
115783
115785
|
}
|
|
115784
115786
|
if (e.type === "TOURNAMENT_END") playTableSound("winner");
|
|
115785
115787
|
}
|
|
@@ -115978,18 +115980,21 @@ function buildPublicPlayerStats(events, players, currentHand) {
|
|
|
115978
115980
|
const completedHandNumbers = new Set((events ?? []).filter((e) => e.type === "HAND_END" && Number(e.handNumber) < Number(currentHand)).map((e) => Number(e.handNumber)));
|
|
115979
115981
|
const decisions = (events ?? []).filter((e) => e.type === "DECISION" && completedHandNumbers.has(Number(e.handNumber)));
|
|
115980
115982
|
const handEnds = (events ?? []).filter((e) => e.type === "HAND_END" && completedHandNumbers.has(Number(e.handNumber)));
|
|
115983
|
+
const handStarts = (events ?? []).filter((e) => e.type === "HAND_START" && completedHandNumbers.has(Number(e.handNumber)));
|
|
115981
115984
|
return (players ?? []).map((player) => {
|
|
115982
115985
|
const rows = decisions.filter((e) => e.playerId === player.id);
|
|
115983
115986
|
const preflop = rows.filter((e) => e.street === "PREFLOP");
|
|
115984
|
-
const
|
|
115987
|
+
const dealtHands = new Set(handStarts.filter((e) => Array.isArray(e.playerIds) && e.playerIds.includes(player.id)).map((e) => Number(e.handNumber)));
|
|
115988
|
+
if (!dealtHands.size) for (const e of rows) dealtHands.add(Number(e.handNumber));
|
|
115985
115989
|
const vpipHands = new Set(preflop.filter((e) => ["CALL", "BET", "RAISE"].includes(e.action?.type)).map((e) => e.handNumber));
|
|
115986
115990
|
const pfrHands = new Set(preflop.filter((e) => ["BET", "RAISE"].includes(e.action?.type)).map((e) => e.handNumber));
|
|
115987
115991
|
const aggressive = rows.filter((e) => ["BET", "RAISE"].includes(e.action?.type)).length;
|
|
115988
115992
|
const calls = rows.filter((e) => e.action?.type === "CALL").length;
|
|
115989
115993
|
const checks = rows.filter((e) => e.action?.type === "CHECK").length;
|
|
115990
115994
|
const folds = rows.filter((e) => e.action?.type === "FOLD").length;
|
|
115995
|
+
const foldOpportunities = rows.filter((e) => (e.legalActions ?? []).some((action2) => action2.type === "FOLD")).length;
|
|
115991
115996
|
let wins = 0;
|
|
115992
|
-
const observedHands = new Set(
|
|
115997
|
+
const observedHands = new Set(dealtHands);
|
|
115993
115998
|
for (const end of handEnds) {
|
|
115994
115999
|
if ((end.winners ?? []).some((w) => (w.playerId ?? w.id ?? w) === player.id)) {
|
|
115995
116000
|
wins++;
|
|
@@ -116001,12 +116006,13 @@ function buildPublicPlayerStats(events, players, currentHand) {
|
|
|
116001
116006
|
playerId: player.id,
|
|
116002
116007
|
playerName: player.name,
|
|
116003
116008
|
sampleHands: observedHands.size,
|
|
116004
|
-
preflopSamples:
|
|
116009
|
+
preflopSamples: dealtHands.size,
|
|
116005
116010
|
decisions: rows.length,
|
|
116006
|
-
vpipPct: pct(vpipHands.size,
|
|
116007
|
-
pfrPct: pct(pfrHands.size,
|
|
116008
|
-
|
|
116009
|
-
|
|
116011
|
+
vpipPct: pct(vpipHands.size, dealtHands.size),
|
|
116012
|
+
pfrPct: pct(pfrHands.size, dealtHands.size),
|
|
116013
|
+
// Standard aggression frequency (AFq), not the aggression-factor ratio.
|
|
116014
|
+
aggressionPct: pct(aggressive, aggressive + calls + folds),
|
|
116015
|
+
foldPct: pct(folds, foldOpportunities),
|
|
116010
116016
|
callPct: pct(calls, strategicActions),
|
|
116011
116017
|
checkPct: pct(checks, strategicActions),
|
|
116012
116018
|
wins
|
|
@@ -116080,12 +116086,15 @@ function normalizeConfig(input = {}) {
|
|
|
116080
116086
|
parseHeaders(c.headers);
|
|
116081
116087
|
return { ...c, baseUrl: normalizeBaseUrl(c.baseUrl) };
|
|
116082
116088
|
});
|
|
116089
|
+
const smallBlind = Math.max(1, Math.round(Number(input.smallBlind) || 25));
|
|
116090
|
+
const bigBlind = Math.max(2, Math.round(Number(input.bigBlind) || 50));
|
|
116091
|
+
if (bigBlind < smallBlind * 2) throw new Error("Big blind must be at least twice the small blind");
|
|
116083
116092
|
return {
|
|
116084
116093
|
id: id("tournament"),
|
|
116085
116094
|
name: "pokertools-arena",
|
|
116086
116095
|
startingStack: Math.max(100, Math.round(Number(input.startingStack) || 1e4)),
|
|
116087
|
-
smallBlind
|
|
116088
|
-
bigBlind
|
|
116096
|
+
smallBlind,
|
|
116097
|
+
bigBlind,
|
|
116089
116098
|
ante: Math.max(0, Math.round(Number(input.ante) || 0)),
|
|
116090
116099
|
handsPerLevel: Math.max(1, Math.round(Number(input.handsPerLevel) || 8)),
|
|
116091
116100
|
blindMultiplier: clamp(Number(input.blindMultiplier) || 1.5, 1.1, 3),
|
|
@@ -116104,8 +116113,9 @@ function normalizeConfig(input = {}) {
|
|
|
116104
116113
|
players: players.map((raw, index) => {
|
|
116105
116114
|
const lobbySeat = clamp(Math.round(Number(raw.lobbySeat ?? index)), 0, MAX_LOBBY_SEATS - 1);
|
|
116106
116115
|
const name = String(raw.name || `Player ${lobbySeat + 1}`).trim().slice(0, 40);
|
|
116107
|
-
|
|
116108
|
-
names.
|
|
116116
|
+
const nameKey = name.toLocaleLowerCase();
|
|
116117
|
+
if (names.has(nameKey)) throw new Error(`Duplicate player name: ${name}`);
|
|
116118
|
+
names.add(nameKey);
|
|
116109
116119
|
if (!connIds.has(raw.connectionId)) throw new Error(`Connection missing for ${name}`);
|
|
116110
116120
|
const model = String(raw.model || "").trim();
|
|
116111
116121
|
if (!model) throw new Error(`Model missing for ${name}`);
|
|
@@ -116337,6 +116347,7 @@ var TournamentDirector = class {
|
|
|
116337
116347
|
await this.waitIfPaused();
|
|
116338
116348
|
this.pruneBustedSeats();
|
|
116339
116349
|
this.handNumber++;
|
|
116350
|
+
for (const stat of Object.values(this.stats)) stat.lastAction = null;
|
|
116340
116351
|
this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
|
|
116341
116352
|
try {
|
|
116342
116353
|
this.engine.deal();
|
|
@@ -116349,7 +116360,14 @@ var TournamentDirector = class {
|
|
|
116349
116360
|
}
|
|
116350
116361
|
this.engine.deal();
|
|
116351
116362
|
}
|
|
116352
|
-
this.logEvent("HAND_START", {
|
|
116363
|
+
this.logEvent("HAND_START", {
|
|
116364
|
+
handNumber: this.handNumber,
|
|
116365
|
+
buttonSeat: this.engine.state.buttonSeat,
|
|
116366
|
+
smallBlind: this.engine.state.smallBlind,
|
|
116367
|
+
bigBlind: this.engine.state.bigBlind,
|
|
116368
|
+
ante: this.engine.state.ante,
|
|
116369
|
+
playerIds: (this.engine.state.players ?? []).filter((p) => p && playerStack(p) > 0).map((p) => p.id)
|
|
116370
|
+
});
|
|
116353
116371
|
this.broadcast();
|
|
116354
116372
|
}
|
|
116355
116373
|
handComplete() {
|
|
@@ -116476,17 +116494,13 @@ var TournamentDirector = class {
|
|
|
116476
116494
|
stats.retries += Number(result?.meta?.retryCount || 0);
|
|
116477
116495
|
if (result?.meta?.protocolFallbackTriggered) stats.protocolFallbacks++;
|
|
116478
116496
|
if (this.currentDecision?.id !== decisionId) throw new Error("Decision became stale");
|
|
116479
|
-
const elapsedActive = Math.max(0, Date.now() - startedAt - pausedTotal());
|
|
116480
|
-
if (elapsedActive > baseMs) this.timeBanks[agent.id] = Math.max(0, bankBefore - (elapsedActive - baseMs));
|
|
116481
116497
|
} catch (err) {
|
|
116482
116498
|
elapsed = Math.max(0, Date.now() - startedAt - pausedTotal());
|
|
116483
116499
|
error = err;
|
|
116484
116500
|
for (const incident of err?.incidents || []) recordIncident(incident);
|
|
116485
116501
|
errorCategory = elapsed >= totalMs - 30 || err?.name === "AbortError" ? "timeout" : decisionErrorCategory(err);
|
|
116486
|
-
if (errorCategory === "timeout")
|
|
116487
|
-
|
|
116488
|
-
this.timeBanks[agent.id] = 0;
|
|
116489
|
-
} else if (errorCategory === "rate_limit") stats.rateLimits++;
|
|
116502
|
+
if (errorCategory === "timeout") stats.timeouts++;
|
|
116503
|
+
else if (errorCategory === "rate_limit") stats.rateLimits++;
|
|
116490
116504
|
else if (errorCategory === "provider") stats.providerErrors++;
|
|
116491
116505
|
else {
|
|
116492
116506
|
stats.modelErrors++;
|
|
@@ -116495,6 +116509,8 @@ var TournamentDirector = class {
|
|
|
116495
116509
|
}
|
|
116496
116510
|
if (this.status === "STOPPED") throw new Error("Tournament stopped");
|
|
116497
116511
|
await this.waitIfPaused();
|
|
116512
|
+
const elapsedActive = Math.max(0, Date.now() - startedAt - pausedTotal());
|
|
116513
|
+
this.timeBanks[agent.id] = Math.max(0, bankBefore - Math.max(0, elapsedActive - baseMs));
|
|
116498
116514
|
let chosen = result?.action ?? null, forced = false;
|
|
116499
116515
|
if (chosen && architecture === "hierarchical") {
|
|
116500
116516
|
const engineAction = isAggressiveType(chosen.type) ? { type: chosen.type, playerId: agent.id, amount: Math.max(1, Math.round(asNumber(chosen.amount))) } : { type: chosen.type, playerId: agent.id };
|
|
@@ -117176,7 +117192,7 @@ function renderStatus(s) {
|
|
|
117176
117192
|
els.pauseBtn.classList.toggle("hidden", !running);
|
|
117177
117193
|
els.stopBtn.classList.toggle("hidden", !running);
|
|
117178
117194
|
els.startTopBtn.classList.toggle("hidden", running);
|
|
117179
|
-
els.seatsBtn.classList.toggle("hidden", running);
|
|
117195
|
+
els.seatsBtn.classList.toggle("hidden", running || !["STOPPED", "FINISHED", "ERROR"].includes(status));
|
|
117180
117196
|
els.setupBtn.disabled = running;
|
|
117181
117197
|
els.testsBtn.disabled = running;
|
|
117182
117198
|
const pauseLabel = $(".action-label", els.pauseBtn);
|
|
@@ -117509,7 +117525,7 @@ function renderStats(s) {
|
|
|
117509
117525
|
const st = s.stats?.[p.id] || {}, poker = publicById.get(p.id) || {}, tableP = s.table?.players?.find?.((x) => x?.id === p.id), avg = st.decisions ? Math.round(st.totalLatencyMs / st.decisions) : 0;
|
|
117510
117526
|
const pc = (value) => `${Math.round(Number(value || 0) * 100)}%`;
|
|
117511
117527
|
return `<div class="stat-card"><div class="stat-top"><div><div class="stat-name">${escapeHtml(visiblePlayerName(p.name, p.model))}</div><div class="stat-model mono">${escapeHtml(p.model)} \xB7 ${escapeHtml(effectiveProtocol(p, s.config.connections.find((c) => c.id === p.connectionId)))}</div></div><strong>${fmt(tableP?.stack || 0)}</strong></div>
|
|
117512
|
-
<div class="poker-profile"><div><b>${pc(poker.vpipPct)}</b><span>VPIP</span></div><div><b>${pc(poker.pfrPct)}</b><span>PFR</span></div><div><b>${pc(poker.aggressionPct)}</b><span>
|
|
117528
|
+
<div class="poker-profile"><div title="Voluntarily put chips in pot"><b>${pc(poker.vpipPct)}</b><span>VPIP</span></div><div title="Preflop raise"><b>${pc(poker.pfrPct)}</b><span>PFR</span></div><div title="Aggression frequency"><b>${pc(poker.aggressionPct)}</b><span>AFq</span></div><div title="Fold when folding was legal"><b>${pc(poker.foldPct)}</b><span>FOLD</span></div><div><b>${poker.sampleHands || 0}</b><span>HANDS</span></div></div>
|
|
117513
117529
|
<div class="stat-values"><div class="metric"><b>${st.decisions || 0}</b><span>moves</span></div><div class="metric"><b>${avg}ms</b><span>avg</span></div><div class="metric"><b>${st.autoFallbacks || 0}</b><span>auto</span></div></div><div class="stat-reliability"><span><b>${st.modelErrors || 0}</b> model</span><span><b>${st.providerErrors || 0}</b> provider</span><span><b>${st.rateLimits || 0}</b> rate</span><span><b>${st.timeouts || 0}</b> timeout</span><span><b>${st.protocolFallbacks || 0}</b> protocol</span><span><b>${st.retries || 0}</b> retry</span></div></div>`;
|
|
117514
117530
|
}).join("");
|
|
117515
117531
|
}
|
|
@@ -117545,7 +117561,7 @@ function decisionRenderSignature(s) {
|
|
|
117545
117561
|
return JSON.stringify([d ? [d.id, d.playerId, d.model, d.protocol, d.provider, d.startedAt, d.baseMs, d.timeBankMs, d.pausedMs || 0, Boolean(d.pausedAt), d.architecture || null, d.stage || null, d.legalActions] : null, last?.id || null, s?.status || "IDLE", seatAssignments.filter(Boolean).length]);
|
|
117546
117562
|
}
|
|
117547
117563
|
function feedRenderSignature(s) {
|
|
117548
|
-
return (s?.events || []).filter((e) => e.type === "DECISION").slice(-
|
|
117564
|
+
return (s?.events || []).filter((e) => e.type === "DECISION" || e.type === "SPECTATOR_EXPLANATION").slice(-24).map((e) => `${e.type}:${e.id}`).join("|");
|
|
117549
117565
|
}
|
|
117550
117566
|
function eventsRenderSignature(s) {
|
|
117551
117567
|
const ev = s?.events || [];
|
|
@@ -117786,6 +117802,7 @@ function setRecordButton(active, label = null) {
|
|
|
117786
117802
|
if (icon) icon.textContent = active ? "\u25A0" : "\u25CF";
|
|
117787
117803
|
if (text) text.textContent = label || (active ? "Stop rec" : "Record");
|
|
117788
117804
|
els.recordBtn.title = active ? "Stop table recording and save video" : "Record only the poker table";
|
|
117805
|
+
els.recordBtn.setAttribute("aria-label", els.recordBtn.title);
|
|
117789
117806
|
}
|
|
117790
117807
|
function saveRecordingBlob(blob) {
|
|
117791
117808
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -118261,6 +118278,8 @@ els.soundBtn.addEventListener("click", async () => {
|
|
|
118261
118278
|
if (label) label.textContent = soundEnabled ? "Sound on" : "Sound";
|
|
118262
118279
|
els.soundBtn.setAttribute("aria-pressed", String(soundEnabled));
|
|
118263
118280
|
els.soundBtn.classList.toggle("active", soundEnabled);
|
|
118281
|
+
els.soundBtn.title = soundEnabled ? "Disable table sounds" : "Enable table sounds";
|
|
118282
|
+
els.soundBtn.setAttribute("aria-label", els.soundBtn.title);
|
|
118264
118283
|
if (soundEnabled) {
|
|
118265
118284
|
try {
|
|
118266
118285
|
await getAudioContext()?.resume();
|
|
@@ -118301,8 +118320,7 @@ els.exportBtn.addEventListener("click", () => {
|
|
|
118301
118320
|
});
|
|
118302
118321
|
els.seatsBtn.addEventListener("click", () => {
|
|
118303
118322
|
if (director && ["RUNNING", "PAUSED"].includes(director.status)) return;
|
|
118304
|
-
|
|
118305
|
-
render(currentState || { status: "IDLE", events: [] });
|
|
118323
|
+
void startConfiguredTournament();
|
|
118306
118324
|
});
|
|
118307
118325
|
els.seatsLayer.addEventListener("click", (event) => {
|
|
118308
118326
|
const seat = event.target.closest("[data-lobby-seat]");
|
|
@@ -118363,6 +118381,7 @@ els.setupForm.addEventListener("submit", (event) => {
|
|
|
118363
118381
|
try {
|
|
118364
118382
|
const raw = collectSetupRaw(true);
|
|
118365
118383
|
for (const connection of raw.connections) parseHeaders(connection.headers);
|
|
118384
|
+
normalizeConfig(raw);
|
|
118366
118385
|
saveSetupWithoutSecrets(raw);
|
|
118367
118386
|
els.setupDialog.close();
|
|
118368
118387
|
render(currentState || { status: "IDLE", events: [] });
|
|
@@ -118403,10 +118422,20 @@ async function startConfiguredTournament() {
|
|
|
118403
118422
|
}
|
|
118404
118423
|
}
|
|
118405
118424
|
els.startTopBtn.addEventListener("click", startConfiguredTournament);
|
|
118406
|
-
|
|
118425
|
+
function activateInspectorTab(tab, { focus = false } = {}) {
|
|
118407
118426
|
activeInspectorTab = tab.dataset.tab || "live";
|
|
118408
|
-
$$(".tab").forEach((t) =>
|
|
118409
|
-
|
|
118427
|
+
$$(".tab").forEach((t) => {
|
|
118428
|
+
const active = t === tab;
|
|
118429
|
+
t.classList.toggle("active", active);
|
|
118430
|
+
t.setAttribute("aria-selected", String(active));
|
|
118431
|
+
t.tabIndex = active ? 0 : -1;
|
|
118432
|
+
});
|
|
118433
|
+
$$(".tab-panel").forEach((p) => {
|
|
118434
|
+
const active = p.id === `tab-${tab.dataset.tab}`;
|
|
118435
|
+
p.classList.toggle("active", active);
|
|
118436
|
+
p.hidden = !active;
|
|
118437
|
+
});
|
|
118438
|
+
if (focus) tab.focus();
|
|
118410
118439
|
if (!currentState) return;
|
|
118411
118440
|
if (activeInspectorTab === "live") {
|
|
118412
118441
|
renderMemo.feed = feedRenderSignature(currentState);
|
|
@@ -118418,7 +118447,18 @@ $$(".tab").forEach((tab) => tab.addEventListener("click", () => {
|
|
|
118418
118447
|
renderMemo.stats = statsRenderSignature(currentState);
|
|
118419
118448
|
renderStats(currentState);
|
|
118420
118449
|
}
|
|
118421
|
-
}
|
|
118450
|
+
}
|
|
118451
|
+
$$(".tab").forEach((tab) => {
|
|
118452
|
+
tab.addEventListener("click", () => activateInspectorTab(tab));
|
|
118453
|
+
tab.addEventListener("keydown", (event) => {
|
|
118454
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
|
|
118455
|
+
event.preventDefault();
|
|
118456
|
+
const tabs = $$(".tab");
|
|
118457
|
+
const current = tabs.indexOf(tab);
|
|
118458
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (current + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
|
|
118459
|
+
activateInspectorTab(tabs[next], { focus: true });
|
|
118460
|
+
});
|
|
118461
|
+
});
|
|
118422
118462
|
window.addEventListener("beforeunload", (event) => {
|
|
118423
118463
|
if (tableRecording) tableRecording.stream?.getTracks?.().forEach((t) => t.stop());
|
|
118424
118464
|
if (director && ["RUNNING", "PAUSED"].includes(director.status)) {
|
package/dist/index.html
CHANGED
|
@@ -64,17 +64,14 @@
|
|
|
64
64
|
<div class="brand-title">pokertools-arena</div>
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
67
|
+
<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>
|
|
67
68
|
<div class="topbar-actions">
|
|
68
69
|
<button id="startTopBtn" class="button primary top-action" type="button" title="Start tournament"><span class="action-icon">▶</span><span class="action-label">Start</span></button>
|
|
69
|
-
<button id="seatsBtn" class="button ghost top-action" type="button" title="
|
|
70
|
-
<button id="setupBtn" class="button ghost top-action" type="button" title="Connections and tournament settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
|
|
71
|
-
<button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
|
|
72
|
-
<button id="soundBtn" class="button ghost top-action sound-button" type="button" aria-pressed="false" title="Enable table sounds"><span class="action-icon">♪</span><span class="action-label">Sound</span></button>
|
|
73
|
-
<button id="recordBtn" class="button ghost top-action record-button" type="button" aria-pressed="false" title="Record only the poker table"><span class="action-icon">●</span><span class="action-label">Record</span></button>
|
|
74
|
-
<button id="exportBtn" class="button ghost top-action" type="button" disabled title="Export tournament log"><span class="action-icon">↓</span><span class="action-label">Log</span></button>
|
|
70
|
+
<button id="seatsBtn" class="button ghost top-action hidden" type="button" title="Restart with a new tournament"><span class="action-icon">↻</span><span class="action-label">Restart</span></button>
|
|
75
71
|
<button id="pauseBtn" class="button ghost top-action hidden" type="button"><span class="action-icon">Ⅱ</span><span class="action-label">Pause</span></button>
|
|
76
72
|
<button id="stopBtn" class="button danger top-action hidden" type="button"><span class="action-icon">■</span><span class="action-label">Stop</span></button>
|
|
77
|
-
<
|
|
73
|
+
<button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
|
|
74
|
+
<button id="setupBtn" class="button ghost top-action icon-only-action" type="button" title="Connections and tournament settings" aria-label="Settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
|
|
78
75
|
</div>
|
|
79
76
|
</header>
|
|
80
77
|
|
|
@@ -83,6 +80,10 @@
|
|
|
83
80
|
<div class="arena-head">
|
|
84
81
|
<div class="arena-status"><span id="statusDot" class="status-dot"></span><span id="statusLabel">IDLE</span></div>
|
|
85
82
|
<div id="tournamentMeta" class="mono muted">Seat 2–10 models, then press Start</div>
|
|
83
|
+
<div class="arena-head-actions">
|
|
84
|
+
<button id="soundBtn" class="arena-icon-button sound-button" type="button" aria-pressed="false" title="Enable table sounds" aria-label="Enable table sounds"><span class="action-icon">♪</span></button>
|
|
85
|
+
<button id="recordBtn" class="arena-icon-button record-button" type="button" aria-pressed="false" title="Record only the poker table" aria-label="Record only the poker table"><span class="action-icon">●</span></button>
|
|
86
|
+
</div>
|
|
86
87
|
</div>
|
|
87
88
|
|
|
88
89
|
<div id="winnerBanner" class="winner-banner hidden" role="status" aria-live="polite"></div>
|
|
@@ -109,13 +110,13 @@
|
|
|
109
110
|
</section>
|
|
110
111
|
|
|
111
112
|
<aside class="inspector">
|
|
112
|
-
<div class="tabs">
|
|
113
|
-
<button class="tab active" data-tab="live">Live</button>
|
|
114
|
-
<button class="tab" data-tab="log">Log</button>
|
|
115
|
-
<button class="tab" data-tab="stats">Stats</button>
|
|
113
|
+
<div class="tabs" role="tablist" aria-label="Tournament inspector">
|
|
114
|
+
<button id="tabButton-live" class="tab active" data-tab="live" type="button" role="tab" aria-selected="true" aria-controls="tab-live">Live</button>
|
|
115
|
+
<button id="tabButton-log" class="tab" data-tab="log" type="button" role="tab" aria-selected="false" aria-controls="tab-log" tabindex="-1">Log</button>
|
|
116
|
+
<button id="tabButton-stats" class="tab" data-tab="stats" type="button" role="tab" aria-selected="false" aria-controls="tab-stats" tabindex="-1">Stats</button>
|
|
116
117
|
</div>
|
|
117
118
|
|
|
118
|
-
<div id="tab-live" class="tab-panel active">
|
|
119
|
+
<div id="tab-live" class="tab-panel active" role="tabpanel" aria-labelledby="tabButton-live">
|
|
119
120
|
<section class="panel-block decision-panel">
|
|
120
121
|
<div id="decisionPanelTitle" class="panel-title">Current decision</div>
|
|
121
122
|
<div id="decisionEmpty" class="empty-state">Configure seats on the table. During play, the active model, clock, and legal actions appear here.</div>
|
|
@@ -137,8 +138,13 @@
|
|
|
137
138
|
<section class="panel-block"><div class="panel-title">Recent decisions</div><div id="decisionFeed" class="decision-feed"></div></section>
|
|
138
139
|
</div>
|
|
139
140
|
|
|
140
|
-
<div id="tab-log" class="tab-panel"
|
|
141
|
-
|
|
141
|
+
<div id="tab-log" class="tab-panel" role="tabpanel" aria-labelledby="tabButton-log" hidden>
|
|
142
|
+
<section class="panel-block grow">
|
|
143
|
+
<div class="panel-title-row"><div class="panel-title">Tournament events</div><button id="exportBtn" class="button ghost small-button" type="button" disabled title="Download the tournament log as JSONL"><span class="action-icon">↓</span><span class="action-label">Download</span></button></div>
|
|
144
|
+
<div id="eventLog" class="event-log mono" aria-label="Tournament event log"></div>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
<div id="tab-stats" class="tab-panel" role="tabpanel" aria-labelledby="tabButton-stats" hidden><section class="panel-block grow"><div class="panel-title">Models</div><div id="statsGrid" class="stats-grid"></div></section></div>
|
|
142
148
|
</aside>
|
|
143
149
|
</main>
|
|
144
150
|
|