pokertools-arena 0.4.4 → 0.4.5

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 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.4.** 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, and decision logs no longer repeat the typed-decision caption.
20
+ > **Current release: 0.4.5.** 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, and Stop immediately freezes the clock and table effects.
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,6 +115737,12 @@ function animateBoardCards(previousCount, currentCount) {
115727
115737
  }
115728
115738
  function processVisualEffects(s) {
115729
115739
  const events = s?.events || [];
115740
+ if (["STOPPED", "ERROR"].includes(s?.status)) {
115741
+ if (events.length) lastProcessedEventId = events.at(-1).id;
115742
+ lastVisualState = s;
115743
+ stopTableEffects();
115744
+ return;
115745
+ }
115730
115746
  if (!animationsAllowed()) {
115731
115747
  if (events.length) lastProcessedEventId = events.at(-1).id;
115732
115748
  lastVisualState = s;
@@ -117353,6 +117369,10 @@ function championBadgeHtml() {
117353
117369
  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
117370
  }
117355
117371
  function renderDecision(s) {
117372
+ if (["STOPPED", "ERROR"].includes(s?.status)) {
117373
+ stopClock();
117374
+ return;
117375
+ }
117356
117376
  const d = s?.currentDecision;
117357
117377
  const last = latestDecisionEvent(s);
117358
117378
  const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
@@ -118269,7 +118289,12 @@ els.pauseBtn.addEventListener("click", () => {
118269
118289
  if (!director) return;
118270
118290
  currentState?.status === "PAUSED" ? director.resume() : director.pause();
118271
118291
  });
118272
- els.stopBtn.addEventListener("click", () => director?.stop());
118292
+ els.stopBtn.addEventListener("click", () => {
118293
+ if (!director) return;
118294
+ stopClock();
118295
+ stopTableEffects();
118296
+ director.stop();
118297
+ });
118273
118298
  els.exportBtn.addEventListener("click", () => {
118274
118299
  if (!director?.events?.length) return;
118275
118300
  downloadText(`${director.config?.id || "pokertools-arena"}.jsonl`, director.exportJsonl());
@@ -118375,6 +118375,16 @@ function showActionToast(text, type = "") {
118375
118375
  function seatEl(playerId) {
118376
118376
  return $(`.seat[data-player-id="${CSS.escape(String(playerId))}"]`, els.seatsLayer);
118377
118377
  }
118378
+ function stopTableEffects() {
118379
+ clearTimeout(showActionToast.timer);
118380
+ els.actionToast?.classList.add("hidden");
118381
+ els.pokerTable?.classList.remove("action-message-visible");
118382
+ els.fxLayer?.replaceChildren();
118383
+ for (const el of $$(".seat.fold-flash, .seat.check-flash")) {
118384
+ el.classList.remove("fold-flash");
118385
+ el.classList.remove("check-flash");
118386
+ }
118387
+ }
118378
118388
  function animateNewHand(s) {
118379
118389
  if (!animationsAllowed()) return;
118380
118390
  requestAnimationFrame(() => {
@@ -118402,6 +118412,12 @@ function animateBoardCards(previousCount, currentCount) {
118402
118412
  }
118403
118413
  function processVisualEffects(s) {
118404
118414
  const events = s?.events || [];
118415
+ if (["STOPPED", "ERROR"].includes(s?.status)) {
118416
+ if (events.length) lastProcessedEventId = events.at(-1).id;
118417
+ lastVisualState = s;
118418
+ stopTableEffects();
118419
+ return;
118420
+ }
118405
118421
  if (!animationsAllowed()) {
118406
118422
  if (events.length) lastProcessedEventId = events.at(-1).id;
118407
118423
  lastVisualState = s;
@@ -120028,6 +120044,10 @@ function championBadgeHtml() {
120028
120044
  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
120045
  }
120030
120046
  function renderDecision(s) {
120047
+ if (["STOPPED", "ERROR"].includes(s?.status)) {
120048
+ stopClock();
120049
+ return;
120050
+ }
120031
120051
  const d = s?.currentDecision;
120032
120052
  const last = latestDecisionEvent(s);
120033
120053
  const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
@@ -120944,7 +120964,12 @@ els.pauseBtn.addEventListener("click", () => {
120944
120964
  if (!director) return;
120945
120965
  currentState?.status === "PAUSED" ? director.resume() : director.pause();
120946
120966
  });
120947
- els.stopBtn.addEventListener("click", () => director?.stop());
120967
+ els.stopBtn.addEventListener("click", () => {
120968
+ if (!director) return;
120969
+ stopClock();
120970
+ stopTableEffects();
120971
+ director.stop();
120972
+ });
120948
120973
  els.exportBtn.addEventListener("click", () => {
120949
120974
  if (!director?.events?.length) return;
120950
120975
  downloadText(`${director.config?.id || "pokertools-arena"}.jsonl`, director.exportJsonl());
@@ -1,4 +1,4 @@
1
- # pokertools-arena 0.4.4 — benchmark methodology & results
1
+ # pokertools-arena 0.4.5 — benchmark methodology & results
2
2
 
3
3
  Generated: 2026-09-19T18:19:58.352Z
4
4
  Methodology: paired fixed-state corpus; deterministic interleaving with recorded experiment seed.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "generatedAt": "2026-09-19T18:19:58.352Z",
5
5
  "experiments": [
6
6
  "paired"
@@ -1,5 +1,15 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.4.5 — Maintenance
4
+
5
+ - **Stop now freezes the display.** Stopping a tournament clears the decision
6
+ clock and cancels every table effect still in flight (chip flies, action
7
+ toast, fold/check flashes and their pending timers). Previously the turn ring
8
+ and clock kept counting and queued animations finished after the run had
9
+ stopped, because the last broadcast still carried `currentDecision` and the
10
+ effects queue was never drained. `renderDecision` and `processVisualEffects`
11
+ now hard-stop on `STOPPED`/`ERROR`.
12
+
3
13
  ## 0.4.4 — Maintenance
4
14
 
5
15
  - **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.4",
3
+ "version": "0.4.5",
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,6 +180,15 @@ function animateBoardCards(previousCount, currentCount) {
169
180
  }
170
181
  function processVisualEffects(s) {
171
182
  const events = s?.events || [];
183
+ // Once a run is stopped (or errored) no further effects may be scheduled.
184
+ // Without this, already-live chip flies, flashes and sounds continued after
185
+ // Stop, and an in-flight broadcast could replay the last decision's effects.
186
+ if (['STOPPED', 'ERROR'].includes(s?.status)) {
187
+ if (events.length) lastProcessedEventId = events.at(-1).id;
188
+ lastVisualState = s;
189
+ stopTableEffects();
190
+ return;
191
+ }
172
192
  if (!animationsAllowed()) {
173
193
  if (events.length) lastProcessedEventId = events.at(-1).id;
174
194
  lastVisualState = s;
@@ -1569,6 +1589,9 @@ function championBadgeHtml() {
1569
1589
  + '</svg></span>';
1570
1590
  }
1571
1591
  function renderDecision(s) {
1592
+ // A stopped/errored run must not keep a live clock. stop() clears
1593
+ // currentDecision, but the last broadcast can still carry it, so guard here.
1594
+ if (['STOPPED', 'ERROR'].includes(s?.status)) { stopClock(); return; }
1572
1595
  const d = s?.currentDecision;
1573
1596
  const last = latestDecisionEvent(s);
1574
1597
  const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
@@ -2326,7 +2349,11 @@ els.setupBtn.addEventListener('click', openSetup);
2326
2349
  els.closeSetup.addEventListener('click', () => els.setupDialog.close());
2327
2350
  els.addConnectionBtn.addEventListener('click', () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: 'openai', baseUrl: 'https://api.openai.com/v1' }));
2328
2351
  els.pauseBtn.addEventListener('click', () => { if (!director) return; currentState?.status === 'PAUSED' ? director.resume() : director.pause(); });
2329
- els.stopBtn.addEventListener('click', () => director?.stop());
2352
+ els.stopBtn.addEventListener('click', () => {
2353
+ if (!director) return;
2354
+ stopClock(); stopTableEffects();
2355
+ director.stop();
2356
+ });
2330
2357
  els.exportBtn.addEventListener('click', () => { if (!director?.events?.length) return; downloadText(`${director.config?.id || 'pokertools-arena'}.jsonl`, director.exportJsonl()); });
2331
2358
  els.seatsBtn.addEventListener('click', () => {
2332
2359
  if (director && ['RUNNING', 'PAUSED'].includes(director.status)) return;