pokertools-arena 0.4.1 → 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 +1 -1
- package/dist/app.js +46 -7
- package/dist/index.html +5 -5
- package/dist/pokertools-arena.html +57 -18
- package/dist/styles.css +6 -6
- package/docs/diagnostics/SUMMARY.md +1 -1
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +38 -0
- package/package.json +1 -1
- package/src/app.js +56 -8
- package/src/index.html +5 -5
- package/src/lib/decision-core.js +0 -2
- package/src/styles.css +6 -6
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.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
|
@@ -115138,7 +115138,6 @@ function decide(agent, connection, args) {
|
|
|
115138
115138
|
}
|
|
115139
115139
|
var FAMILY_OBJECTIVE = "Choose exactly one legal action family that best maximizes tournament chip EV from the supplied state. Do not choose an amount; a size is chosen in a separate step.";
|
|
115140
115140
|
var SIZE_OBJECTIVE = (family) => `The action family ${String(family).toUpperCase()} has already been selected. Choose exactly one legal ${family === ACTION_FAMILY.RAISE ? "raise size" : "bet size"} that best maximizes tournament chip EV. Do not change the action family.`;
|
|
115141
|
-
var SPECTATOR_NOTE = "This model returns typed decisions rather than a text rationale.";
|
|
115142
115141
|
function buildJevFamilyQuestions(families, { instructions = FAMILY_OBJECTIVE, criteria = null } = {}) {
|
|
115143
115142
|
return { type: "choice", instructions, criteria: criteria ?? familyCriteria(families) };
|
|
115144
115143
|
}
|
|
@@ -115594,6 +115593,7 @@ var lastProcessedEventId = null;
|
|
|
115594
115593
|
var MAX_LOBBY_SEATS = 10;
|
|
115595
115594
|
var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
115596
115595
|
var editingSeatIndex = null;
|
|
115596
|
+
var seatNameAuto = false;
|
|
115597
115597
|
var lobbyVisible = true;
|
|
115598
115598
|
var pendingAutostart = false;
|
|
115599
115599
|
var arenaMaxDecisions = 0;
|
|
@@ -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;
|
|
@@ -116863,19 +116879,28 @@ function updateSeatSummary() {
|
|
|
116863
116879
|
const count = seatAssignments.filter(Boolean).length;
|
|
116864
116880
|
if (els.seatSummary) els.seatSummary.textContent = `${count} / ${MAX_LOBBY_SEATS} seated`;
|
|
116865
116881
|
}
|
|
116882
|
+
function syncSeatNameFromModel() {
|
|
116883
|
+
if (!seatNameAuto || editingSeatIndex == null) return;
|
|
116884
|
+
const model = els.seatModel.value.trim();
|
|
116885
|
+
els.seatName.value = model ? displayModelName(model) : `Player ${editingSeatIndex + 1}`;
|
|
116886
|
+
}
|
|
116866
116887
|
function openSeatEditor(seatIndex) {
|
|
116867
116888
|
if (!Number.isInteger(seatIndex) || seatIndex < 0 || seatIndex >= MAX_LOBBY_SEATS) return;
|
|
116868
116889
|
editingSeatIndex = seatIndex;
|
|
116869
116890
|
const locked = Boolean(director && ["RUNNING", "PAUSED"].includes(director.status));
|
|
116870
116891
|
const draft = seatAssignments[seatIndex] ? { ...seatAssignments[seatIndex] } : defaultSeatDraft(seatIndex);
|
|
116871
116892
|
els.seatDialogTitle.textContent = `Seat ${seatIndex + 1}`;
|
|
116872
|
-
|
|
116893
|
+
const placeholderName = `Player ${seatIndex + 1}`;
|
|
116894
|
+
const storedName = String(draft.name || "").trim();
|
|
116895
|
+
seatNameAuto = !storedName || /^player\s+\d+$/i.test(storedName) || Boolean(draft.model) && storedName === displayModelName(draft.model);
|
|
116896
|
+
els.seatName.value = seatNameAuto && draft.model ? displayModelName(draft.model) : storedName || placeholderName;
|
|
116873
116897
|
refreshSeatConnectionSelect(draft.connectionId || "");
|
|
116874
116898
|
if (draft.connectionId && [...els.seatConnection.options].some((option) => option.value === draft.connectionId)) els.seatConnection.value = draft.connectionId;
|
|
116875
116899
|
els.seatModel.value = draft.model || "";
|
|
116876
116900
|
els.seatProtocol.value = draft.protocol || "tool";
|
|
116877
116901
|
els.seatProvider.value = draft.provider || "";
|
|
116878
116902
|
applySeatProtocolRules();
|
|
116903
|
+
syncSeatNameFromModel();
|
|
116879
116904
|
void refreshSeatModelCatalog();
|
|
116880
116905
|
els.seatError.classList.add("hidden");
|
|
116881
116906
|
els.seatLockNotice.classList.toggle("hidden", !locked);
|
|
@@ -117317,8 +117342,6 @@ function decisionTelemetryHtml(event, { limit = 4 } = {}) {
|
|
|
117317
117342
|
const reasoningTokens = event?.usage?.completion_tokens_details?.reasoning_tokens;
|
|
117318
117343
|
if (reasoningTokens) facts.push(`<span>Reasoning <b>${reasoningTokens} tokens</b></span>`);
|
|
117319
117344
|
if (facts.length) parts.push(`<div class="decision-facts">${facts.join("")}</div>`);
|
|
117320
|
-
const isTypedDecision = meta.family != null || meta.aggression != null || meta.bluffSpot != null || meta.method === "openrouter-decisions" || meta.method === "jev-choice" || String(meta.method || "").includes("hierarchical");
|
|
117321
|
-
if (isTypedDecision) parts.push(`<div class="decision-facts-note">${escapeHtml(SPECTATOR_NOTE)} Bluff opportunity is a property of the spot, not the reason for the chosen action.</div>`);
|
|
117322
117345
|
return parts.join("");
|
|
117323
117346
|
}
|
|
117324
117347
|
function setDecisionContext({ hand = "\u2014", street = "\u2014", position = "\u2014", options = "\u2014", label = "Legal actions", hint = "Choose one", labels = null } = {}) {
|
|
@@ -117346,6 +117369,10 @@ function championBadgeHtml() {
|
|
|
117346
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>';
|
|
117347
117370
|
}
|
|
117348
117371
|
function renderDecision(s) {
|
|
117372
|
+
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
117373
|
+
stopClock();
|
|
117374
|
+
return;
|
|
117375
|
+
}
|
|
117349
117376
|
const d = s?.currentDecision;
|
|
117350
117377
|
const last = latestDecisionEvent(s);
|
|
117351
117378
|
const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
|
|
@@ -117576,7 +117603,7 @@ function cloneJson(value) {
|
|
|
117576
117603
|
}
|
|
117577
117604
|
function sanityAgents() {
|
|
117578
117605
|
const connections = readConnections();
|
|
117579
|
-
return readSeatPlayers().map((player) => ({ ...player, connection: connections.find((c) => c.id === player.connectionId) })).filter((row) => row.connection);
|
|
117606
|
+
return readSeatPlayers().map((player) => ({ ...player, id: player.id || `player-${player.lobbySeat + 1}`, connection: connections.find((c) => c.id === player.connectionId) })).filter((row) => row.connection);
|
|
117580
117607
|
}
|
|
117581
117608
|
function sanityProtocolLabel(agent) {
|
|
117582
117609
|
const protocol = effectiveProtocol(agent, agent.connection);
|
|
@@ -118262,7 +118289,12 @@ els.pauseBtn.addEventListener("click", () => {
|
|
|
118262
118289
|
if (!director) return;
|
|
118263
118290
|
currentState?.status === "PAUSED" ? director.resume() : director.pause();
|
|
118264
118291
|
});
|
|
118265
|
-
els.stopBtn.addEventListener("click", () =>
|
|
118292
|
+
els.stopBtn.addEventListener("click", () => {
|
|
118293
|
+
if (!director) return;
|
|
118294
|
+
stopClock();
|
|
118295
|
+
stopTableEffects();
|
|
118296
|
+
director.stop();
|
|
118297
|
+
});
|
|
118266
118298
|
els.exportBtn.addEventListener("click", () => {
|
|
118267
118299
|
if (!director?.events?.length) return;
|
|
118268
118300
|
downloadText(`${director.config?.id || "pokertools-arena"}.jsonl`, director.exportJsonl());
|
|
@@ -118288,9 +118320,16 @@ for (const dialog of [els.setupDialog, els.seatDialog, els.testsDialog, els.repl
|
|
|
118288
118320
|
}
|
|
118289
118321
|
els.seatConnection.addEventListener("change", () => {
|
|
118290
118322
|
applySeatProtocolRules();
|
|
118323
|
+
syncSeatNameFromModel();
|
|
118291
118324
|
void refreshSeatModelCatalog();
|
|
118292
118325
|
});
|
|
118293
|
-
els.seatModel.addEventListener("input",
|
|
118326
|
+
els.seatModel.addEventListener("input", () => {
|
|
118327
|
+
applySeatProtocolRules();
|
|
118328
|
+
syncSeatNameFromModel();
|
|
118329
|
+
});
|
|
118330
|
+
els.seatName.addEventListener("input", () => {
|
|
118331
|
+
seatNameAuto = false;
|
|
118332
|
+
});
|
|
118294
118333
|
els.seatProtocol.addEventListener("change", applySeatProtocolRules);
|
|
118295
118334
|
els.refreshModelsBtn?.addEventListener("click", () => void refreshSeatModelCatalog({ force: true }));
|
|
118296
118335
|
els.removeSeatBtn.addEventListener("click", () => {
|
package/dist/index.html
CHANGED
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
<h2>Settings</h2>
|
|
166
166
|
<button class="info-button" type="button" aria-label="About settings" data-tip="API keys stay in this browser tab and are never written to saved configuration or tournament logs.">i</button>
|
|
167
167
|
</div>
|
|
168
|
-
<button id="closeSetup" type="button" class="icon-button" aria-label="Close settings"
|
|
168
|
+
<button id="closeSetup" type="button" class="icon-button" aria-label="Close settings"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
169
169
|
</div>
|
|
170
170
|
|
|
171
171
|
<div class="dialog-body setup-body">
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
<h2 id="seatDialogTitle">Seat 1</h2>
|
|
247
247
|
<button class="info-button" type="button" aria-label="About seat context" data-tip="Each model receives only its own hole cards plus the same canonical public table state, recent public hands, opponent statistics, and legal actions.">i</button>
|
|
248
248
|
</div>
|
|
249
|
-
<button id="closeSeat" type="button" class="icon-button" aria-label="Close seat editor"
|
|
249
|
+
<button id="closeSeat" type="button" class="icon-button" aria-label="Close seat editor"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
250
250
|
</div>
|
|
251
251
|
<div class="dialog-body seat-editor-body">
|
|
252
252
|
<div id="seatLockNotice" class="seat-lock-notice hidden">Seat assignments are locked during a tournament.</div>
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
<div class="dialog-title-row"><h2>Decision sanity suite</h2><button class="info-button" type="button" aria-label="About decision tests" data-tip="A small comprehension and reliability check. It is intentionally not a GTO solver, Elo score, or tournament-strength ranking.">i</button></div>
|
|
285
285
|
<div class="dialog-subtitle">The same 10 fixed poker spots are sent independently to every seated model.</div>
|
|
286
286
|
</div>
|
|
287
|
-
<button id="closeTests" type="button" class="icon-button" aria-label="Close tests"
|
|
287
|
+
<button id="closeTests" type="button" class="icon-button" aria-label="Close tests"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
288
288
|
</div>
|
|
289
289
|
<div class="tests-body">
|
|
290
290
|
<div class="tests-how" aria-label="How the suite works">
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
<div class="dialog-title-row"><h2 id="replayTitle">Decision replay</h2><span id="replayBadge" class="replay-badge">HAND —</span></div>
|
|
318
318
|
<div id="replaySubtitle" class="dialog-subtitle">Snapshot captured immediately before the model acted.</div>
|
|
319
319
|
</div>
|
|
320
|
-
<button id="closeReplay" type="button" class="icon-button" aria-label="Close decision replay"
|
|
320
|
+
<button id="closeReplay" type="button" class="icon-button" aria-label="Close decision replay"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
321
321
|
</div>
|
|
322
322
|
<div class="dialog-body replay-body">
|
|
323
323
|
<section class="replay-table-wrap">
|
|
@@ -361,7 +361,7 @@
|
|
|
361
361
|
<span class="field-label-row">Preset <button class="info-button inline-info" type="button" aria-label="About connection presets" data-tip="OpenAI-compatible keeps Base URL fully editable. OpenRouter only fills its standard URL and enables OpenRouter-specific routing. TypeSafe uses the native System One endpoint.">i</button></span>
|
|
362
362
|
<select data-field="kind"><option value="openai">OpenAI-compatible</option><option value="openrouter">OpenRouter</option><option value="typesafe">TypeSafe System One</option></select>
|
|
363
363
|
</label>
|
|
364
|
-
<button type="button" class="remove-connection icon-button" title="Remove connection" aria-label="Remove connection"
|
|
364
|
+
<button type="button" class="remove-connection icon-button" title="Remove connection" aria-label="Remove connection"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
365
365
|
</div>
|
|
366
366
|
<div class="connection-main-grid">
|
|
367
367
|
<label class="connection-url">Base URL<input data-field="baseUrl" spellcheck="false" placeholder="https://api.example.com/v1" /></label>
|
|
@@ -895,7 +895,11 @@ input::placeholder{color:#4e5862}
|
|
|
895
895
|
.button,.icon-button,.field-icon-button,input,select{box-sizing:border-box}
|
|
896
896
|
.button{min-height:var(--control-h);padding:0 12px;border-radius:var(--radius-sm);font-size:10px;font-weight:800;line-height:1;white-space:nowrap}
|
|
897
897
|
.small-button{min-height:30px;height:30px;padding-inline:10px;font-size:9px}
|
|
898
|
-
.icon-button{width:30px;height:30px;min-width:30px;border-radius:var(--radius-sm);display:grid;place-items:center;padding:0}
|
|
898
|
+
.icon-button{width:30px;height:30px;min-width:30px;border-radius:var(--radius-sm);display:grid;place-items:center;padding:0;line-height:1}
|
|
899
|
+
/* SVG glyphs center geometrically; a text "×" sits on its baseline and looks
|
|
900
|
+
vertically off-centre inside these small buttons. */
|
|
901
|
+
.icon-button svg{width:14px;height:14px;display:block;pointer-events:none}
|
|
902
|
+
.dialog-head .icon-button svg{width:13px;height:13px}
|
|
899
903
|
input,select{width:100%;min-width:0;height:var(--control-h);padding:0 9px;border-radius:var(--radius-sm);font-size:11px;background:#0a0d10;border:1px solid var(--modal-border);color:var(--text)}
|
|
900
904
|
input:focus,select:focus{outline:none;border-color:rgba(146,201,164,.34);box-shadow:0 0 0 3px rgba(146,201,164,.05)}
|
|
901
905
|
label{min-width:0}
|
|
@@ -2321,10 +2325,7 @@ a.button{text-decoration:none}
|
|
|
2321
2325
|
.card .card-corner.bottom{display:none!important}
|
|
2322
2326
|
}
|
|
2323
2327
|
|
|
2324
|
-
/* 0.2.23 — decision diagnostics + deterministic hand evaluation
|
|
2325
|
-
Spectator-only annotations. The note explicitly separates typed model
|
|
2326
|
-
answers from any fabricated rationale. */
|
|
2327
|
-
.decision-facts-note{margin-top:6px;font-size:10px;line-height:1.4;color:var(--muted,#8aa098);border-left:2px solid rgba(184,236,111,.35);padding-left:8px}
|
|
2328
|
+
/* 0.2.23 — decision diagnostics + deterministic hand evaluation. */
|
|
2328
2329
|
.replay-reason-text{margin-bottom:10px;line-height:1.45}
|
|
2329
2330
|
.replay-summary span b{white-space:nowrap}
|
|
2330
2331
|
|
|
@@ -2339,7 +2340,6 @@ a.button{text-decoration:none}
|
|
|
2339
2340
|
.stage-chip{white-space:nowrap}
|
|
2340
2341
|
.telemetry-group{margin-top:6px}
|
|
2341
2342
|
.telemetry-caption{display:block;font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted,#8aa098);margin-bottom:3px}
|
|
2342
|
-
.decision-facts-note{white-space:normal}
|
|
2343
2343
|
|
|
2344
2344
|
/* 0.4.0 — methodology, reproducibility and repository-organization release.
|
|
2345
2345
|
Browser application source moved under src/; visual behavior is unchanged. */
|
|
@@ -2456,7 +2456,7 @@ a.button{text-decoration:none}
|
|
|
2456
2456
|
<h2>Settings</h2>
|
|
2457
2457
|
<button class="info-button" type="button" aria-label="About settings" data-tip="API keys stay in this browser tab and are never written to saved configuration or tournament logs.">i</button>
|
|
2458
2458
|
</div>
|
|
2459
|
-
<button id="closeSetup" type="button" class="icon-button" aria-label="Close settings"
|
|
2459
|
+
<button id="closeSetup" type="button" class="icon-button" aria-label="Close settings"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
2460
2460
|
</div>
|
|
2461
2461
|
|
|
2462
2462
|
<div class="dialog-body setup-body">
|
|
@@ -2537,7 +2537,7 @@ a.button{text-decoration:none}
|
|
|
2537
2537
|
<h2 id="seatDialogTitle">Seat 1</h2>
|
|
2538
2538
|
<button class="info-button" type="button" aria-label="About seat context" data-tip="Each model receives only its own hole cards plus the same canonical public table state, recent public hands, opponent statistics, and legal actions.">i</button>
|
|
2539
2539
|
</div>
|
|
2540
|
-
<button id="closeSeat" type="button" class="icon-button" aria-label="Close seat editor"
|
|
2540
|
+
<button id="closeSeat" type="button" class="icon-button" aria-label="Close seat editor"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
2541
2541
|
</div>
|
|
2542
2542
|
<div class="dialog-body seat-editor-body">
|
|
2543
2543
|
<div id="seatLockNotice" class="seat-lock-notice hidden">Seat assignments are locked during a tournament.</div>
|
|
@@ -2575,7 +2575,7 @@ a.button{text-decoration:none}
|
|
|
2575
2575
|
<div class="dialog-title-row"><h2>Decision sanity suite</h2><button class="info-button" type="button" aria-label="About decision tests" data-tip="A small comprehension and reliability check. It is intentionally not a GTO solver, Elo score, or tournament-strength ranking.">i</button></div>
|
|
2576
2576
|
<div class="dialog-subtitle">The same 10 fixed poker spots are sent independently to every seated model.</div>
|
|
2577
2577
|
</div>
|
|
2578
|
-
<button id="closeTests" type="button" class="icon-button" aria-label="Close tests"
|
|
2578
|
+
<button id="closeTests" type="button" class="icon-button" aria-label="Close tests"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
2579
2579
|
</div>
|
|
2580
2580
|
<div class="tests-body">
|
|
2581
2581
|
<div class="tests-how" aria-label="How the suite works">
|
|
@@ -2608,7 +2608,7 @@ a.button{text-decoration:none}
|
|
|
2608
2608
|
<div class="dialog-title-row"><h2 id="replayTitle">Decision replay</h2><span id="replayBadge" class="replay-badge">HAND —</span></div>
|
|
2609
2609
|
<div id="replaySubtitle" class="dialog-subtitle">Snapshot captured immediately before the model acted.</div>
|
|
2610
2610
|
</div>
|
|
2611
|
-
<button id="closeReplay" type="button" class="icon-button" aria-label="Close decision replay"
|
|
2611
|
+
<button id="closeReplay" type="button" class="icon-button" aria-label="Close decision replay"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
2612
2612
|
</div>
|
|
2613
2613
|
<div class="dialog-body replay-body">
|
|
2614
2614
|
<section class="replay-table-wrap">
|
|
@@ -2652,7 +2652,7 @@ a.button{text-decoration:none}
|
|
|
2652
2652
|
<span class="field-label-row">Preset <button class="info-button inline-info" type="button" aria-label="About connection presets" data-tip="OpenAI-compatible keeps Base URL fully editable. OpenRouter only fills its standard URL and enables OpenRouter-specific routing. TypeSafe uses the native System One endpoint.">i</button></span>
|
|
2653
2653
|
<select data-field="kind"><option value="openai">OpenAI-compatible</option><option value="openrouter">OpenRouter</option><option value="typesafe">TypeSafe System One</option></select>
|
|
2654
2654
|
</label>
|
|
2655
|
-
<button type="button" class="remove-connection icon-button" title="Remove connection" aria-label="Remove connection"
|
|
2655
|
+
<button type="button" class="remove-connection icon-button" title="Remove connection" aria-label="Remove connection"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
2656
2656
|
</div>
|
|
2657
2657
|
<div class="connection-main-grid">
|
|
2658
2658
|
<label class="connection-url">Base URL<input data-field="baseUrl" spellcheck="false" placeholder="https://api.example.com/v1" /></label>
|
|
@@ -117813,7 +117813,6 @@ function decide(agent, connection, args) {
|
|
|
117813
117813
|
}
|
|
117814
117814
|
var FAMILY_OBJECTIVE = "Choose exactly one legal action family that best maximizes tournament chip EV from the supplied state. Do not choose an amount; a size is chosen in a separate step.";
|
|
117815
117815
|
var SIZE_OBJECTIVE = (family) => `The action family ${String(family).toUpperCase()} has already been selected. Choose exactly one legal ${family === ACTION_FAMILY.RAISE ? "raise size" : "bet size"} that best maximizes tournament chip EV. Do not change the action family.`;
|
|
117816
|
-
var SPECTATOR_NOTE = "This model returns typed decisions rather than a text rationale.";
|
|
117817
117816
|
function buildJevFamilyQuestions(families, { instructions = FAMILY_OBJECTIVE, criteria = null } = {}) {
|
|
117818
117817
|
return { type: "choice", instructions, criteria: criteria ?? familyCriteria(families) };
|
|
117819
117818
|
}
|
|
@@ -118269,6 +118268,7 @@ var lastProcessedEventId = null;
|
|
|
118269
118268
|
var MAX_LOBBY_SEATS = 10;
|
|
118270
118269
|
var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
118271
118270
|
var editingSeatIndex = null;
|
|
118271
|
+
var seatNameAuto = false;
|
|
118272
118272
|
var lobbyVisible = true;
|
|
118273
118273
|
var pendingAutostart = false;
|
|
118274
118274
|
var arenaMaxDecisions = 0;
|
|
@@ -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;
|
|
@@ -119538,19 +119554,28 @@ function updateSeatSummary() {
|
|
|
119538
119554
|
const count = seatAssignments.filter(Boolean).length;
|
|
119539
119555
|
if (els.seatSummary) els.seatSummary.textContent = `${count} / ${MAX_LOBBY_SEATS} seated`;
|
|
119540
119556
|
}
|
|
119557
|
+
function syncSeatNameFromModel() {
|
|
119558
|
+
if (!seatNameAuto || editingSeatIndex == null) return;
|
|
119559
|
+
const model = els.seatModel.value.trim();
|
|
119560
|
+
els.seatName.value = model ? displayModelName(model) : `Player ${editingSeatIndex + 1}`;
|
|
119561
|
+
}
|
|
119541
119562
|
function openSeatEditor(seatIndex) {
|
|
119542
119563
|
if (!Number.isInteger(seatIndex) || seatIndex < 0 || seatIndex >= MAX_LOBBY_SEATS) return;
|
|
119543
119564
|
editingSeatIndex = seatIndex;
|
|
119544
119565
|
const locked = Boolean(director && ["RUNNING", "PAUSED"].includes(director.status));
|
|
119545
119566
|
const draft = seatAssignments[seatIndex] ? { ...seatAssignments[seatIndex] } : defaultSeatDraft(seatIndex);
|
|
119546
119567
|
els.seatDialogTitle.textContent = `Seat ${seatIndex + 1}`;
|
|
119547
|
-
|
|
119568
|
+
const placeholderName = `Player ${seatIndex + 1}`;
|
|
119569
|
+
const storedName = String(draft.name || "").trim();
|
|
119570
|
+
seatNameAuto = !storedName || /^player\s+\d+$/i.test(storedName) || Boolean(draft.model) && storedName === displayModelName(draft.model);
|
|
119571
|
+
els.seatName.value = seatNameAuto && draft.model ? displayModelName(draft.model) : storedName || placeholderName;
|
|
119548
119572
|
refreshSeatConnectionSelect(draft.connectionId || "");
|
|
119549
119573
|
if (draft.connectionId && [...els.seatConnection.options].some((option) => option.value === draft.connectionId)) els.seatConnection.value = draft.connectionId;
|
|
119550
119574
|
els.seatModel.value = draft.model || "";
|
|
119551
119575
|
els.seatProtocol.value = draft.protocol || "tool";
|
|
119552
119576
|
els.seatProvider.value = draft.provider || "";
|
|
119553
119577
|
applySeatProtocolRules();
|
|
119578
|
+
syncSeatNameFromModel();
|
|
119554
119579
|
void refreshSeatModelCatalog();
|
|
119555
119580
|
els.seatError.classList.add("hidden");
|
|
119556
119581
|
els.seatLockNotice.classList.toggle("hidden", !locked);
|
|
@@ -119992,8 +120017,6 @@ function decisionTelemetryHtml(event, { limit = 4 } = {}) {
|
|
|
119992
120017
|
const reasoningTokens = event?.usage?.completion_tokens_details?.reasoning_tokens;
|
|
119993
120018
|
if (reasoningTokens) facts.push(`<span>Reasoning <b>${reasoningTokens} tokens</b></span>`);
|
|
119994
120019
|
if (facts.length) parts.push(`<div class="decision-facts">${facts.join("")}</div>`);
|
|
119995
|
-
const isTypedDecision = meta.family != null || meta.aggression != null || meta.bluffSpot != null || meta.method === "openrouter-decisions" || meta.method === "jev-choice" || String(meta.method || "").includes("hierarchical");
|
|
119996
|
-
if (isTypedDecision) parts.push(`<div class="decision-facts-note">${escapeHtml(SPECTATOR_NOTE)} Bluff opportunity is a property of the spot, not the reason for the chosen action.</div>`);
|
|
119997
120020
|
return parts.join("");
|
|
119998
120021
|
}
|
|
119999
120022
|
function setDecisionContext({ hand = "\u2014", street = "\u2014", position = "\u2014", options = "\u2014", label = "Legal actions", hint = "Choose one", labels = null } = {}) {
|
|
@@ -120021,6 +120044,10 @@ function championBadgeHtml() {
|
|
|
120021
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>';
|
|
120022
120045
|
}
|
|
120023
120046
|
function renderDecision(s) {
|
|
120047
|
+
if (["STOPPED", "ERROR"].includes(s?.status)) {
|
|
120048
|
+
stopClock();
|
|
120049
|
+
return;
|
|
120050
|
+
}
|
|
120024
120051
|
const d = s?.currentDecision;
|
|
120025
120052
|
const last = latestDecisionEvent(s);
|
|
120026
120053
|
const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
|
|
@@ -120251,7 +120278,7 @@ function cloneJson(value) {
|
|
|
120251
120278
|
}
|
|
120252
120279
|
function sanityAgents() {
|
|
120253
120280
|
const connections = readConnections();
|
|
120254
|
-
return readSeatPlayers().map((player) => ({ ...player, connection: connections.find((c) => c.id === player.connectionId) })).filter((row) => row.connection);
|
|
120281
|
+
return readSeatPlayers().map((player) => ({ ...player, id: player.id || `player-${player.lobbySeat + 1}`, connection: connections.find((c) => c.id === player.connectionId) })).filter((row) => row.connection);
|
|
120255
120282
|
}
|
|
120256
120283
|
function sanityProtocolLabel(agent) {
|
|
120257
120284
|
const protocol = effectiveProtocol(agent, agent.connection);
|
|
@@ -120937,7 +120964,12 @@ els.pauseBtn.addEventListener("click", () => {
|
|
|
120937
120964
|
if (!director) return;
|
|
120938
120965
|
currentState?.status === "PAUSED" ? director.resume() : director.pause();
|
|
120939
120966
|
});
|
|
120940
|
-
els.stopBtn.addEventListener("click", () =>
|
|
120967
|
+
els.stopBtn.addEventListener("click", () => {
|
|
120968
|
+
if (!director) return;
|
|
120969
|
+
stopClock();
|
|
120970
|
+
stopTableEffects();
|
|
120971
|
+
director.stop();
|
|
120972
|
+
});
|
|
120941
120973
|
els.exportBtn.addEventListener("click", () => {
|
|
120942
120974
|
if (!director?.events?.length) return;
|
|
120943
120975
|
downloadText(`${director.config?.id || "pokertools-arena"}.jsonl`, director.exportJsonl());
|
|
@@ -120963,9 +120995,16 @@ for (const dialog of [els.setupDialog, els.seatDialog, els.testsDialog, els.repl
|
|
|
120963
120995
|
}
|
|
120964
120996
|
els.seatConnection.addEventListener("change", () => {
|
|
120965
120997
|
applySeatProtocolRules();
|
|
120998
|
+
syncSeatNameFromModel();
|
|
120966
120999
|
void refreshSeatModelCatalog();
|
|
120967
121000
|
});
|
|
120968
|
-
els.seatModel.addEventListener("input",
|
|
121001
|
+
els.seatModel.addEventListener("input", () => {
|
|
121002
|
+
applySeatProtocolRules();
|
|
121003
|
+
syncSeatNameFromModel();
|
|
121004
|
+
});
|
|
121005
|
+
els.seatName.addEventListener("input", () => {
|
|
121006
|
+
seatNameAuto = false;
|
|
121007
|
+
});
|
|
120969
121008
|
els.seatProtocol.addEventListener("change", applySeatProtocolRules);
|
|
120970
121009
|
els.refreshModelsBtn?.addEventListener("click", () => void refreshSeatModelCatalog({ force: true }));
|
|
120971
121010
|
els.removeSeatBtn.addEventListener("click", () => {
|
package/dist/styles.css
CHANGED
|
@@ -839,7 +839,11 @@ input::placeholder{color:#4e5862}
|
|
|
839
839
|
.button,.icon-button,.field-icon-button,input,select{box-sizing:border-box}
|
|
840
840
|
.button{min-height:var(--control-h);padding:0 12px;border-radius:var(--radius-sm);font-size:10px;font-weight:800;line-height:1;white-space:nowrap}
|
|
841
841
|
.small-button{min-height:30px;height:30px;padding-inline:10px;font-size:9px}
|
|
842
|
-
.icon-button{width:30px;height:30px;min-width:30px;border-radius:var(--radius-sm);display:grid;place-items:center;padding:0}
|
|
842
|
+
.icon-button{width:30px;height:30px;min-width:30px;border-radius:var(--radius-sm);display:grid;place-items:center;padding:0;line-height:1}
|
|
843
|
+
/* SVG glyphs center geometrically; a text "×" sits on its baseline and looks
|
|
844
|
+
vertically off-centre inside these small buttons. */
|
|
845
|
+
.icon-button svg{width:14px;height:14px;display:block;pointer-events:none}
|
|
846
|
+
.dialog-head .icon-button svg{width:13px;height:13px}
|
|
843
847
|
input,select{width:100%;min-width:0;height:var(--control-h);padding:0 9px;border-radius:var(--radius-sm);font-size:11px;background:#0a0d10;border:1px solid var(--modal-border);color:var(--text)}
|
|
844
848
|
input:focus,select:focus{outline:none;border-color:rgba(146,201,164,.34);box-shadow:0 0 0 3px rgba(146,201,164,.05)}
|
|
845
849
|
label{min-width:0}
|
|
@@ -2265,10 +2269,7 @@ a.button{text-decoration:none}
|
|
|
2265
2269
|
.card .card-corner.bottom{display:none!important}
|
|
2266
2270
|
}
|
|
2267
2271
|
|
|
2268
|
-
/* 0.2.23 — decision diagnostics + deterministic hand evaluation
|
|
2269
|
-
Spectator-only annotations. The note explicitly separates typed model
|
|
2270
|
-
answers from any fabricated rationale. */
|
|
2271
|
-
.decision-facts-note{margin-top:6px;font-size:10px;line-height:1.4;color:var(--muted,#8aa098);border-left:2px solid rgba(184,236,111,.35);padding-left:8px}
|
|
2272
|
+
/* 0.2.23 — decision diagnostics + deterministic hand evaluation. */
|
|
2272
2273
|
.replay-reason-text{margin-bottom:10px;line-height:1.45}
|
|
2273
2274
|
.replay-summary span b{white-space:nowrap}
|
|
2274
2275
|
|
|
@@ -2283,7 +2284,6 @@ a.button{text-decoration:none}
|
|
|
2283
2284
|
.stage-chip{white-space:nowrap}
|
|
2284
2285
|
.telemetry-group{margin-top:6px}
|
|
2285
2286
|
.telemetry-caption{display:block;font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted,#8aa098);margin-bottom:3px}
|
|
2286
|
-
.decision-facts-note{white-space:normal}
|
|
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. */
|
|
@@ -1,5 +1,43 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.4.4 — Maintenance
|
|
14
|
+
|
|
15
|
+
- **Cleaner decision logs.** Removed the repeated spectator caption ("This
|
|
16
|
+
model returns typed decisions rather than a text rationale…") that appeared on
|
|
17
|
+
every typed decision in the live feed and the replay panel. Typed decisions
|
|
18
|
+
already show their selected family/size plus confidence telemetry, so logs are
|
|
19
|
+
concise and no longer restate that a rationale is absent. `SPECTATOR_NOTE` and
|
|
20
|
+
its CSS were removed; release checks guard against reintroduction.
|
|
21
|
+
|
|
22
|
+
## 0.4.3 — Maintenance
|
|
23
|
+
|
|
24
|
+
- **Seat editor name.** Opening a seat restored from `.env` or saved
|
|
25
|
+
configuration now shows the model-derived name (for example `Gemma`) instead
|
|
26
|
+
of the generic `Player 1`, matching the table, lobby and stats surfaces. The
|
|
27
|
+
name follows the chosen model until it is edited by hand.
|
|
28
|
+
- **Close buttons.** Dialog close buttons and the remove-connection button use a
|
|
29
|
+
centered inline SVG cross instead of a baseline-aligned text `×`, so the glyph
|
|
30
|
+
is vertically centered.
|
|
31
|
+
|
|
32
|
+
## 0.4.2 — Maintenance
|
|
33
|
+
|
|
34
|
+
- **Decision sanity suite model attribution.** Seat assignments restored from
|
|
35
|
+
`.env` or saved configuration had no `id`, so every sanity agent shared an
|
|
36
|
+
undefined id. Each model summary then counted every model's decisions (for
|
|
37
|
+
example `24/10` when only 10 spots exist) and every scenario column rendered
|
|
38
|
+
the last model's result. Sanity agents now derive the same stable
|
|
39
|
+
`player-<seat>` id the tournament uses, so each model is reported separately.
|
|
40
|
+
|
|
3
41
|
## 0.4.1 — Maintenance
|
|
4
42
|
|
|
5
43
|
A maintenance release on top of the 0.4.0 methodology work.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pokertools-arena",
|
|
3
|
-
"version": "0.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
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
BENCHMARK_MODES, DECISION_ARCHITECTURES, DEFAULT_BENCHMARK_MODE, DEFAULT_DECISION_ARCHITECTURE,
|
|
15
15
|
REPRESENTATION_MODES, DEFAULT_REPRESENTATION_MODE, DECISION_ARCHITECTURE_VERSION,
|
|
16
16
|
legalActionFamilies, legalAggressiveSizes, buildHierarchicalDecision, familyCriteria, sizeCriteria,
|
|
17
|
-
applyBenchmarkMode, renderDecisionState, aggregateActionProbabilitiesByFamily, probabilityStats, SIZE_LABELS,
|
|
17
|
+
applyBenchmarkMode, renderDecisionState, aggregateActionProbabilitiesByFamily, probabilityStats, SIZE_LABELS,
|
|
18
18
|
isAggressiveType, familyForActionType, FAMILY_LABELS, decisionClockPhase,
|
|
19
19
|
} from './lib/decision-core.js';
|
|
20
20
|
|
|
@@ -47,6 +47,7 @@ let lastProcessedEventId = null;
|
|
|
47
47
|
const MAX_LOBBY_SEATS = 10;
|
|
48
48
|
let seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
49
49
|
let editingSeatIndex = null;
|
|
50
|
+
let seatNameAuto = false;
|
|
50
51
|
let lobbyVisible = true;
|
|
51
52
|
let pendingAutostart = false;
|
|
52
53
|
let arenaMaxDecisions = 0;
|
|
@@ -141,6 +142,17 @@ function showActionToast(text, type = '') {
|
|
|
141
142
|
}, 1550);
|
|
142
143
|
}
|
|
143
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
|
+
}
|
|
144
156
|
function animateNewHand(s) {
|
|
145
157
|
if (!animationsAllowed()) return;
|
|
146
158
|
requestAnimationFrame(() => {
|
|
@@ -168,6 +180,15 @@ function animateBoardCards(previousCount, currentCount) {
|
|
|
168
180
|
}
|
|
169
181
|
function processVisualEffects(s) {
|
|
170
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
|
+
}
|
|
171
192
|
if (!animationsAllowed()) {
|
|
172
193
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
173
194
|
lastVisualState = s;
|
|
@@ -1065,19 +1086,31 @@ function updateSeatSummary() {
|
|
|
1065
1086
|
const count = seatAssignments.filter(Boolean).length;
|
|
1066
1087
|
if (els.seatSummary) els.seatSummary.textContent = `${count} / ${MAX_LOBBY_SEATS} seated`;
|
|
1067
1088
|
}
|
|
1089
|
+
// The seat name follows the chosen model until the user edits it by hand, so
|
|
1090
|
+
// seats restored from .env or saved config show "Gemma" instead of the generic
|
|
1091
|
+
// "Player 1" the other surfaces already hide via visiblePlayerName().
|
|
1092
|
+
function syncSeatNameFromModel() {
|
|
1093
|
+
if (!seatNameAuto || editingSeatIndex == null) return;
|
|
1094
|
+
const model = els.seatModel.value.trim();
|
|
1095
|
+
els.seatName.value = model ? displayModelName(model) : `Player ${editingSeatIndex + 1}`;
|
|
1096
|
+
}
|
|
1068
1097
|
function openSeatEditor(seatIndex) {
|
|
1069
1098
|
if (!Number.isInteger(seatIndex) || seatIndex < 0 || seatIndex >= MAX_LOBBY_SEATS) return;
|
|
1070
1099
|
editingSeatIndex = seatIndex;
|
|
1071
1100
|
const locked = Boolean(director && ['RUNNING', 'PAUSED'].includes(director.status));
|
|
1072
1101
|
const draft = seatAssignments[seatIndex] ? { ...seatAssignments[seatIndex] } : defaultSeatDraft(seatIndex);
|
|
1073
1102
|
els.seatDialogTitle.textContent = `Seat ${seatIndex + 1}`;
|
|
1074
|
-
|
|
1103
|
+
const placeholderName = `Player ${seatIndex + 1}`;
|
|
1104
|
+
const storedName = String(draft.name || '').trim();
|
|
1105
|
+
seatNameAuto = !storedName || /^player\s+\d+$/i.test(storedName) || (Boolean(draft.model) && storedName === displayModelName(draft.model));
|
|
1106
|
+
els.seatName.value = seatNameAuto && draft.model ? displayModelName(draft.model) : (storedName || placeholderName);
|
|
1075
1107
|
refreshSeatConnectionSelect(draft.connectionId || '');
|
|
1076
1108
|
if (draft.connectionId && [...els.seatConnection.options].some(option => option.value === draft.connectionId)) els.seatConnection.value = draft.connectionId;
|
|
1077
1109
|
els.seatModel.value = draft.model || '';
|
|
1078
1110
|
els.seatProtocol.value = draft.protocol || 'tool';
|
|
1079
1111
|
els.seatProvider.value = draft.provider || '';
|
|
1080
1112
|
applySeatProtocolRules();
|
|
1113
|
+
syncSeatNameFromModel();
|
|
1081
1114
|
void refreshSeatModelCatalog();
|
|
1082
1115
|
els.seatError.classList.add('hidden');
|
|
1083
1116
|
els.seatLockNotice.classList.toggle('hidden', !locked);
|
|
@@ -1522,8 +1555,9 @@ function decisionTelemetryHtml(event, { limit = 4 } = {}) {
|
|
|
1522
1555
|
const reasoningTokens = event?.usage?.completion_tokens_details?.reasoning_tokens;
|
|
1523
1556
|
if (reasoningTokens) facts.push(`<span>Reasoning <b>${reasoningTokens} tokens</b></span>`);
|
|
1524
1557
|
if (facts.length) parts.push(`<div class="decision-facts">${facts.join('')}</div>`);
|
|
1525
|
-
|
|
1526
|
-
|
|
1558
|
+
// Typed decisions (hierarchical / Jev / OpenRouter-decisions) have no prose
|
|
1559
|
+
// rationale; the telemetry above already shows family, size and confidence.
|
|
1560
|
+
// A repeated "no text rationale" caption was removed as log noise.
|
|
1527
1561
|
return parts.join('');
|
|
1528
1562
|
}
|
|
1529
1563
|
function setDecisionContext({ hand = '—', street = '—', position = '—', options = '—', label = 'Legal actions', hint = 'Choose one', labels = null } = {}) {
|
|
@@ -1555,6 +1589,9 @@ function championBadgeHtml() {
|
|
|
1555
1589
|
+ '</svg></span>';
|
|
1556
1590
|
}
|
|
1557
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; }
|
|
1558
1595
|
const d = s?.currentDecision;
|
|
1559
1596
|
const last = latestDecisionEvent(s);
|
|
1560
1597
|
const shouldShowCard = Boolean(d || last || seatAssignments.filter(Boolean).length);
|
|
@@ -1746,7 +1783,13 @@ function openSetup({ preserveError = false } = {}) { if (!preserveError) els.set
|
|
|
1746
1783
|
function cloneJson(value) { return JSON.parse(JSON.stringify(value)); }
|
|
1747
1784
|
function sanityAgents() {
|
|
1748
1785
|
const connections = readConnections();
|
|
1749
|
-
|
|
1786
|
+
// Seat assignments restored from .env or saved config carry no id, so derive
|
|
1787
|
+
// the same stable id the tournament uses. Without it every agent.id is
|
|
1788
|
+
// undefined, so sanity results from all models collapse into one summary
|
|
1789
|
+
// (e.g. "24/10") and every grid column shows the last model's result.
|
|
1790
|
+
return readSeatPlayers()
|
|
1791
|
+
.map(player => ({ ...player, id: player.id || `player-${player.lobbySeat + 1}`, connection: connections.find(c => c.id === player.connectionId) }))
|
|
1792
|
+
.filter(row => row.connection);
|
|
1750
1793
|
}
|
|
1751
1794
|
function sanityProtocolLabel(agent) {
|
|
1752
1795
|
const protocol = effectiveProtocol(agent, agent.connection);
|
|
@@ -2306,7 +2349,11 @@ els.setupBtn.addEventListener('click', openSetup);
|
|
|
2306
2349
|
els.closeSetup.addEventListener('click', () => els.setupDialog.close());
|
|
2307
2350
|
els.addConnectionBtn.addEventListener('click', () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: 'openai', baseUrl: 'https://api.openai.com/v1' }));
|
|
2308
2351
|
els.pauseBtn.addEventListener('click', () => { if (!director) return; currentState?.status === 'PAUSED' ? director.resume() : director.pause(); });
|
|
2309
|
-
els.stopBtn.addEventListener('click', () =>
|
|
2352
|
+
els.stopBtn.addEventListener('click', () => {
|
|
2353
|
+
if (!director) return;
|
|
2354
|
+
stopClock(); stopTableEffects();
|
|
2355
|
+
director.stop();
|
|
2356
|
+
});
|
|
2310
2357
|
els.exportBtn.addEventListener('click', () => { if (!director?.events?.length) return; downloadText(`${director.config?.id || 'pokertools-arena'}.jsonl`, director.exportJsonl()); });
|
|
2311
2358
|
els.seatsBtn.addEventListener('click', () => {
|
|
2312
2359
|
if (director && ['RUNNING', 'PAUSED'].includes(director.status)) return;
|
|
@@ -2326,8 +2373,9 @@ for (const dialog of [els.setupDialog, els.seatDialog, els.testsDialog, els.repl
|
|
|
2326
2373
|
dialog.addEventListener('click', event => { if (event.target === dialog) dialog.close(); });
|
|
2327
2374
|
dialog.addEventListener('close', hideTooltip);
|
|
2328
2375
|
}
|
|
2329
|
-
els.seatConnection.addEventListener('change', () => { applySeatProtocolRules(); void refreshSeatModelCatalog(); });
|
|
2330
|
-
els.seatModel.addEventListener('input', applySeatProtocolRules);
|
|
2376
|
+
els.seatConnection.addEventListener('change', () => { applySeatProtocolRules(); syncSeatNameFromModel(); void refreshSeatModelCatalog(); });
|
|
2377
|
+
els.seatModel.addEventListener('input', () => { applySeatProtocolRules(); syncSeatNameFromModel(); });
|
|
2378
|
+
els.seatName.addEventListener('input', () => { seatNameAuto = false; });
|
|
2331
2379
|
els.seatProtocol.addEventListener('change', applySeatProtocolRules);
|
|
2332
2380
|
els.refreshModelsBtn?.addEventListener('click', () => void refreshSeatModelCatalog({ force: true }));
|
|
2333
2381
|
els.removeSeatBtn.addEventListener('click', () => {
|
package/src/index.html
CHANGED
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
<h2>Settings</h2>
|
|
166
166
|
<button class="info-button" type="button" aria-label="About settings" data-tip="API keys stay in this browser tab and are never written to saved configuration or tournament logs.">i</button>
|
|
167
167
|
</div>
|
|
168
|
-
<button id="closeSetup" type="button" class="icon-button" aria-label="Close settings"
|
|
168
|
+
<button id="closeSetup" type="button" class="icon-button" aria-label="Close settings"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
169
169
|
</div>
|
|
170
170
|
|
|
171
171
|
<div class="dialog-body setup-body">
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
<h2 id="seatDialogTitle">Seat 1</h2>
|
|
247
247
|
<button class="info-button" type="button" aria-label="About seat context" data-tip="Each model receives only its own hole cards plus the same canonical public table state, recent public hands, opponent statistics, and legal actions.">i</button>
|
|
248
248
|
</div>
|
|
249
|
-
<button id="closeSeat" type="button" class="icon-button" aria-label="Close seat editor"
|
|
249
|
+
<button id="closeSeat" type="button" class="icon-button" aria-label="Close seat editor"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
250
250
|
</div>
|
|
251
251
|
<div class="dialog-body seat-editor-body">
|
|
252
252
|
<div id="seatLockNotice" class="seat-lock-notice hidden">Seat assignments are locked during a tournament.</div>
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
<div class="dialog-title-row"><h2>Decision sanity suite</h2><button class="info-button" type="button" aria-label="About decision tests" data-tip="A small comprehension and reliability check. It is intentionally not a GTO solver, Elo score, or tournament-strength ranking.">i</button></div>
|
|
285
285
|
<div class="dialog-subtitle">The same 10 fixed poker spots are sent independently to every seated model.</div>
|
|
286
286
|
</div>
|
|
287
|
-
<button id="closeTests" type="button" class="icon-button" aria-label="Close tests"
|
|
287
|
+
<button id="closeTests" type="button" class="icon-button" aria-label="Close tests"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
288
288
|
</div>
|
|
289
289
|
<div class="tests-body">
|
|
290
290
|
<div class="tests-how" aria-label="How the suite works">
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
<div class="dialog-title-row"><h2 id="replayTitle">Decision replay</h2><span id="replayBadge" class="replay-badge">HAND —</span></div>
|
|
318
318
|
<div id="replaySubtitle" class="dialog-subtitle">Snapshot captured immediately before the model acted.</div>
|
|
319
319
|
</div>
|
|
320
|
-
<button id="closeReplay" type="button" class="icon-button" aria-label="Close decision replay"
|
|
320
|
+
<button id="closeReplay" type="button" class="icon-button" aria-label="Close decision replay"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
321
321
|
</div>
|
|
322
322
|
<div class="dialog-body replay-body">
|
|
323
323
|
<section class="replay-table-wrap">
|
|
@@ -361,7 +361,7 @@
|
|
|
361
361
|
<span class="field-label-row">Preset <button class="info-button inline-info" type="button" aria-label="About connection presets" data-tip="OpenAI-compatible keeps Base URL fully editable. OpenRouter only fills its standard URL and enables OpenRouter-specific routing. TypeSafe uses the native System One endpoint.">i</button></span>
|
|
362
362
|
<select data-field="kind"><option value="openai">OpenAI-compatible</option><option value="openrouter">OpenRouter</option><option value="typesafe">TypeSafe System One</option></select>
|
|
363
363
|
</label>
|
|
364
|
-
<button type="button" class="remove-connection icon-button" title="Remove connection" aria-label="Remove connection"
|
|
364
|
+
<button type="button" class="remove-connection icon-button" title="Remove connection" aria-label="Remove connection"><svg class="icon-glyph" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button>
|
|
365
365
|
</div>
|
|
366
366
|
<div class="connection-main-grid">
|
|
367
367
|
<label class="connection-url">Base URL<input data-field="baseUrl" spellcheck="false" placeholder="https://api.example.com/v1" /></label>
|
package/src/lib/decision-core.js
CHANGED
|
@@ -1000,8 +1000,6 @@ export function decide(agent, connection, args) {
|
|
|
1000
1000
|
// ===========================================================================
|
|
1001
1001
|
export const FAMILY_OBJECTIVE = 'Choose exactly one legal action family that best maximizes tournament chip EV from the supplied state. Do not choose an amount; a size is chosen in a separate step.';
|
|
1002
1002
|
export const SIZE_OBJECTIVE = family => `The action family ${String(family).toUpperCase()} has already been selected. Choose exactly one legal ${family === ACTION_FAMILY.RAISE ? 'raise size' : 'bet size'} that best maximizes tournament chip EV. Do not change the action family.`;
|
|
1003
|
-
export const SPECTATOR_NOTE = 'This model returns typed decisions rather than a text rationale.';
|
|
1004
|
-
|
|
1005
1003
|
export function buildJevFamilyQuestions(families, { instructions = FAMILY_OBJECTIVE, criteria = null } = {}) {
|
|
1006
1004
|
return { type: 'choice', instructions, criteria: criteria ?? familyCriteria(families) };
|
|
1007
1005
|
}
|
package/src/styles.css
CHANGED
|
@@ -839,7 +839,11 @@ input::placeholder{color:#4e5862}
|
|
|
839
839
|
.button,.icon-button,.field-icon-button,input,select{box-sizing:border-box}
|
|
840
840
|
.button{min-height:var(--control-h);padding:0 12px;border-radius:var(--radius-sm);font-size:10px;font-weight:800;line-height:1;white-space:nowrap}
|
|
841
841
|
.small-button{min-height:30px;height:30px;padding-inline:10px;font-size:9px}
|
|
842
|
-
.icon-button{width:30px;height:30px;min-width:30px;border-radius:var(--radius-sm);display:grid;place-items:center;padding:0}
|
|
842
|
+
.icon-button{width:30px;height:30px;min-width:30px;border-radius:var(--radius-sm);display:grid;place-items:center;padding:0;line-height:1}
|
|
843
|
+
/* SVG glyphs center geometrically; a text "×" sits on its baseline and looks
|
|
844
|
+
vertically off-centre inside these small buttons. */
|
|
845
|
+
.icon-button svg{width:14px;height:14px;display:block;pointer-events:none}
|
|
846
|
+
.dialog-head .icon-button svg{width:13px;height:13px}
|
|
843
847
|
input,select{width:100%;min-width:0;height:var(--control-h);padding:0 9px;border-radius:var(--radius-sm);font-size:11px;background:#0a0d10;border:1px solid var(--modal-border);color:var(--text)}
|
|
844
848
|
input:focus,select:focus{outline:none;border-color:rgba(146,201,164,.34);box-shadow:0 0 0 3px rgba(146,201,164,.05)}
|
|
845
849
|
label{min-width:0}
|
|
@@ -2265,10 +2269,7 @@ a.button{text-decoration:none}
|
|
|
2265
2269
|
.card .card-corner.bottom{display:none!important}
|
|
2266
2270
|
}
|
|
2267
2271
|
|
|
2268
|
-
/* 0.2.23 — decision diagnostics + deterministic hand evaluation
|
|
2269
|
-
Spectator-only annotations. The note explicitly separates typed model
|
|
2270
|
-
answers from any fabricated rationale. */
|
|
2271
|
-
.decision-facts-note{margin-top:6px;font-size:10px;line-height:1.4;color:var(--muted,#8aa098);border-left:2px solid rgba(184,236,111,.35);padding-left:8px}
|
|
2272
|
+
/* 0.2.23 — decision diagnostics + deterministic hand evaluation. */
|
|
2272
2273
|
.replay-reason-text{margin-bottom:10px;line-height:1.45}
|
|
2273
2274
|
.replay-summary span b{white-space:nowrap}
|
|
2274
2275
|
|
|
@@ -2283,7 +2284,6 @@ a.button{text-decoration:none}
|
|
|
2283
2284
|
.stage-chip{white-space:nowrap}
|
|
2284
2285
|
.telemetry-group{margin-top:6px}
|
|
2285
2286
|
.telemetry-caption{display:block;font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted,#8aa098);margin-bottom:3px}
|
|
2286
|
-
.decision-facts-note{white-space:normal}
|
|
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. */
|