pokertools-arena 0.4.4 → 0.4.8
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 +37 -10
- package/dist/index.html +14 -8
- package/dist/pokertools-arena.html +112 -18
- package/dist/styles.css +61 -0
- package/docs/diagnostics/SUMMARY.md +1 -1
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +41 -0
- package/package.json +1 -1
- package/src/app.js +56 -10
- package/src/index.html +14 -8
- package/src/styles.css +61 -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.8.** 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, and the header, table chrome and small-screen board cards are more compact.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
package/dist/app.js
CHANGED
|
@@ -115700,6 +115700,16 @@ function showActionToast(text, type = "") {
|
|
|
115700
115700
|
function seatEl(playerId) {
|
|
115701
115701
|
return $(`.seat[data-player-id="${CSS.escape(String(playerId))}"]`, els.seatsLayer);
|
|
115702
115702
|
}
|
|
115703
|
+
function stopTableEffects() {
|
|
115704
|
+
clearTimeout(showActionToast.timer);
|
|
115705
|
+
els.actionToast?.classList.add("hidden");
|
|
115706
|
+
els.pokerTable?.classList.remove("action-message-visible");
|
|
115707
|
+
els.fxLayer?.replaceChildren();
|
|
115708
|
+
for (const el of $$(".seat.fold-flash, .seat.check-flash")) {
|
|
115709
|
+
el.classList.remove("fold-flash");
|
|
115710
|
+
el.classList.remove("check-flash");
|
|
115711
|
+
}
|
|
115712
|
+
}
|
|
115703
115713
|
function animateNewHand(s) {
|
|
115704
115714
|
if (!animationsAllowed()) return;
|
|
115705
115715
|
requestAnimationFrame(() => {
|
|
@@ -115727,12 +115737,18 @@ function animateBoardCards(previousCount, currentCount) {
|
|
|
115727
115737
|
}
|
|
115728
115738
|
function processVisualEffects(s) {
|
|
115729
115739
|
const events = s?.events || [];
|
|
115740
|
+
const previous = lastVisualState;
|
|
115741
|
+
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
115742
|
+
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
115743
|
+
lastVisualState = s;
|
|
115744
|
+
stopTableEffects();
|
|
115745
|
+
return;
|
|
115746
|
+
}
|
|
115730
115747
|
if (!animationsAllowed()) {
|
|
115731
115748
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
115732
115749
|
lastVisualState = s;
|
|
115733
115750
|
return;
|
|
115734
115751
|
}
|
|
115735
|
-
const previous = lastVisualState;
|
|
115736
115752
|
if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
|
|
115737
115753
|
const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
|
|
115738
115754
|
if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
|
|
@@ -115758,12 +115774,14 @@ function processVisualEffects(s) {
|
|
|
115758
115774
|
}
|
|
115759
115775
|
showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} \xB7 ${e.action?.description || type}`, type);
|
|
115760
115776
|
}
|
|
115761
|
-
if (e.type === "HAND_END"
|
|
115762
|
-
const before = new Map((previous
|
|
115763
|
-
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])];
|
|
115764
115782
|
const pot = $(".hud-pot", document);
|
|
115765
|
-
|
|
115766
|
-
if (
|
|
115783
|
+
winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
|
|
115784
|
+
if (winnerIds.length) playTableSound("winner");
|
|
115767
115785
|
}
|
|
115768
115786
|
if (e.type === "TOURNAMENT_END") playTableSound("winner");
|
|
115769
115787
|
}
|
|
@@ -116321,6 +116339,7 @@ var TournamentDirector = class {
|
|
|
116321
116339
|
await this.waitIfPaused();
|
|
116322
116340
|
this.pruneBustedSeats();
|
|
116323
116341
|
this.handNumber++;
|
|
116342
|
+
for (const stat of Object.values(this.stats)) stat.lastAction = null;
|
|
116324
116343
|
this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
|
|
116325
116344
|
try {
|
|
116326
116345
|
this.engine.deal();
|
|
@@ -117160,7 +117179,7 @@ function renderStatus(s) {
|
|
|
117160
117179
|
els.pauseBtn.classList.toggle("hidden", !running);
|
|
117161
117180
|
els.stopBtn.classList.toggle("hidden", !running);
|
|
117162
117181
|
els.startTopBtn.classList.toggle("hidden", running);
|
|
117163
|
-
els.seatsBtn.classList.toggle("hidden", running);
|
|
117182
|
+
els.seatsBtn.classList.toggle("hidden", running || !["STOPPED", "FINISHED", "ERROR"].includes(status));
|
|
117164
117183
|
els.setupBtn.disabled = running;
|
|
117165
117184
|
els.testsBtn.disabled = running;
|
|
117166
117185
|
const pauseLabel = $(".action-label", els.pauseBtn);
|
|
@@ -117353,6 +117372,10 @@ function championBadgeHtml() {
|
|
|
117353
117372
|
return '<span class="champion-badge" role="img" aria-label="Tournament champion"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.6 2.6h10.8v4.7a5.4 5.4 0 0 1-10.8 0V2.6Z"/><path d="M10.7 12.4h2.6V15h-2.6z"/><path d="M7.2 15h9.6v2.1H7.2z"/><path d="M5.6 18.2h12.8v2.3H5.6z"/></svg></span>';
|
|
117354
117373
|
}
|
|
117355
117374
|
function renderDecision(s) {
|
|
117375
|
+
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
117376
|
+
stopClock();
|
|
117377
|
+
return;
|
|
117378
|
+
}
|
|
117356
117379
|
const d = s?.currentDecision;
|
|
117357
117380
|
const last = latestDecisionEvent(s);
|
|
117358
117381
|
const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
|
|
@@ -118269,15 +118292,19 @@ els.pauseBtn.addEventListener("click", () => {
|
|
|
118269
118292
|
if (!director) return;
|
|
118270
118293
|
currentState?.status === "PAUSED" ? director.resume() : director.pause();
|
|
118271
118294
|
});
|
|
118272
|
-
els.stopBtn.addEventListener("click", () =>
|
|
118295
|
+
els.stopBtn.addEventListener("click", () => {
|
|
118296
|
+
if (!director) return;
|
|
118297
|
+
stopClock();
|
|
118298
|
+
stopTableEffects();
|
|
118299
|
+
director.stop();
|
|
118300
|
+
});
|
|
118273
118301
|
els.exportBtn.addEventListener("click", () => {
|
|
118274
118302
|
if (!director?.events?.length) return;
|
|
118275
118303
|
downloadText(`${director.config?.id || "pokertools-arena"}.jsonl`, director.exportJsonl());
|
|
118276
118304
|
});
|
|
118277
118305
|
els.seatsBtn.addEventListener("click", () => {
|
|
118278
118306
|
if (director && ["RUNNING", "PAUSED"].includes(director.status)) return;
|
|
118279
|
-
|
|
118280
|
-
render(currentState || { status: "IDLE", events: [] });
|
|
118307
|
+
void startConfiguredTournament();
|
|
118281
118308
|
});
|
|
118282
118309
|
els.seatsLayer.addEventListener("click", (event) => {
|
|
118283
118310
|
const seat = event.target.closest("[data-lobby-seat]");
|
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>
|
|
@@ -137,7 +138,12 @@
|
|
|
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
|
+
<div id="tab-log" class="tab-panel">
|
|
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"></div>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
141
147
|
<div id="tab-stats" class="tab-panel"><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>
|
|
@@ -2344,6 +2344,67 @@ a.button{text-decoration:none}
|
|
|
2344
2344
|
/* 0.4.0 — methodology, reproducibility and repository-organization release.
|
|
2345
2345
|
Browser application source moved under src/; visual behavior is unchanged. */
|
|
2346
2346
|
|
|
2347
|
+
/* 0.4.8 — compact header, table-card parity, quieter chrome.
|
|
2348
|
+
|
|
2349
|
+
Header: brand left, a backgroundless GitHub mark, then actions right. The
|
|
2350
|
+
board uses the same rank-plus-center-suit card language as the hole cards on
|
|
2351
|
+
small screens instead of the crowded duplicated corners. */
|
|
2352
|
+
.topbar{flex-wrap:nowrap}
|
|
2353
|
+
.brandbar{flex:0 1 auto}
|
|
2354
|
+
.repo-link{display:inline-grid;place-items:center;width:26px;height:26px;margin-left:6px;border:0;border-radius:7px;background:none;color:var(--muted);flex:0 0 auto;transition:color .15s ease,opacity .15s ease}
|
|
2355
|
+
.repo-link .repo-icon{width:17px;height:17px;opacity:.9}
|
|
2356
|
+
.repo-link:hover{color:var(--text)}
|
|
2357
|
+
.repo-link:hover .repo-icon{opacity:1}
|
|
2358
|
+
.topbar-actions{margin-left:auto;flex:0 1 auto}
|
|
2359
|
+
/* Settings is an icon-only control: no label, no wasted width. */
|
|
2360
|
+
.icon-only-action{padding:0;width:30px;min-width:30px;justify-content:center}
|
|
2361
|
+
.icon-only-action .action-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
2362
|
+
|
|
2363
|
+
/* Sound and recording live with the table status they affect. */
|
|
2364
|
+
.arena-head{gap:10px}
|
|
2365
|
+
.arena-head-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto}
|
|
2366
|
+
.arena-icon-button{display:inline-grid;place-items:center;width:24px;height:24px;min-width:24px;padding:0;border:1px solid var(--modal-border,rgba(232,238,243,.09));border-radius:7px;background:rgba(255,255,255,.02);color:#8d98a2;font-size:10px;cursor:pointer;transition:color .14s ease,border-color .14s ease,background .14s ease}
|
|
2367
|
+
.arena-icon-button:hover{color:#d8e0e5;border-color:rgba(146,201,164,.3)}
|
|
2368
|
+
.arena-icon-button.active{color:var(--accent);border-color:rgba(114,185,138,.35);background:rgba(114,185,138,.08)}
|
|
2369
|
+
.arena-icon-button.record-button.active{color:var(--danger);border-color:rgba(200,94,105,.4);background:rgba(200,94,105,.1)}
|
|
2370
|
+
.arena-icon-button:disabled{opacity:.42;cursor:not-allowed}
|
|
2371
|
+
.arena-head-actions .action-icon{font-size:10px;opacity:1}
|
|
2372
|
+
|
|
2373
|
+
/* Download lives inside the Log tab it produces. */
|
|
2374
|
+
.panel-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px}
|
|
2375
|
+
.panel-title-row .panel-title{margin-bottom:0}
|
|
2376
|
+
.panel-title-row .button{flex:0 0 auto}
|
|
2377
|
+
|
|
2378
|
+
/* The bank clock leads the right-hand clock block instead of trailing it. */
|
|
2379
|
+
.clock-wrap{align-items:flex-start;text-align:left}
|
|
2380
|
+
#bankClock{order:-1;color:var(--muted);font-size:9px;line-height:1.2}
|
|
2381
|
+
|
|
2382
|
+
/* Board cards: rank + big centre suit, no duplicated corners. This is the
|
|
2383
|
+
default at small widths and every compact/tight/micro density, so table and
|
|
2384
|
+
hole cards read the same way. */
|
|
2385
|
+
@media(max-width:720px){
|
|
2386
|
+
.board .card .card-corner.bottom,
|
|
2387
|
+
.poker-table[data-density="compact"] .board .card .card-corner.bottom,
|
|
2388
|
+
.poker-table[data-density="tight"] .board .card .card-corner.bottom{display:none!important}
|
|
2389
|
+
.board .card .card-corner i,
|
|
2390
|
+
.poker-table[data-density="compact"] .board .card .card-corner i,
|
|
2391
|
+
.poker-table[data-density="tight"] .board .card .card-corner i{display:none!important}
|
|
2392
|
+
.board .card .card-corner,
|
|
2393
|
+
.poker-table[data-density="compact"] .board .card .card-corner,
|
|
2394
|
+
.poker-table[data-density="tight"] .board .card .card-corner{left:3px!important;top:3px!important;line-height:1!important}
|
|
2395
|
+
.board .card .card-corner b,
|
|
2396
|
+
.poker-table[data-density="compact"] .board .card .card-corner b,
|
|
2397
|
+
.poker-table[data-density="tight"] .board .card .card-corner b{font-size:10px!important}
|
|
2398
|
+
.board .card .card-suit,
|
|
2399
|
+
.poker-table[data-density="compact"] .board .card .card-suit,
|
|
2400
|
+
.poker-table[data-density="tight"] .board .card .card-suit{font-size:17px!important}
|
|
2401
|
+
}
|
|
2402
|
+
.poker-table[data-density="tight"] .board .card .card-suit{font-size:16px!important}
|
|
2403
|
+
.poker-table[data-density="micro"] .board .card .card-corner i,
|
|
2404
|
+
.poker-table[data-density="micro"] .board .card .card-corner.bottom{display:none!important}
|
|
2405
|
+
.poker-table[data-density="micro"] .board .card .card-suit{font-size:13px!important}
|
|
2406
|
+
|
|
2407
|
+
|
|
2347
2408
|
</style>
|
|
2348
2409
|
</head>
|
|
2349
2410
|
<body>
|
|
@@ -2355,17 +2416,14 @@ a.button{text-decoration:none}
|
|
|
2355
2416
|
<div class="brand-title">pokertools-arena</div>
|
|
2356
2417
|
</div>
|
|
2357
2418
|
</div>
|
|
2419
|
+
<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>
|
|
2358
2420
|
<div class="topbar-actions">
|
|
2359
2421
|
<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>
|
|
2360
|
-
<button id="seatsBtn" class="button ghost top-action" type="button" title="
|
|
2361
|
-
<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>
|
|
2362
|
-
<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>
|
|
2363
|
-
<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>
|
|
2364
|
-
<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>
|
|
2365
|
-
<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>
|
|
2422
|
+
<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>
|
|
2366
2423
|
<button id="pauseBtn" class="button ghost top-action hidden" type="button"><span class="action-icon">Ⅱ</span><span class="action-label">Pause</span></button>
|
|
2367
2424
|
<button id="stopBtn" class="button danger top-action hidden" type="button"><span class="action-icon">■</span><span class="action-label">Stop</span></button>
|
|
2368
|
-
<
|
|
2425
|
+
<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>
|
|
2426
|
+
<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>
|
|
2369
2427
|
</div>
|
|
2370
2428
|
</header>
|
|
2371
2429
|
|
|
@@ -2374,6 +2432,10 @@ a.button{text-decoration:none}
|
|
|
2374
2432
|
<div class="arena-head">
|
|
2375
2433
|
<div class="arena-status"><span id="statusDot" class="status-dot"></span><span id="statusLabel">IDLE</span></div>
|
|
2376
2434
|
<div id="tournamentMeta" class="mono muted">Seat 2–10 models, then press Start</div>
|
|
2435
|
+
<div class="arena-head-actions">
|
|
2436
|
+
<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>
|
|
2437
|
+
<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>
|
|
2438
|
+
</div>
|
|
2377
2439
|
</div>
|
|
2378
2440
|
|
|
2379
2441
|
<div id="winnerBanner" class="winner-banner hidden" role="status" aria-live="polite"></div>
|
|
@@ -2428,7 +2490,12 @@ a.button{text-decoration:none}
|
|
|
2428
2490
|
<section class="panel-block"><div class="panel-title">Recent decisions</div><div id="decisionFeed" class="decision-feed"></div></section>
|
|
2429
2491
|
</div>
|
|
2430
2492
|
|
|
2431
|
-
<div id="tab-log" class="tab-panel"
|
|
2493
|
+
<div id="tab-log" class="tab-panel">
|
|
2494
|
+
<section class="panel-block grow">
|
|
2495
|
+
<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>
|
|
2496
|
+
<div id="eventLog" class="event-log mono"></div>
|
|
2497
|
+
</section>
|
|
2498
|
+
</div>
|
|
2432
2499
|
<div id="tab-stats" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Models</div><div id="statsGrid" class="stats-grid"></div></section></div>
|
|
2433
2500
|
</aside>
|
|
2434
2501
|
</main>
|
|
@@ -118375,6 +118442,16 @@ function showActionToast(text, type = "") {
|
|
|
118375
118442
|
function seatEl(playerId) {
|
|
118376
118443
|
return $(`.seat[data-player-id="${CSS.escape(String(playerId))}"]`, els.seatsLayer);
|
|
118377
118444
|
}
|
|
118445
|
+
function stopTableEffects() {
|
|
118446
|
+
clearTimeout(showActionToast.timer);
|
|
118447
|
+
els.actionToast?.classList.add("hidden");
|
|
118448
|
+
els.pokerTable?.classList.remove("action-message-visible");
|
|
118449
|
+
els.fxLayer?.replaceChildren();
|
|
118450
|
+
for (const el of $$(".seat.fold-flash, .seat.check-flash")) {
|
|
118451
|
+
el.classList.remove("fold-flash");
|
|
118452
|
+
el.classList.remove("check-flash");
|
|
118453
|
+
}
|
|
118454
|
+
}
|
|
118378
118455
|
function animateNewHand(s) {
|
|
118379
118456
|
if (!animationsAllowed()) return;
|
|
118380
118457
|
requestAnimationFrame(() => {
|
|
@@ -118402,12 +118479,18 @@ function animateBoardCards(previousCount, currentCount) {
|
|
|
118402
118479
|
}
|
|
118403
118480
|
function processVisualEffects(s) {
|
|
118404
118481
|
const events = s?.events || [];
|
|
118482
|
+
const previous = lastVisualState;
|
|
118483
|
+
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
118484
|
+
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
118485
|
+
lastVisualState = s;
|
|
118486
|
+
stopTableEffects();
|
|
118487
|
+
return;
|
|
118488
|
+
}
|
|
118405
118489
|
if (!animationsAllowed()) {
|
|
118406
118490
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
118407
118491
|
lastVisualState = s;
|
|
118408
118492
|
return;
|
|
118409
118493
|
}
|
|
118410
|
-
const previous = lastVisualState;
|
|
118411
118494
|
if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
|
|
118412
118495
|
const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
|
|
118413
118496
|
if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
|
|
@@ -118433,12 +118516,14 @@ function processVisualEffects(s) {
|
|
|
118433
118516
|
}
|
|
118434
118517
|
showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} \xB7 ${e.action?.description || type}`, type);
|
|
118435
118518
|
}
|
|
118436
|
-
if (e.type === "HAND_END"
|
|
118437
|
-
const before = new Map((previous
|
|
118438
|
-
const
|
|
118519
|
+
if (e.type === "HAND_END") {
|
|
118520
|
+
const before = new Map((previous?.table?.players || []).filter(Boolean).map((p) => [p.id, Number(p.stack || 0)]));
|
|
118521
|
+
const payloadIds = (e.winners || []).map((w) => w?.playerId ?? w?.id ?? w).filter(Boolean);
|
|
118522
|
+
const fromStacks = (s?.table?.players || []).filter((p) => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0))).map((p) => p.id);
|
|
118523
|
+
const winnerIds = [.../* @__PURE__ */ new Set([...payloadIds, ...fromStacks])];
|
|
118439
118524
|
const pot = $(".hud-pot", document);
|
|
118440
|
-
|
|
118441
|
-
if (
|
|
118525
|
+
winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
|
|
118526
|
+
if (winnerIds.length) playTableSound("winner");
|
|
118442
118527
|
}
|
|
118443
118528
|
if (e.type === "TOURNAMENT_END") playTableSound("winner");
|
|
118444
118529
|
}
|
|
@@ -118996,6 +119081,7 @@ var TournamentDirector = class {
|
|
|
118996
119081
|
await this.waitIfPaused();
|
|
118997
119082
|
this.pruneBustedSeats();
|
|
118998
119083
|
this.handNumber++;
|
|
119084
|
+
for (const stat of Object.values(this.stats)) stat.lastAction = null;
|
|
118999
119085
|
this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
|
|
119000
119086
|
try {
|
|
119001
119087
|
this.engine.deal();
|
|
@@ -119835,7 +119921,7 @@ function renderStatus(s) {
|
|
|
119835
119921
|
els.pauseBtn.classList.toggle("hidden", !running);
|
|
119836
119922
|
els.stopBtn.classList.toggle("hidden", !running);
|
|
119837
119923
|
els.startTopBtn.classList.toggle("hidden", running);
|
|
119838
|
-
els.seatsBtn.classList.toggle("hidden", running);
|
|
119924
|
+
els.seatsBtn.classList.toggle("hidden", running || !["STOPPED", "FINISHED", "ERROR"].includes(status));
|
|
119839
119925
|
els.setupBtn.disabled = running;
|
|
119840
119926
|
els.testsBtn.disabled = running;
|
|
119841
119927
|
const pauseLabel = $(".action-label", els.pauseBtn);
|
|
@@ -120028,6 +120114,10 @@ function championBadgeHtml() {
|
|
|
120028
120114
|
return '<span class="champion-badge" role="img" aria-label="Tournament champion"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.6 2.6h10.8v4.7a5.4 5.4 0 0 1-10.8 0V2.6Z"/><path d="M10.7 12.4h2.6V15h-2.6z"/><path d="M7.2 15h9.6v2.1H7.2z"/><path d="M5.6 18.2h12.8v2.3H5.6z"/></svg></span>';
|
|
120029
120115
|
}
|
|
120030
120116
|
function renderDecision(s) {
|
|
120117
|
+
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
120118
|
+
stopClock();
|
|
120119
|
+
return;
|
|
120120
|
+
}
|
|
120031
120121
|
const d = s?.currentDecision;
|
|
120032
120122
|
const last = latestDecisionEvent(s);
|
|
120033
120123
|
const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
|
|
@@ -120944,15 +121034,19 @@ els.pauseBtn.addEventListener("click", () => {
|
|
|
120944
121034
|
if (!director) return;
|
|
120945
121035
|
currentState?.status === "PAUSED" ? director.resume() : director.pause();
|
|
120946
121036
|
});
|
|
120947
|
-
els.stopBtn.addEventListener("click", () =>
|
|
121037
|
+
els.stopBtn.addEventListener("click", () => {
|
|
121038
|
+
if (!director) return;
|
|
121039
|
+
stopClock();
|
|
121040
|
+
stopTableEffects();
|
|
121041
|
+
director.stop();
|
|
121042
|
+
});
|
|
120948
121043
|
els.exportBtn.addEventListener("click", () => {
|
|
120949
121044
|
if (!director?.events?.length) return;
|
|
120950
121045
|
downloadText(`${director.config?.id || "pokertools-arena"}.jsonl`, director.exportJsonl());
|
|
120951
121046
|
});
|
|
120952
121047
|
els.seatsBtn.addEventListener("click", () => {
|
|
120953
121048
|
if (director && ["RUNNING", "PAUSED"].includes(director.status)) return;
|
|
120954
|
-
|
|
120955
|
-
render(currentState || { status: "IDLE", events: [] });
|
|
121049
|
+
void startConfiguredTournament();
|
|
120956
121050
|
});
|
|
120957
121051
|
els.seatsLayer.addEventListener("click", (event) => {
|
|
120958
121052
|
const seat = event.target.closest("[data-lobby-seat]");
|
package/dist/styles.css
CHANGED
|
@@ -2287,3 +2287,64 @@ a.button{text-decoration:none}
|
|
|
2287
2287
|
|
|
2288
2288
|
/* 0.4.0 — methodology, reproducibility and repository-organization release.
|
|
2289
2289
|
Browser application source moved under src/; visual behavior is unchanged. */
|
|
2290
|
+
|
|
2291
|
+
/* 0.4.8 — compact header, table-card parity, quieter chrome.
|
|
2292
|
+
|
|
2293
|
+
Header: brand left, a backgroundless GitHub mark, then actions right. The
|
|
2294
|
+
board uses the same rank-plus-center-suit card language as the hole cards on
|
|
2295
|
+
small screens instead of the crowded duplicated corners. */
|
|
2296
|
+
.topbar{flex-wrap:nowrap}
|
|
2297
|
+
.brandbar{flex:0 1 auto}
|
|
2298
|
+
.repo-link{display:inline-grid;place-items:center;width:26px;height:26px;margin-left:6px;border:0;border-radius:7px;background:none;color:var(--muted);flex:0 0 auto;transition:color .15s ease,opacity .15s ease}
|
|
2299
|
+
.repo-link .repo-icon{width:17px;height:17px;opacity:.9}
|
|
2300
|
+
.repo-link:hover{color:var(--text)}
|
|
2301
|
+
.repo-link:hover .repo-icon{opacity:1}
|
|
2302
|
+
.topbar-actions{margin-left:auto;flex:0 1 auto}
|
|
2303
|
+
/* Settings is an icon-only control: no label, no wasted width. */
|
|
2304
|
+
.icon-only-action{padding:0;width:30px;min-width:30px;justify-content:center}
|
|
2305
|
+
.icon-only-action .action-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
2306
|
+
|
|
2307
|
+
/* Sound and recording live with the table status they affect. */
|
|
2308
|
+
.arena-head{gap:10px}
|
|
2309
|
+
.arena-head-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto}
|
|
2310
|
+
.arena-icon-button{display:inline-grid;place-items:center;width:24px;height:24px;min-width:24px;padding:0;border:1px solid var(--modal-border,rgba(232,238,243,.09));border-radius:7px;background:rgba(255,255,255,.02);color:#8d98a2;font-size:10px;cursor:pointer;transition:color .14s ease,border-color .14s ease,background .14s ease}
|
|
2311
|
+
.arena-icon-button:hover{color:#d8e0e5;border-color:rgba(146,201,164,.3)}
|
|
2312
|
+
.arena-icon-button.active{color:var(--accent);border-color:rgba(114,185,138,.35);background:rgba(114,185,138,.08)}
|
|
2313
|
+
.arena-icon-button.record-button.active{color:var(--danger);border-color:rgba(200,94,105,.4);background:rgba(200,94,105,.1)}
|
|
2314
|
+
.arena-icon-button:disabled{opacity:.42;cursor:not-allowed}
|
|
2315
|
+
.arena-head-actions .action-icon{font-size:10px;opacity:1}
|
|
2316
|
+
|
|
2317
|
+
/* Download lives inside the Log tab it produces. */
|
|
2318
|
+
.panel-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px}
|
|
2319
|
+
.panel-title-row .panel-title{margin-bottom:0}
|
|
2320
|
+
.panel-title-row .button{flex:0 0 auto}
|
|
2321
|
+
|
|
2322
|
+
/* The bank clock leads the right-hand clock block instead of trailing it. */
|
|
2323
|
+
.clock-wrap{align-items:flex-start;text-align:left}
|
|
2324
|
+
#bankClock{order:-1;color:var(--muted);font-size:9px;line-height:1.2}
|
|
2325
|
+
|
|
2326
|
+
/* Board cards: rank + big centre suit, no duplicated corners. This is the
|
|
2327
|
+
default at small widths and every compact/tight/micro density, so table and
|
|
2328
|
+
hole cards read the same way. */
|
|
2329
|
+
@media(max-width:720px){
|
|
2330
|
+
.board .card .card-corner.bottom,
|
|
2331
|
+
.poker-table[data-density="compact"] .board .card .card-corner.bottom,
|
|
2332
|
+
.poker-table[data-density="tight"] .board .card .card-corner.bottom{display:none!important}
|
|
2333
|
+
.board .card .card-corner i,
|
|
2334
|
+
.poker-table[data-density="compact"] .board .card .card-corner i,
|
|
2335
|
+
.poker-table[data-density="tight"] .board .card .card-corner i{display:none!important}
|
|
2336
|
+
.board .card .card-corner,
|
|
2337
|
+
.poker-table[data-density="compact"] .board .card .card-corner,
|
|
2338
|
+
.poker-table[data-density="tight"] .board .card .card-corner{left:3px!important;top:3px!important;line-height:1!important}
|
|
2339
|
+
.board .card .card-corner b,
|
|
2340
|
+
.poker-table[data-density="compact"] .board .card .card-corner b,
|
|
2341
|
+
.poker-table[data-density="tight"] .board .card .card-corner b{font-size:10px!important}
|
|
2342
|
+
.board .card .card-suit,
|
|
2343
|
+
.poker-table[data-density="compact"] .board .card .card-suit,
|
|
2344
|
+
.poker-table[data-density="tight"] .board .card .card-suit{font-size:17px!important}
|
|
2345
|
+
}
|
|
2346
|
+
.poker-table[data-density="tight"] .board .card .card-suit{font-size:16px!important}
|
|
2347
|
+
.poker-table[data-density="micro"] .board .card .card-corner i,
|
|
2348
|
+
.poker-table[data-density="micro"] .board .card .card-corner.bottom{display:none!important}
|
|
2349
|
+
.poker-table[data-density="micro"] .board .card .card-suit{font-size:13px!important}
|
|
2350
|
+
|
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.4.8 — Maintenance
|
|
4
|
+
|
|
5
|
+
- **Compact header.** Left: the logo, `pokertools-arena`, and a backgroundless
|
|
6
|
+
GitHub mark. Right: Start / Restart / Pause / Stop, Tests, and an icon-only
|
|
7
|
+
Settings control. The `♟ Seats` button became **Restart**, shown only after a
|
|
8
|
+
run has started and stopped or finished.
|
|
9
|
+
- **Sound and recording** moved into the table header (`arena-head`) beside the
|
|
10
|
+
status they affect; **log download** moved into the `Log` tab that produces it.
|
|
11
|
+
- **Bank clock** now leads the right-hand clock block instead of trailing it.
|
|
12
|
+
- **Board cards on small screens** use the same rank-plus-center-suit language as
|
|
13
|
+
the hole cards: the duplicated bottom-right corner and inline suit glyph are
|
|
14
|
+
dropped at narrow widths and at compact/tight/micro densities.
|
|
15
|
+
|
|
16
|
+
## 0.4.7 — Maintenance
|
|
17
|
+
|
|
18
|
+
- **Pot pays out to the winner.** The pot→winner chip animation never fired.
|
|
19
|
+
`processVisualEffects` read `lastVisualState` *after* `animateNewHand` and
|
|
20
|
+
`animateBoardCards` had already advanced it, so at a hand boundary the
|
|
21
|
+
`HAND_END` stack diff compared a state against itself and matched no winners.
|
|
22
|
+
The previous state is now snapshotted before any helper runs, and the payout
|
|
23
|
+
is driven by the `HAND_END` winner payload (with the stack diff as fallback),
|
|
24
|
+
so chips fly from the pot to every winner and the win sound plays.
|
|
25
|
+
|
|
26
|
+
## 0.4.6 — Maintenance
|
|
27
|
+
|
|
28
|
+
- **Seat cards clear their last action each hand.** `stats.lastAction` was only
|
|
29
|
+
ever written, never reset, so every player card kept showing the previous
|
|
30
|
+
hand's action into the next deal. `startHand` now clears the last action for
|
|
31
|
+
every seat. Eliminations still display, because they are derived from the
|
|
32
|
+
elimination list at render time rather than stored as `lastAction`.
|
|
33
|
+
|
|
34
|
+
## 0.4.5 — Maintenance
|
|
35
|
+
|
|
36
|
+
- **Stop now freezes the display.** Stopping a tournament clears the decision
|
|
37
|
+
clock and cancels every table effect still in flight (chip flies, action
|
|
38
|
+
toast, fold/check flashes and their pending timers). Previously the turn ring
|
|
39
|
+
and clock kept counting and queued animations finished after the run had
|
|
40
|
+
stopped, because the last broadcast still carried `currentDecision` and the
|
|
41
|
+
effects queue was never drained. `renderDecision` and `processVisualEffects`
|
|
42
|
+
now hard-stop on `STOPPED`/`ERROR`.
|
|
43
|
+
|
|
3
44
|
## 0.4.4 — Maintenance
|
|
4
45
|
|
|
5
46
|
- **Cleaner decision logs.** Removed the repeated spectator caption ("This
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pokertools-arena",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Browser-first AI poker benchmark powered by @pokertools/engine with generic OpenAI-compatible endpoints, optional OpenRouter/Jev Decisions routing, deterministic hierarchical decisions, paired fixed-state methodology, and reproducible reporting.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/app.js
CHANGED
|
@@ -142,6 +142,17 @@ function showActionToast(text, type = '') {
|
|
|
142
142
|
}, 1550);
|
|
143
143
|
}
|
|
144
144
|
function seatEl(playerId) { return $(`.seat[data-player-id="${CSS.escape(String(playerId))}"]`, els.seatsLayer); }
|
|
145
|
+
// Cancel every effect the table may still have in flight: chip flies, toast,
|
|
146
|
+
// fold/check flashes and their pending timeouts. Called when a run stops so the
|
|
147
|
+
// display freezes instead of finishing queued animations.
|
|
148
|
+
function stopTableEffects() {
|
|
149
|
+
clearTimeout(showActionToast.timer);
|
|
150
|
+
els.actionToast?.classList.add('hidden');
|
|
151
|
+
els.pokerTable?.classList.remove('action-message-visible');
|
|
152
|
+
els.fxLayer?.replaceChildren();
|
|
153
|
+
for (const el of $$('.seat.fold-flash, .seat.check-flash'))
|
|
154
|
+
{ el.classList.remove('fold-flash'); el.classList.remove('check-flash'); }
|
|
155
|
+
}
|
|
145
156
|
function animateNewHand(s) {
|
|
146
157
|
if (!animationsAllowed()) return;
|
|
147
158
|
requestAnimationFrame(() => {
|
|
@@ -169,12 +180,25 @@ function animateBoardCards(previousCount, currentCount) {
|
|
|
169
180
|
}
|
|
170
181
|
function processVisualEffects(s) {
|
|
171
182
|
const events = s?.events || [];
|
|
183
|
+
// Snapshot the previous state BEFORE any helper runs. animateNewHand and
|
|
184
|
+
// animateBoardCards advance lastVisualState internally, so reading it later
|
|
185
|
+
// made the HAND_END winner diff compare a state against itself and the
|
|
186
|
+
// pot→winner payout animation never fired.
|
|
187
|
+
const previous = lastVisualState;
|
|
188
|
+
// Once a run is stopped (or errored) no further effects may be scheduled.
|
|
189
|
+
// Without this, already-live chip flies, flashes and sounds continued after
|
|
190
|
+
// Stop, and an in-flight broadcast could replay the last decision's effects.
|
|
191
|
+
if (['STOPPED', 'ERROR'].includes(s?.status)) {
|
|
192
|
+
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
193
|
+
lastVisualState = s;
|
|
194
|
+
stopTableEffects();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
172
197
|
if (!animationsAllowed()) {
|
|
173
198
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
174
199
|
lastVisualState = s;
|
|
175
200
|
return;
|
|
176
201
|
}
|
|
177
|
-
const previous = lastVisualState;
|
|
178
202
|
if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
|
|
179
203
|
const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
|
|
180
204
|
if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
|
|
@@ -190,11 +214,19 @@ function processVisualEffects(s) {
|
|
|
190
214
|
if (type === ACTION.CHECK && seat) { seat.classList.add('check-flash'); setTimeout(() => seat.classList.remove('check-flash'), 500); playTableSound('check'); }
|
|
191
215
|
showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} · ${e.action?.description || type}`, type);
|
|
192
216
|
}
|
|
193
|
-
if (e.type === 'HAND_END'
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
217
|
+
if (e.type === 'HAND_END') {
|
|
218
|
+
// Prefer the event payload: `previous` can be missing on a resumed or
|
|
219
|
+
// re-rendered state, so the stack diff is only a fallback. Chips fly back
|
|
220
|
+
// from the pot to every winner at showdown, and the hand is announced.
|
|
221
|
+
const before = new Map((previous?.table?.players || []).filter(Boolean).map(p => [p.id, Number(p.stack || 0)]));
|
|
222
|
+
const payloadIds = (e.winners || []).map(w => w?.playerId ?? w?.id ?? w).filter(Boolean);
|
|
223
|
+
const fromStacks = (s?.table?.players || [])
|
|
224
|
+
.filter(p => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0)))
|
|
225
|
+
.map(p => p.id);
|
|
226
|
+
const winnerIds = [...new Set([...payloadIds, ...fromStacks])];
|
|
227
|
+
const pot = $('.hud-pot', document);
|
|
228
|
+
winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
|
|
229
|
+
if (winnerIds.length) playTableSound('winner');
|
|
198
230
|
}
|
|
199
231
|
if (e.type === 'TOURNAMENT_END') playTableSound('winner');
|
|
200
232
|
}
|
|
@@ -622,6 +654,10 @@ class TournamentDirector {
|
|
|
622
654
|
}
|
|
623
655
|
async startHand() {
|
|
624
656
|
await this.waitIfPaused(); this.pruneBustedSeats(); this.handNumber++;
|
|
657
|
+
// A new hand clears every seat's "last action" so a card never shows what a
|
|
658
|
+
// player did in the previous hand. Elimination stamps are presentation-only
|
|
659
|
+
// and are applied in renderTable, so they survive this reset.
|
|
660
|
+
for (const stat of Object.values(this.stats)) stat.lastAction = null;
|
|
625
661
|
this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map(p => [p.id, playerStack(p)]));
|
|
626
662
|
try { this.engine.deal(); }
|
|
627
663
|
catch {
|
|
@@ -1364,7 +1400,9 @@ function renderStatus(s) {
|
|
|
1364
1400
|
els.pauseBtn.classList.toggle('hidden', !running);
|
|
1365
1401
|
els.stopBtn.classList.toggle('hidden', !running);
|
|
1366
1402
|
els.startTopBtn.classList.toggle('hidden', running);
|
|
1367
|
-
|
|
1403
|
+
// Restart is offered only after a run has actually started and stopped or
|
|
1404
|
+
// finished; before the first Start the primary action is Start itself.
|
|
1405
|
+
els.seatsBtn.classList.toggle('hidden', running || !['STOPPED', 'FINISHED', 'ERROR'].includes(status));
|
|
1368
1406
|
els.setupBtn.disabled = running;
|
|
1369
1407
|
els.testsBtn.disabled = running;
|
|
1370
1408
|
const pauseLabel = $('.action-label', els.pauseBtn);
|
|
@@ -1569,6 +1607,9 @@ function championBadgeHtml() {
|
|
|
1569
1607
|
+ '</svg></span>';
|
|
1570
1608
|
}
|
|
1571
1609
|
function renderDecision(s) {
|
|
1610
|
+
// A stopped/errored run must not keep a live clock. stop() clears
|
|
1611
|
+
// currentDecision, but the last broadcast can still carry it, so guard here.
|
|
1612
|
+
if (['STOPPED', 'ERROR'].includes(s?.status)) { stopClock(); return; }
|
|
1572
1613
|
const d = s?.currentDecision;
|
|
1573
1614
|
const last = latestDecisionEvent(s);
|
|
1574
1615
|
const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
|
|
@@ -2326,12 +2367,17 @@ els.setupBtn.addEventListener('click', openSetup);
|
|
|
2326
2367
|
els.closeSetup.addEventListener('click', () => els.setupDialog.close());
|
|
2327
2368
|
els.addConnectionBtn.addEventListener('click', () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: 'openai', baseUrl: 'https://api.openai.com/v1' }));
|
|
2328
2369
|
els.pauseBtn.addEventListener('click', () => { if (!director) return; currentState?.status === 'PAUSED' ? director.resume() : director.pause(); });
|
|
2329
|
-
els.stopBtn.addEventListener('click', () =>
|
|
2370
|
+
els.stopBtn.addEventListener('click', () => {
|
|
2371
|
+
if (!director) return;
|
|
2372
|
+
stopClock(); stopTableEffects();
|
|
2373
|
+
director.stop();
|
|
2374
|
+
});
|
|
2330
2375
|
els.exportBtn.addEventListener('click', () => { if (!director?.events?.length) return; downloadText(`${director.config?.id || 'pokertools-arena'}.jsonl`, director.exportJsonl()); });
|
|
2331
2376
|
els.seatsBtn.addEventListener('click', () => {
|
|
2377
|
+
// This button only appears once a run has stopped or finished, where it
|
|
2378
|
+
// restarts the tournament from the current seats.
|
|
2332
2379
|
if (director && ['RUNNING', 'PAUSED'].includes(director.status)) return;
|
|
2333
|
-
|
|
2334
|
-
render(currentState || { status: 'IDLE', events: [] });
|
|
2380
|
+
void startConfiguredTournament();
|
|
2335
2381
|
});
|
|
2336
2382
|
els.seatsLayer.addEventListener('click', event => {
|
|
2337
2383
|
const seat = event.target.closest('[data-lobby-seat]');
|
package/src/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>
|
|
@@ -137,7 +138,12 @@
|
|
|
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
|
+
<div id="tab-log" class="tab-panel">
|
|
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"></div>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
141
147
|
<div id="tab-stats" class="tab-panel"><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>
|
package/src/styles.css
CHANGED
|
@@ -2287,3 +2287,64 @@ a.button{text-decoration:none}
|
|
|
2287
2287
|
|
|
2288
2288
|
/* 0.4.0 — methodology, reproducibility and repository-organization release.
|
|
2289
2289
|
Browser application source moved under src/; visual behavior is unchanged. */
|
|
2290
|
+
|
|
2291
|
+
/* 0.4.8 — compact header, table-card parity, quieter chrome.
|
|
2292
|
+
|
|
2293
|
+
Header: brand left, a backgroundless GitHub mark, then actions right. The
|
|
2294
|
+
board uses the same rank-plus-center-suit card language as the hole cards on
|
|
2295
|
+
small screens instead of the crowded duplicated corners. */
|
|
2296
|
+
.topbar{flex-wrap:nowrap}
|
|
2297
|
+
.brandbar{flex:0 1 auto}
|
|
2298
|
+
.repo-link{display:inline-grid;place-items:center;width:26px;height:26px;margin-left:6px;border:0;border-radius:7px;background:none;color:var(--muted);flex:0 0 auto;transition:color .15s ease,opacity .15s ease}
|
|
2299
|
+
.repo-link .repo-icon{width:17px;height:17px;opacity:.9}
|
|
2300
|
+
.repo-link:hover{color:var(--text)}
|
|
2301
|
+
.repo-link:hover .repo-icon{opacity:1}
|
|
2302
|
+
.topbar-actions{margin-left:auto;flex:0 1 auto}
|
|
2303
|
+
/* Settings is an icon-only control: no label, no wasted width. */
|
|
2304
|
+
.icon-only-action{padding:0;width:30px;min-width:30px;justify-content:center}
|
|
2305
|
+
.icon-only-action .action-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
|
|
2306
|
+
|
|
2307
|
+
/* Sound and recording live with the table status they affect. */
|
|
2308
|
+
.arena-head{gap:10px}
|
|
2309
|
+
.arena-head-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto}
|
|
2310
|
+
.arena-icon-button{display:inline-grid;place-items:center;width:24px;height:24px;min-width:24px;padding:0;border:1px solid var(--modal-border,rgba(232,238,243,.09));border-radius:7px;background:rgba(255,255,255,.02);color:#8d98a2;font-size:10px;cursor:pointer;transition:color .14s ease,border-color .14s ease,background .14s ease}
|
|
2311
|
+
.arena-icon-button:hover{color:#d8e0e5;border-color:rgba(146,201,164,.3)}
|
|
2312
|
+
.arena-icon-button.active{color:var(--accent);border-color:rgba(114,185,138,.35);background:rgba(114,185,138,.08)}
|
|
2313
|
+
.arena-icon-button.record-button.active{color:var(--danger);border-color:rgba(200,94,105,.4);background:rgba(200,94,105,.1)}
|
|
2314
|
+
.arena-icon-button:disabled{opacity:.42;cursor:not-allowed}
|
|
2315
|
+
.arena-head-actions .action-icon{font-size:10px;opacity:1}
|
|
2316
|
+
|
|
2317
|
+
/* Download lives inside the Log tab it produces. */
|
|
2318
|
+
.panel-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px}
|
|
2319
|
+
.panel-title-row .panel-title{margin-bottom:0}
|
|
2320
|
+
.panel-title-row .button{flex:0 0 auto}
|
|
2321
|
+
|
|
2322
|
+
/* The bank clock leads the right-hand clock block instead of trailing it. */
|
|
2323
|
+
.clock-wrap{align-items:flex-start;text-align:left}
|
|
2324
|
+
#bankClock{order:-1;color:var(--muted);font-size:9px;line-height:1.2}
|
|
2325
|
+
|
|
2326
|
+
/* Board cards: rank + big centre suit, no duplicated corners. This is the
|
|
2327
|
+
default at small widths and every compact/tight/micro density, so table and
|
|
2328
|
+
hole cards read the same way. */
|
|
2329
|
+
@media(max-width:720px){
|
|
2330
|
+
.board .card .card-corner.bottom,
|
|
2331
|
+
.poker-table[data-density="compact"] .board .card .card-corner.bottom,
|
|
2332
|
+
.poker-table[data-density="tight"] .board .card .card-corner.bottom{display:none!important}
|
|
2333
|
+
.board .card .card-corner i,
|
|
2334
|
+
.poker-table[data-density="compact"] .board .card .card-corner i,
|
|
2335
|
+
.poker-table[data-density="tight"] .board .card .card-corner i{display:none!important}
|
|
2336
|
+
.board .card .card-corner,
|
|
2337
|
+
.poker-table[data-density="compact"] .board .card .card-corner,
|
|
2338
|
+
.poker-table[data-density="tight"] .board .card .card-corner{left:3px!important;top:3px!important;line-height:1!important}
|
|
2339
|
+
.board .card .card-corner b,
|
|
2340
|
+
.poker-table[data-density="compact"] .board .card .card-corner b,
|
|
2341
|
+
.poker-table[data-density="tight"] .board .card .card-corner b{font-size:10px!important}
|
|
2342
|
+
.board .card .card-suit,
|
|
2343
|
+
.poker-table[data-density="compact"] .board .card .card-suit,
|
|
2344
|
+
.poker-table[data-density="tight"] .board .card .card-suit{font-size:17px!important}
|
|
2345
|
+
}
|
|
2346
|
+
.poker-table[data-density="tight"] .board .card .card-suit{font-size:16px!important}
|
|
2347
|
+
.poker-table[data-density="micro"] .board .card .card-corner i,
|
|
2348
|
+
.poker-table[data-density="micro"] .board .card .card-corner.bottom{display:none!important}
|
|
2349
|
+
.poker-table[data-density="micro"] .board .card .card-suit{font-size:13px!important}
|
|
2350
|
+
|