pokertools-arena 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/README.md +6 -3
- package/bin/pokertools-arena.mjs +1 -1
- package/dist/app.js +366 -200
- package/dist/index.html +19 -19
- package/dist/pokertools-arena.html +843 -3199
- package/dist/styles.css +489 -3011
- package/docs/diagnostics/SUMMARY.md +1 -2
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +49 -1
- package/package.json +1 -1
- package/src/app.js +289 -228
- package/src/config/arena-config.js +86 -0
- package/src/index.html +19 -19
- package/src/styles.css +489 -3011
package/dist/app.js
CHANGED
|
@@ -114139,6 +114139,89 @@ var DECISION_SANITY_SCENARIOS = Object.freeze([
|
|
|
114139
114139
|
}
|
|
114140
114140
|
]);
|
|
114141
114141
|
|
|
114142
|
+
// src/config/arena-config.js
|
|
114143
|
+
var BASE_CONFIG = {
|
|
114144
|
+
table: {
|
|
114145
|
+
minPlayers: 2,
|
|
114146
|
+
maxPlayers: 10,
|
|
114147
|
+
startingStack: 3e3,
|
|
114148
|
+
smallBlind: 25,
|
|
114149
|
+
bigBlind: 50,
|
|
114150
|
+
ante: 5,
|
|
114151
|
+
handsPerLevel: 8,
|
|
114152
|
+
blindMultiplier: 1.5
|
|
114153
|
+
},
|
|
114154
|
+
timing: {
|
|
114155
|
+
actionSeconds: 30,
|
|
114156
|
+
timeBankSeconds: 30,
|
|
114157
|
+
lowTimeSeconds: 5,
|
|
114158
|
+
lowTimeFraction: 0.25,
|
|
114159
|
+
betweenActionsMs: 250,
|
|
114160
|
+
betweenHandsMs: 700
|
|
114161
|
+
},
|
|
114162
|
+
limits: {
|
|
114163
|
+
startingStack: { min: 100 },
|
|
114164
|
+
smallBlind: { min: 1 },
|
|
114165
|
+
bigBlind: { min: 2 },
|
|
114166
|
+
ante: { min: 0 },
|
|
114167
|
+
handsPerLevel: { min: 1 },
|
|
114168
|
+
blindMultiplier: { min: 1.1, max: 3, step: 0.1 },
|
|
114169
|
+
actionSeconds: { min: 1, max: 120 },
|
|
114170
|
+
timeBankSeconds: { min: 0, max: 600 },
|
|
114171
|
+
lowTimeSeconds: { min: 0, max: 600 },
|
|
114172
|
+
lowTimeFraction: { min: 0, max: 1, step: 0.05 },
|
|
114173
|
+
betweenActionsMs: { min: 0, max: 5e3 },
|
|
114174
|
+
betweenHandsMs: { min: 0, max: 1e4 }
|
|
114175
|
+
},
|
|
114176
|
+
connections: {
|
|
114177
|
+
defaultKind: "openai",
|
|
114178
|
+
presets: {
|
|
114179
|
+
openai: "https://api.openai.com/v1",
|
|
114180
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
114181
|
+
typesafe: "https://api.typesafe.ai/v1/systemone"
|
|
114182
|
+
}
|
|
114183
|
+
},
|
|
114184
|
+
history: {
|
|
114185
|
+
recentDecisions: 12,
|
|
114186
|
+
snapshotEvents: 300,
|
|
114187
|
+
persistedEvents: 100
|
|
114188
|
+
}
|
|
114189
|
+
};
|
|
114190
|
+
function mergeConfig(base, override) {
|
|
114191
|
+
if (!override || typeof override !== "object" || Array.isArray(override)) return { ...base };
|
|
114192
|
+
const merged = { ...base };
|
|
114193
|
+
for (const [key, value] of Object.entries(override)) {
|
|
114194
|
+
if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
|
|
114195
|
+
merged[key] = mergeConfig(base[key], value);
|
|
114196
|
+
} else if (value !== void 0) {
|
|
114197
|
+
merged[key] = value;
|
|
114198
|
+
}
|
|
114199
|
+
}
|
|
114200
|
+
return merged;
|
|
114201
|
+
}
|
|
114202
|
+
var ARENA_CONFIG = Object.freeze(mergeConfig(BASE_CONFIG, globalThis.__POKERTOOLS_ARENA_CONFIG__));
|
|
114203
|
+
var TABLE_DEFAULTS = Object.freeze(ARENA_CONFIG.table);
|
|
114204
|
+
var TIMING_DEFAULTS = Object.freeze(ARENA_CONFIG.timing);
|
|
114205
|
+
var CONFIG_LIMITS = Object.freeze(ARENA_CONFIG.limits);
|
|
114206
|
+
var CONNECTION_PRESETS = Object.freeze(ARENA_CONFIG.connections.presets);
|
|
114207
|
+
var HISTORY_CONFIG = Object.freeze(ARENA_CONFIG.history);
|
|
114208
|
+
var FORM_DEFAULTS = Object.freeze({ ...TABLE_DEFAULTS, ...TIMING_DEFAULTS });
|
|
114209
|
+
function applyConfiguredFormDefaults(form) {
|
|
114210
|
+
for (const [name, value] of Object.entries(FORM_DEFAULTS)) {
|
|
114211
|
+
const input = form?.elements?.namedItem(name);
|
|
114212
|
+
if (!input) continue;
|
|
114213
|
+
input.value = String(value);
|
|
114214
|
+
const limits = CONFIG_LIMITS[name] || {};
|
|
114215
|
+
for (const attribute of ["min", "max", "step"]) {
|
|
114216
|
+
if (limits[attribute] != null) input.setAttribute(attribute, String(limits[attribute]));
|
|
114217
|
+
}
|
|
114218
|
+
}
|
|
114219
|
+
}
|
|
114220
|
+
function defaultConnections() {
|
|
114221
|
+
const kind = ARENA_CONFIG.connections.defaultKind;
|
|
114222
|
+
return [{ name: "API", kind, baseUrl: CONNECTION_PRESETS[kind], apiKey: "", headers: "" }];
|
|
114223
|
+
}
|
|
114224
|
+
|
|
114142
114225
|
// src/lib/decision-core.js
|
|
114143
114226
|
var import_evaluator = __toESM(require_dist2(), 1);
|
|
114144
114227
|
var ACTION = Object.freeze({
|
|
@@ -115561,7 +115644,6 @@ var els = {
|
|
|
115561
115644
|
handValue: $("#handValue"),
|
|
115562
115645
|
levelValue: $("#levelValue"),
|
|
115563
115646
|
streetLabel: $("#streetLabel"),
|
|
115564
|
-
winnerBanner: $("#winnerBanner"),
|
|
115565
115647
|
decisionPanelTitle: $("#decisionPanelTitle"),
|
|
115566
115648
|
decisionEmpty: $("#decisionEmpty"),
|
|
115567
115649
|
decisionCard: $("#decisionCard"),
|
|
@@ -115582,8 +115664,10 @@ var els = {
|
|
|
115582
115664
|
decisionLabelPosition: $("#decisionLabelPosition"),
|
|
115583
115665
|
decisionLabelOptions: $("#decisionLabelOptions"),
|
|
115584
115666
|
decisionFeed: $("#decisionFeed"),
|
|
115667
|
+
decisionFeedSummary: $("#decisionFeedSummary"),
|
|
115585
115668
|
eventLog: $("#eventLog"),
|
|
115586
115669
|
logSummary: $("#logSummary"),
|
|
115670
|
+
logTabCount: $("#logTabCount"),
|
|
115587
115671
|
logSearch: $("#logSearch"),
|
|
115588
115672
|
logFilter: $("#logFilter"),
|
|
115589
115673
|
logClear: $("#logClear"),
|
|
@@ -115653,7 +115737,7 @@ var audioContext = null;
|
|
|
115653
115737
|
var soundMasterGain = null;
|
|
115654
115738
|
var lastVisualState = null;
|
|
115655
115739
|
var lastProcessedEventId = null;
|
|
115656
|
-
var MAX_LOBBY_SEATS =
|
|
115740
|
+
var MAX_LOBBY_SEATS = TABLE_DEFAULTS.maxPlayers;
|
|
115657
115741
|
var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
115658
115742
|
var editingSeatIndex = null;
|
|
115659
115743
|
var seatNameAuto = false;
|
|
@@ -115674,14 +115758,6 @@ var activeDecisionClockId = null;
|
|
|
115674
115758
|
var tableRecording = null;
|
|
115675
115759
|
var currentReplayEvent = null;
|
|
115676
115760
|
var renderMemo = { status: "", table: "", decision: "", feed: "", events: "", stats: "" };
|
|
115677
|
-
var TIMING_DEFAULTS = Object.freeze({
|
|
115678
|
-
actionSeconds: 20,
|
|
115679
|
-
timeBankSeconds: 30,
|
|
115680
|
-
lowTimeSeconds: 5,
|
|
115681
|
-
lowTimeFraction: 0.25,
|
|
115682
|
-
betweenActionsMs: 250,
|
|
115683
|
-
betweenHandsMs: 700
|
|
115684
|
-
});
|
|
115685
115761
|
function animationsAllowed() {
|
|
115686
115762
|
return document.visibilityState === "visible" && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
115687
115763
|
}
|
|
@@ -115702,13 +115778,6 @@ function soundMaster() {
|
|
|
115702
115778
|
}
|
|
115703
115779
|
return soundMasterGain;
|
|
115704
115780
|
}
|
|
115705
|
-
function recordingAudioTrack() {
|
|
115706
|
-
const ctx = getAudioContext();
|
|
115707
|
-
if (!ctx || typeof ctx.createMediaStreamDestination !== "function") return null;
|
|
115708
|
-
const destination = ctx.createMediaStreamDestination();
|
|
115709
|
-
soundMaster()?.connect(destination);
|
|
115710
|
-
return { destination, track: destination.stream.getAudioTracks()[0] || null };
|
|
115711
|
-
}
|
|
115712
115781
|
function tone(freq, duration = 0.05, gain = 0.035, offset = 0, type = "sine") {
|
|
115713
115782
|
if (!soundEnabled) return;
|
|
115714
115783
|
const ctx = getAudioContext();
|
|
@@ -115939,10 +116008,6 @@ function processVisualEffects(s) {
|
|
|
115939
116008
|
if (events.length) lastProcessedEventId = events.at(-1).id;
|
|
115940
116009
|
lastVisualState = s;
|
|
115941
116010
|
}
|
|
115942
|
-
var defaultConnections = [
|
|
115943
|
-
{ name: "API", kind: "openai", baseUrl: "https://api.openai.com/v1", apiKey: "", headers: "" }
|
|
115944
|
-
];
|
|
115945
|
-
var defaultPlayers = [];
|
|
115946
116011
|
function injectedEnvironmentConfig() {
|
|
115947
116012
|
const raw = globalThis.__POKERTOOLS_ENV__;
|
|
115948
116013
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -115950,7 +116015,7 @@ function injectedEnvironmentConfig() {
|
|
|
115950
116015
|
const players = Array.isArray(raw.players) ? raw.players.filter((p) => p && p.model) : [];
|
|
115951
116016
|
if (!connections.length && !players.length) return null;
|
|
115952
116017
|
const settings = raw.settings && typeof raw.settings === "object" ? {
|
|
115953
|
-
startingStack: Math.max(
|
|
116018
|
+
startingStack: Math.max(CONFIG_LIMITS.startingStack.min, Math.round(Number(raw.settings.startingStack) || TABLE_DEFAULTS.startingStack)),
|
|
115954
116019
|
autostart: Boolean(raw.settings.autostart),
|
|
115955
116020
|
maxDecisions: Math.max(0, Math.round(Number(raw.settings.maxDecisions) || 0))
|
|
115956
116021
|
} : null;
|
|
@@ -115960,7 +116025,7 @@ function injectedEnvironmentConfig() {
|
|
|
115960
116025
|
id: String(c.id || `env-connection-${index + 1}`),
|
|
115961
116026
|
name: String(c.name || "API"),
|
|
115962
116027
|
kind: ["openai", "openrouter", "typesafe"].includes(c.kind) ? c.kind : "openai",
|
|
115963
|
-
baseUrl: String(c.baseUrl ||
|
|
116028
|
+
baseUrl: String(c.baseUrl || CONNECTION_PRESETS.openai),
|
|
115964
116029
|
apiKey: String(c.apiKey || ""),
|
|
115965
116030
|
headers: String(c.headers || "")
|
|
115966
116031
|
})),
|
|
@@ -116205,7 +116270,7 @@ function buildBlindStructure(config) {
|
|
|
116205
116270
|
let sb = Math.max(1, Math.round(config.smallBlind));
|
|
116206
116271
|
let bb = Math.max(sb * 2, Math.round(config.bigBlind));
|
|
116207
116272
|
let ante = Math.max(0, Math.round(config.ante || 0));
|
|
116208
|
-
const multiplier = clamp(Number(config.blindMultiplier) ||
|
|
116273
|
+
const multiplier = clamp(Number(config.blindMultiplier) || TABLE_DEFAULTS.blindMultiplier, CONFIG_LIMITS.blindMultiplier.min, CONFIG_LIMITS.blindMultiplier.max);
|
|
116209
116274
|
const safe = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
116210
116275
|
for (let i = 0; i < 60; i++) {
|
|
116211
116276
|
if (!safe(sb) || !safe(bb) || !safe(ante)) break;
|
|
@@ -116222,7 +116287,7 @@ function buildBlindStructure(config) {
|
|
|
116222
116287
|
return levels;
|
|
116223
116288
|
}
|
|
116224
116289
|
function normalizeConfig(input = {}) {
|
|
116225
|
-
const players = Array.isArray(input.players) ? input.players.slice(0,
|
|
116290
|
+
const players = Array.isArray(input.players) ? input.players.slice(0, MAX_LOBBY_SEATS) : [];
|
|
116226
116291
|
const connections = Array.isArray(input.connections) ? input.connections : [];
|
|
116227
116292
|
if (players.length < 2) throw new Error("At least 2 players are required");
|
|
116228
116293
|
if (!connections.length) throw new Error("Add at least one API connection");
|
|
@@ -116236,24 +116301,24 @@ function normalizeConfig(input = {}) {
|
|
|
116236
116301
|
parseHeaders(c.headers);
|
|
116237
116302
|
return { ...c, baseUrl: normalizeBaseUrl(c.baseUrl) };
|
|
116238
116303
|
});
|
|
116239
|
-
const smallBlind = Math.max(
|
|
116240
|
-
const bigBlind = Math.max(
|
|
116304
|
+
const smallBlind = Math.max(CONFIG_LIMITS.smallBlind.min, Math.round(Number(input.smallBlind) || TABLE_DEFAULTS.smallBlind));
|
|
116305
|
+
const bigBlind = Math.max(CONFIG_LIMITS.bigBlind.min, Math.round(Number(input.bigBlind) || TABLE_DEFAULTS.bigBlind));
|
|
116241
116306
|
if (bigBlind < smallBlind * 2) throw new Error("Big blind must be at least twice the small blind");
|
|
116242
116307
|
return {
|
|
116243
116308
|
id: id("tournament"),
|
|
116244
116309
|
name: "pokertools-arena",
|
|
116245
|
-
startingStack: Math.max(
|
|
116310
|
+
startingStack: Math.max(CONFIG_LIMITS.startingStack.min, Math.round(Number(input.startingStack) || TABLE_DEFAULTS.startingStack)),
|
|
116246
116311
|
smallBlind,
|
|
116247
116312
|
bigBlind,
|
|
116248
|
-
ante: Math.max(
|
|
116249
|
-
handsPerLevel: Math.max(
|
|
116250
|
-
blindMultiplier: clamp(Number(input.blindMultiplier) ||
|
|
116251
|
-
actionSeconds: clamp(Number(input.actionSeconds) || TIMING_DEFAULTS.actionSeconds,
|
|
116252
|
-
timeBankSeconds: clamp(Number(input.timeBankSeconds) || TIMING_DEFAULTS.timeBankSeconds,
|
|
116253
|
-
lowTimeSeconds: clamp(Number.isFinite(Number(input.lowTimeSeconds)) ? Number(input.lowTimeSeconds) : TIMING_DEFAULTS.lowTimeSeconds,
|
|
116254
|
-
lowTimeFraction: clamp(Number.isFinite(Number(input.lowTimeFraction)) ? Number(input.lowTimeFraction) : TIMING_DEFAULTS.lowTimeFraction,
|
|
116255
|
-
betweenActionsMs: clamp(Number(input.betweenActionsMs) || TIMING_DEFAULTS.betweenActionsMs,
|
|
116256
|
-
betweenHandsMs: clamp(Number(input.betweenHandsMs) || TIMING_DEFAULTS.betweenHandsMs,
|
|
116313
|
+
ante: Math.max(CONFIG_LIMITS.ante.min, Math.round(Number(input.ante) || TABLE_DEFAULTS.ante)),
|
|
116314
|
+
handsPerLevel: Math.max(CONFIG_LIMITS.handsPerLevel.min, Math.round(Number(input.handsPerLevel) || TABLE_DEFAULTS.handsPerLevel)),
|
|
116315
|
+
blindMultiplier: clamp(Number(input.blindMultiplier) || TABLE_DEFAULTS.blindMultiplier, CONFIG_LIMITS.blindMultiplier.min, CONFIG_LIMITS.blindMultiplier.max),
|
|
116316
|
+
actionSeconds: clamp(Number(input.actionSeconds) || TIMING_DEFAULTS.actionSeconds, CONFIG_LIMITS.actionSeconds.min, CONFIG_LIMITS.actionSeconds.max),
|
|
116317
|
+
timeBankSeconds: clamp(Number(input.timeBankSeconds) || TIMING_DEFAULTS.timeBankSeconds, CONFIG_LIMITS.timeBankSeconds.min, CONFIG_LIMITS.timeBankSeconds.max),
|
|
116318
|
+
lowTimeSeconds: clamp(Number.isFinite(Number(input.lowTimeSeconds)) ? Number(input.lowTimeSeconds) : TIMING_DEFAULTS.lowTimeSeconds, CONFIG_LIMITS.lowTimeSeconds.min, CONFIG_LIMITS.lowTimeSeconds.max),
|
|
116319
|
+
lowTimeFraction: clamp(Number.isFinite(Number(input.lowTimeFraction)) ? Number(input.lowTimeFraction) : TIMING_DEFAULTS.lowTimeFraction, CONFIG_LIMITS.lowTimeFraction.min, CONFIG_LIMITS.lowTimeFraction.max),
|
|
116320
|
+
betweenActionsMs: clamp(Number(input.betweenActionsMs) || TIMING_DEFAULTS.betweenActionsMs, CONFIG_LIMITS.betweenActionsMs.min, CONFIG_LIMITS.betweenActionsMs.max),
|
|
116321
|
+
betweenHandsMs: clamp(Number(input.betweenHandsMs) || TIMING_DEFAULTS.betweenHandsMs, CONFIG_LIMITS.betweenHandsMs.min, CONFIG_LIMITS.betweenHandsMs.max),
|
|
116257
116322
|
connections: cleanConnections,
|
|
116258
116323
|
benchmarkMode: input.benchmarkMode === BENCHMARK_MODES.RAW ? BENCHMARK_MODES.RAW : DEFAULT_BENCHMARK_MODE,
|
|
116259
116324
|
decisionArchitecture: input.decisionArchitecture === DECISION_ARCHITECTURES.FLAT ? DECISION_ARCHITECTURES.FLAT : DEFAULT_DECISION_ARCHITECTURE,
|
|
@@ -116427,7 +116492,7 @@ var TournamentDirector = class {
|
|
|
116427
116492
|
stats: this.stats,
|
|
116428
116493
|
publicPlayerStats,
|
|
116429
116494
|
timeBanks: this.timeBanks,
|
|
116430
|
-
events: this.events.slice(-
|
|
116495
|
+
events: this.events.slice(-HISTORY_CONFIG.snapshotEvents)
|
|
116431
116496
|
};
|
|
116432
116497
|
}
|
|
116433
116498
|
broadcast() {
|
|
@@ -116449,7 +116514,7 @@ var TournamentDirector = class {
|
|
|
116449
116514
|
persistLightweight() {
|
|
116450
116515
|
try {
|
|
116451
116516
|
const state = this.snapshot();
|
|
116452
|
-
const lightweight = { ...state, events: state.events.slice(-
|
|
116517
|
+
const lightweight = { ...state, events: state.events.slice(-HISTORY_CONFIG.persistedEvents), currentDecision: null };
|
|
116453
116518
|
storageSet("pokertoolsArenaLastState", JSON.stringify(lightweight));
|
|
116454
116519
|
} catch {
|
|
116455
116520
|
}
|
|
@@ -116838,16 +116903,12 @@ function refreshSeatConnectionSelect(selected = null) {
|
|
|
116838
116903
|
applySeatProtocolRules();
|
|
116839
116904
|
}
|
|
116840
116905
|
function connectionPresetUrl(kind) {
|
|
116841
|
-
|
|
116842
|
-
if (kind === "typesafe") return "https://api.typesafe.ai/v1/systemone";
|
|
116843
|
-
return "https://api.openai.com/v1";
|
|
116906
|
+
return CONNECTION_PRESETS[kind] || CONNECTION_PRESETS.openai;
|
|
116844
116907
|
}
|
|
116845
116908
|
function shouldReplacePresetUrl(value) {
|
|
116846
116909
|
const normalized = normalizeBaseUrl(value);
|
|
116847
116910
|
return !normalized || [
|
|
116848
|
-
|
|
116849
|
-
"https://openrouter.ai/api/v1",
|
|
116850
|
-
"https://api.typesafe.ai/v1/systemone"
|
|
116911
|
+
...Object.values(CONNECTION_PRESETS)
|
|
116851
116912
|
].includes(normalized);
|
|
116852
116913
|
}
|
|
116853
116914
|
function addConnectionRow(connection = {}) {
|
|
@@ -116871,9 +116932,9 @@ function addConnectionRow(connection = {}) {
|
|
|
116871
116932
|
$("[data-field=kind]", row).addEventListener("change", () => {
|
|
116872
116933
|
const kind = $("[data-field=kind]", row).value;
|
|
116873
116934
|
const url = $("[data-field=baseUrl]", row);
|
|
116874
|
-
if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value =
|
|
116875
|
-
if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value =
|
|
116876
|
-
if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value =
|
|
116935
|
+
if (kind === "typesafe" && (!url.value || url.value.includes("openrouter.ai"))) url.value = CONNECTION_PRESETS.typesafe;
|
|
116936
|
+
if (kind === "openrouter" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openrouter;
|
|
116937
|
+
if (kind === "openai" && (!url.value || url.value.includes("typesafe.ai"))) url.value = CONNECTION_PRESETS.openai;
|
|
116877
116938
|
refreshSeatConnectionSelect();
|
|
116878
116939
|
});
|
|
116879
116940
|
$(".remove-connection", row).addEventListener("click", () => {
|
|
@@ -117096,7 +117157,7 @@ function saveSeatAssignments() {
|
|
|
117096
117157
|
}
|
|
117097
117158
|
function restoreSeatAssignments(rows = []) {
|
|
117098
117159
|
seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
|
|
117099
|
-
const sources = rows
|
|
117160
|
+
const sources = rows;
|
|
117100
117161
|
sources.slice(0, MAX_LOBBY_SEATS).forEach((player, index) => {
|
|
117101
117162
|
const lobbySeat = clamp(Math.round(Number(player.lobbySeat ?? index)), 0, MAX_LOBBY_SEATS - 1);
|
|
117102
117163
|
let connectionId = player.connectionId || "";
|
|
@@ -117166,13 +117227,14 @@ async function primeModelCapabilities() {
|
|
|
117166
117227
|
}));
|
|
117167
117228
|
}
|
|
117168
117229
|
function restoreSetup() {
|
|
117230
|
+
applyConfiguredFormDefaults(els.setupForm);
|
|
117169
117231
|
let saved = null;
|
|
117170
117232
|
try {
|
|
117171
117233
|
saved = JSON.parse(storageGet("pokertoolsArenaBrowserConfig"));
|
|
117172
117234
|
} catch {
|
|
117173
117235
|
}
|
|
117174
117236
|
const injected = injectedEnvironmentConfig();
|
|
117175
|
-
const conns = injected?.connections?.length ? injected.connections : saved?.connections?.length ? saved.connections : defaultConnections;
|
|
117237
|
+
const conns = injected?.connections?.length ? injected.connections : saved?.connections?.length ? saved.connections : defaultConnections();
|
|
117176
117238
|
conns.forEach(addConnectionRow);
|
|
117177
117239
|
restoreSeatAssignments(injected?.players?.length ? injected.players : saved?.players?.length ? saved.players : []);
|
|
117178
117240
|
capabilityPrime = primeModelCapabilities();
|
|
@@ -117198,7 +117260,7 @@ function restoreSetup() {
|
|
|
117198
117260
|
function renderLobbyIfVisible() {
|
|
117199
117261
|
if (lobbyVisible && (!director || !["RUNNING", "PAUSED"].includes(director.status))) renderTable(currentState || { status: "IDLE", events: [] });
|
|
117200
117262
|
}
|
|
117201
|
-
var TABLE_MIN_PLAYERS =
|
|
117263
|
+
var TABLE_MIN_PLAYERS = TABLE_DEFAULTS.minPlayers;
|
|
117202
117264
|
function visualSeatAngle(index, count) {
|
|
117203
117265
|
const safeCount = Math.max(TABLE_MIN_PLAYERS, Math.min(MAX_LOBBY_SEATS, Math.round(Number(count) || TABLE_MIN_PLAYERS)));
|
|
117204
117266
|
const safeIndex = (Math.round(Number(index) || 0) % safeCount + safeCount) % safeCount;
|
|
@@ -117341,7 +117403,6 @@ function renderLobbyTable() {
|
|
|
117341
117403
|
els.handValue.textContent = "\u2014";
|
|
117342
117404
|
els.levelValue.textContent = "\u2014";
|
|
117343
117405
|
els.streetLabel.textContent = "LOBBY";
|
|
117344
|
-
els.winnerBanner.classList.add("hidden");
|
|
117345
117406
|
}
|
|
117346
117407
|
function renderStatus(s) {
|
|
117347
117408
|
const status = s?.status || "IDLE";
|
|
@@ -117412,7 +117473,6 @@ function renderTable(s) {
|
|
|
117412
117473
|
els.levelValue.title = `Level ${Number(table.blindLevel ?? 0) + 1}`;
|
|
117413
117474
|
els.streetLabel.textContent = s.status === "FINISHED" && s.winner ? `WINNER \xB7 ${displayModelName(s.winner.model || "")}` : table.street || "\u2014";
|
|
117414
117475
|
els.streetLabel.classList.toggle("winner-street", s.status === "FINISHED" && Boolean(s.winner));
|
|
117415
|
-
els.winnerBanner.classList.add("hidden");
|
|
117416
117476
|
}
|
|
117417
117477
|
function clearTurnRings(keep = null) {
|
|
117418
117478
|
for (const el of $$(".seat.turn-active")) if (el !== keep) el.classList.remove("turn-active");
|
|
@@ -117651,7 +117711,7 @@ function renderDecision(s) {
|
|
|
117651
117711
|
}
|
|
117652
117712
|
function renderFeed(s) {
|
|
117653
117713
|
const feedArchive = eventArchive(s);
|
|
117654
|
-
const events = feedArchive.filter((e) => e.type === "DECISION").slice(-
|
|
117714
|
+
const events = feedArchive.filter((e) => e.type === "DECISION").slice(-HISTORY_CONFIG.recentDecisions).reverse();
|
|
117655
117715
|
const explanations = new Map(feedArchive.filter((e) => e.type === "SPECTATOR_EXPLANATION").map((e) => [e.decisionId, e.text]));
|
|
117656
117716
|
els.decisionFeed.innerHTML = events.length ? events.map((e) => {
|
|
117657
117717
|
const infra = [];
|
|
@@ -117663,6 +117723,7 @@ function renderFeed(s) {
|
|
|
117663
117723
|
const reason = explanations.get(e.decisionId) || e.publicReason || (e.decisionMeta?.family ? "Typed decision (no text rationale)" : "No public rationale");
|
|
117664
117724
|
return `<button type="button" class="decision-item decision-history-item ${e.error ? "error" : ""}" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))} decision"><div class="decision-item-head"><strong title="${escapeHtml(e.configuredModel || e.resolvedModel || "")}">${escapeHtml(displayModelName(e.configuredModel || e.resolvedModel || ""))}</strong><span class="decision-item-action">${escapeHtml(e.action?.description || "\u2014")}</span></div><div class="decision-reason">${escapeHtml(reason)}</div>${telemetry ? `<div class="decision-item-telemetry">${telemetry}</div>` : ""}<div class="decision-meta mono">${escapeHtml(e.connection || "")} \xB7 ${escapeHtml(shortModel(e.resolvedModel || e.configuredModel || ""))} \xB7 ${e.primaryDecisionLatencyMs || e.latencyMs || 0}ms${infra.length ? ` \xB7 ${escapeHtml(infra.join(" \xB7 "))}` : ""}${e.error ? ` \xB7 ${escapeHtml(e.error)}` : ""}<span class="decision-replay-hint">View hand \u2197</span></div></button>`;
|
|
117665
117725
|
}).join("") : '<div class="empty-state">No decisions yet.</div>';
|
|
117726
|
+
if (els.decisionFeedSummary) els.decisionFeedSummary.textContent = events.length ? `${events.length} most recent` : "Newest first";
|
|
117666
117727
|
}
|
|
117667
117728
|
function eventArchive(s = currentState) {
|
|
117668
117729
|
if (director?.events?.length) return director.events;
|
|
@@ -117714,6 +117775,7 @@ function eventMatchesView(event) {
|
|
|
117714
117775
|
function renderEvents(s) {
|
|
117715
117776
|
const archive = eventArchive(s);
|
|
117716
117777
|
const events = archive.filter(eventMatchesView).reverse();
|
|
117778
|
+
if (els.logTabCount) els.logTabCount.textContent = archive.length > 999 ? "999+" : String(archive.length);
|
|
117717
117779
|
if (els.logSummary) {
|
|
117718
117780
|
const suffix = events.length === archive.length ? "full run" : `${events.length} shown`;
|
|
117719
117781
|
els.logSummary.textContent = `${archive.length.toLocaleString()} event${archive.length === 1 ? "" : "s"} \xB7 ${suffix}`;
|
|
@@ -117728,7 +117790,7 @@ function renderEvents(s) {
|
|
|
117728
117790
|
const attrs = replayable ? ` type="button" data-decision-id="${escapeHtml(e.id)}" aria-label="Replay ${escapeHtml(detail || "decision")}"` : "";
|
|
117729
117791
|
return `<${tag}${attrs} class="event-row event-${category}${e.error || e.type?.includes?.("ERROR") ? " has-error" : ""}">
|
|
117730
117792
|
<span class="event-rail" aria-hidden="true"></span>
|
|
117731
|
-
<span class="event-main"><span class="event-row-head"><span class="event-type">${escapeHtml(e.type)}</span><time class="event-time mono" datetime="${new Date(e.at).toISOString()}">${escapeHtml(time)}</time></span>${detail ? `<span class="event-detail">${escapeHtml(detail)}</span>` : ""}${meta ? `<span class="event-meta mono">${escapeHtml(meta)}</span>` : ""}</span>
|
|
117793
|
+
<span class="event-main"><span class="event-row-head"><span class="event-type">${escapeHtml(e.type.replaceAll("_", " "))}</span><time class="event-time mono" datetime="${new Date(e.at).toISOString()}">${escapeHtml(time)}</time></span>${detail ? `<span class="event-detail">${escapeHtml(detail)}</span>` : ""}${meta ? `<span class="event-meta mono">${escapeHtml(meta)}</span>` : ""}</span>
|
|
117732
117794
|
${replayable ? '<span class="event-open" aria-hidden="true">\u2197</span>' : ""}
|
|
117733
117795
|
</${tag}>`;
|
|
117734
117796
|
}).join("") || '<div class="empty-state log-empty">No events match these filters.</div>';
|
|
@@ -117791,6 +117853,10 @@ function statsRenderSignature(s) {
|
|
|
117791
117853
|
}
|
|
117792
117854
|
function render(s, { force = false } = {}) {
|
|
117793
117855
|
currentState = s;
|
|
117856
|
+
if (els.logTabCount) {
|
|
117857
|
+
const eventCount = eventArchive(s).length;
|
|
117858
|
+
els.logTabCount.textContent = eventCount > 999 ? "999+" : String(eventCount);
|
|
117859
|
+
}
|
|
117794
117860
|
const statusSig = statusRenderSignature(s);
|
|
117795
117861
|
if (force || renderMemo.status !== statusSig) {
|
|
117796
117862
|
renderMemo.status = statusSig;
|
|
@@ -118000,26 +118066,26 @@ function openTests() {
|
|
|
118000
118066
|
}
|
|
118001
118067
|
var TABLE_RECORDING_CONFIG = Object.freeze({
|
|
118002
118068
|
frameRate: 30,
|
|
118003
|
-
maxWidth:
|
|
118004
|
-
maxHeight:
|
|
118005
|
-
minBitrate:
|
|
118006
|
-
maxBitrate:
|
|
118007
|
-
bitsPerPixelPerFrame: 0.
|
|
118008
|
-
audioBitsPerSecond:
|
|
118069
|
+
maxWidth: 2560,
|
|
118070
|
+
maxHeight: 1440,
|
|
118071
|
+
minBitrate: 1e7,
|
|
118072
|
+
maxBitrate: 28e6,
|
|
118073
|
+
bitsPerPixelPerFrame: 0.2,
|
|
118074
|
+
audioBitsPerSecond: 192e3,
|
|
118009
118075
|
background: "#07090b"
|
|
118010
118076
|
});
|
|
118011
|
-
function recorderMimeType() {
|
|
118077
|
+
function recorderMimeType(hasAudio = true) {
|
|
118012
118078
|
if (!globalThis.MediaRecorder) return "";
|
|
118013
|
-
|
|
118079
|
+
const candidates = hasAudio ? [
|
|
118014
118080
|
"video/webm;codecs=vp8,opus",
|
|
118015
118081
|
"video/webm;codecs=vp9,opus",
|
|
118082
|
+
"video/webm"
|
|
118083
|
+
] : [
|
|
118016
118084
|
"video/webm;codecs=vp8",
|
|
118017
118085
|
"video/webm;codecs=vp9",
|
|
118018
118086
|
"video/webm"
|
|
118019
|
-
]
|
|
118020
|
-
|
|
118021
|
-
}
|
|
118022
|
-
return "";
|
|
118087
|
+
];
|
|
118088
|
+
return candidates.find((type) => MediaRecorder.isTypeSupported?.(type)) || "";
|
|
118023
118089
|
}
|
|
118024
118090
|
function recordingVideoBitrate({ width, height, frameRate = TABLE_RECORDING_CONFIG.frameRate }) {
|
|
118025
118091
|
const pixelsPerSecond = Math.max(1, width) * Math.max(1, height) * Math.max(1, frameRate);
|
|
@@ -118037,7 +118103,7 @@ function setRecordButton(active, label = null) {
|
|
|
118037
118103
|
const text = $(".action-label", els.recordBtn);
|
|
118038
118104
|
if (icon) icon.textContent = active ? "\u25A0" : "\u25CF";
|
|
118039
118105
|
if (text) text.textContent = label || (active ? "Stop rec" : "Record");
|
|
118040
|
-
els.recordBtn.title = active ? "Stop table recording and save video" : "Record
|
|
118106
|
+
els.recordBtn.title = active ? "Stop table recording and save video" : "Record poker table with current-tab audio";
|
|
118041
118107
|
els.recordBtn.setAttribute("aria-label", els.recordBtn.title);
|
|
118042
118108
|
}
|
|
118043
118109
|
function saveRecordingBlob(blob) {
|
|
@@ -118068,72 +118134,26 @@ function fitRecordingSize(sourceWidth, sourceHeight) {
|
|
|
118068
118134
|
height: evenRecordingDimension(height * scale)
|
|
118069
118135
|
};
|
|
118070
118136
|
}
|
|
118071
|
-
function
|
|
118072
|
-
return new Promise((resolve, reject) => {
|
|
118073
|
-
let settled = false;
|
|
118074
|
-
const finish = (callback, value) => {
|
|
118075
|
-
if (settled) return;
|
|
118076
|
-
settled = true;
|
|
118077
|
-
clearTimeout(timeoutId);
|
|
118078
|
-
track?.removeEventListener?.("ended", onEnded);
|
|
118079
|
-
video.removeEventListener("loadedmetadata", onReady);
|
|
118080
|
-
video.removeEventListener("resize", onReady);
|
|
118081
|
-
callback(value);
|
|
118082
|
-
};
|
|
118083
|
-
const onReady = () => {
|
|
118084
|
-
if (video.videoWidth > 0 && video.videoHeight > 0) finish(resolve);
|
|
118085
|
-
};
|
|
118086
|
-
const onEnded = () => finish(reject, new Error("Screen sharing ended before recording started"));
|
|
118087
|
-
const timeoutId = setTimeout(
|
|
118088
|
-
() => finish(reject, new Error("Timed out waiting for the shared tab video")),
|
|
118089
|
-
8e3
|
|
118090
|
-
);
|
|
118091
|
-
track?.addEventListener?.("ended", onEnded, { once: true });
|
|
118092
|
-
video.addEventListener("loadedmetadata", onReady);
|
|
118093
|
-
video.addEventListener("resize", onReady);
|
|
118094
|
-
onReady();
|
|
118095
|
-
});
|
|
118096
|
-
}
|
|
118097
|
-
function createCaptureVideo(stream) {
|
|
118098
|
-
const video = document.createElement("video");
|
|
118099
|
-
video.muted = true;
|
|
118100
|
-
video.playsInline = true;
|
|
118101
|
-
video.autoplay = true;
|
|
118102
|
-
video.setAttribute("aria-hidden", "true");
|
|
118103
|
-
Object.assign(video.style, {
|
|
118104
|
-
position: "fixed",
|
|
118105
|
-
left: "0",
|
|
118106
|
-
top: "0",
|
|
118107
|
-
width: "2px",
|
|
118108
|
-
height: "2px",
|
|
118109
|
-
opacity: "0.001",
|
|
118110
|
-
pointerEvents: "none",
|
|
118111
|
-
zIndex: "2147483647"
|
|
118112
|
-
});
|
|
118113
|
-
video.srcObject = stream;
|
|
118114
|
-
document.body.append(video);
|
|
118115
|
-
return video;
|
|
118116
|
-
}
|
|
118117
|
-
function captureViewportMetrics(video) {
|
|
118137
|
+
function captureViewportMetrics(sourceWidth, sourceHeight) {
|
|
118118
118138
|
const viewport = globalThis.visualViewport;
|
|
118119
118139
|
const cssWidth = Math.max(1, viewport?.width || document.documentElement.clientWidth || innerWidth || 1);
|
|
118120
118140
|
const cssHeight = Math.max(1, viewport?.height || document.documentElement.clientHeight || innerHeight || 1);
|
|
118121
|
-
const
|
|
118122
|
-
const
|
|
118141
|
+
const width = Math.max(2, Number(sourceWidth) || cssWidth);
|
|
118142
|
+
const height = Math.max(2, Number(sourceHeight) || cssHeight);
|
|
118123
118143
|
return {
|
|
118124
118144
|
cssWidth,
|
|
118125
118145
|
cssHeight,
|
|
118126
|
-
sourceWidth,
|
|
118127
|
-
sourceHeight,
|
|
118128
|
-
scaleX:
|
|
118129
|
-
scaleY:
|
|
118146
|
+
sourceWidth: width,
|
|
118147
|
+
sourceHeight: height,
|
|
118148
|
+
scaleX: width / cssWidth,
|
|
118149
|
+
scaleY: height / cssHeight,
|
|
118130
118150
|
offsetX: viewport?.offsetLeft || 0,
|
|
118131
118151
|
offsetY: viewport?.offsetTop || 0
|
|
118132
118152
|
};
|
|
118133
118153
|
}
|
|
118134
|
-
function tableCropSourceRect(
|
|
118154
|
+
function tableCropSourceRect(sourceWidth, sourceHeight) {
|
|
118135
118155
|
const rect = els.pokerTable.getBoundingClientRect();
|
|
118136
|
-
const metrics = captureViewportMetrics(
|
|
118156
|
+
const metrics = captureViewportMetrics(sourceWidth, sourceHeight);
|
|
118137
118157
|
const leftCss = rect.left - metrics.offsetX;
|
|
118138
118158
|
const topCss = rect.top - metrics.offsetY;
|
|
118139
118159
|
const sx = clamp(Math.round(leftCss * metrics.scaleX), 0, Math.max(0, metrics.sourceWidth - 2));
|
|
@@ -118142,73 +118162,197 @@ function tableCropSourceRect(video) {
|
|
|
118142
118162
|
const sh = clamp(Math.round(rect.height * metrics.scaleY), 2, metrics.sourceHeight - sy);
|
|
118143
118163
|
return { sx, sy, sw, sh };
|
|
118144
118164
|
}
|
|
118145
|
-
function createTableRecordingCanvas(
|
|
118146
|
-
const source = tableCropSourceRect(
|
|
118165
|
+
function createTableRecordingCanvas(sourceWidth, sourceHeight) {
|
|
118166
|
+
const source = tableCropSourceRect(sourceWidth, sourceHeight);
|
|
118147
118167
|
const output = fitRecordingSize(source.sw, source.sh);
|
|
118148
118168
|
const canvas = document.createElement("canvas");
|
|
118149
118169
|
canvas.width = output.width;
|
|
118150
118170
|
canvas.height = output.height;
|
|
118151
|
-
const ctx = canvas.getContext("2d", { alpha: false
|
|
118171
|
+
const ctx = canvas.getContext("2d", { alpha: false });
|
|
118152
118172
|
if (!ctx) throw new Error("Canvas video recording is not available in this browser");
|
|
118153
118173
|
ctx.imageSmoothingEnabled = true;
|
|
118154
118174
|
ctx.imageSmoothingQuality = "high";
|
|
118155
118175
|
return { canvas, ctx };
|
|
118156
118176
|
}
|
|
118157
|
-
function
|
|
118158
|
-
const
|
|
118159
|
-
|
|
118160
|
-
|
|
118161
|
-
return { stream, track };
|
|
118162
|
-
}
|
|
118163
|
-
function paintTableRecordingFrame(recording) {
|
|
118164
|
-
const { ctx, canvas, captureVideo } = recording;
|
|
118165
|
-
if (!ctx || !canvas || !captureVideo || captureVideo.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return false;
|
|
118166
|
-
const source = tableCropSourceRect(captureVideo);
|
|
118167
|
-
ctx.save();
|
|
118177
|
+
function paintTableRecordingFrame(recording, source, sourceWidth, sourceHeight) {
|
|
118178
|
+
const { ctx, canvas } = recording;
|
|
118179
|
+
if (!ctx || !canvas || !source) return false;
|
|
118180
|
+
const crop = tableCropSourceRect(sourceWidth, sourceHeight);
|
|
118168
118181
|
ctx.globalCompositeOperation = "copy";
|
|
118169
118182
|
ctx.fillStyle = TABLE_RECORDING_CONFIG.background;
|
|
118170
118183
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
118171
118184
|
try {
|
|
118172
118185
|
ctx.drawImage(
|
|
118173
|
-
|
|
118174
|
-
|
|
118175
|
-
|
|
118176
|
-
|
|
118177
|
-
|
|
118186
|
+
source,
|
|
118187
|
+
crop.sx,
|
|
118188
|
+
crop.sy,
|
|
118189
|
+
crop.sw,
|
|
118190
|
+
crop.sh,
|
|
118178
118191
|
0,
|
|
118179
118192
|
0,
|
|
118180
118193
|
canvas.width,
|
|
118181
118194
|
canvas.height
|
|
118182
118195
|
);
|
|
118196
|
+
return true;
|
|
118183
118197
|
} catch {
|
|
118184
|
-
ctx.restore();
|
|
118185
118198
|
return false;
|
|
118199
|
+
} finally {
|
|
118200
|
+
ctx.globalCompositeOperation = "source-over";
|
|
118186
118201
|
}
|
|
118187
|
-
ctx.restore();
|
|
118188
|
-
return true;
|
|
118189
118202
|
}
|
|
118190
|
-
function
|
|
118203
|
+
function createCaptureVideo(stream) {
|
|
118204
|
+
const video = document.createElement("video");
|
|
118205
|
+
video.muted = true;
|
|
118206
|
+
video.playsInline = true;
|
|
118207
|
+
video.autoplay = true;
|
|
118208
|
+
video.setAttribute("aria-hidden", "true");
|
|
118209
|
+
Object.assign(video.style, {
|
|
118210
|
+
position: "fixed",
|
|
118211
|
+
inset: "0 auto auto 0",
|
|
118212
|
+
width: "2px",
|
|
118213
|
+
height: "2px",
|
|
118214
|
+
opacity: "0.001",
|
|
118215
|
+
pointerEvents: "none",
|
|
118216
|
+
zIndex: "-1"
|
|
118217
|
+
});
|
|
118218
|
+
video.srcObject = stream;
|
|
118219
|
+
document.body.append(video);
|
|
118220
|
+
return video;
|
|
118221
|
+
}
|
|
118222
|
+
function waitForCaptureVideo(video, track) {
|
|
118223
|
+
return new Promise((resolve, reject) => {
|
|
118224
|
+
let settled = false;
|
|
118225
|
+
const finish = (callback, value) => {
|
|
118226
|
+
if (settled) return;
|
|
118227
|
+
settled = true;
|
|
118228
|
+
clearTimeout(timeoutId);
|
|
118229
|
+
track?.removeEventListener?.("ended", onEnded);
|
|
118230
|
+
video.removeEventListener("loadedmetadata", onReady);
|
|
118231
|
+
video.removeEventListener("resize", onReady);
|
|
118232
|
+
callback(value);
|
|
118233
|
+
};
|
|
118234
|
+
const onReady = () => {
|
|
118235
|
+
if (video.videoWidth > 0 && video.videoHeight > 0) finish(resolve);
|
|
118236
|
+
};
|
|
118237
|
+
const onEnded = () => finish(reject, new Error("Screen sharing ended before recording started"));
|
|
118238
|
+
const timeoutId = setTimeout(
|
|
118239
|
+
() => finish(reject, new Error("Timed out waiting for the shared tab video")),
|
|
118240
|
+
8e3
|
|
118241
|
+
);
|
|
118242
|
+
track?.addEventListener?.("ended", onEnded, { once: true });
|
|
118243
|
+
video.addEventListener("loadedmetadata", onReady);
|
|
118244
|
+
video.addEventListener("resize", onReady);
|
|
118245
|
+
onReady();
|
|
118246
|
+
});
|
|
118247
|
+
}
|
|
118248
|
+
function supportsTimestampedVideoPipeline() {
|
|
118249
|
+
return typeof globalThis.MediaStreamTrackProcessor === "function" && typeof globalThis.MediaStreamTrackGenerator === "function" && typeof globalThis.VideoFrame === "function";
|
|
118250
|
+
}
|
|
118251
|
+
function createTimestampedVideoPipeline(recording) {
|
|
118252
|
+
if (!supportsTimestampedVideoPipeline()) return null;
|
|
118253
|
+
try {
|
|
118254
|
+
const processor = new MediaStreamTrackProcessor({ track: recording.captureTrack });
|
|
118255
|
+
const generator = new MediaStreamTrackGenerator({ kind: "video" });
|
|
118256
|
+
const reader = processor.readable.getReader();
|
|
118257
|
+
const writer = generator.writable.getWriter();
|
|
118258
|
+
generator.contentHint = "detail";
|
|
118259
|
+
recording.processorReader = reader;
|
|
118260
|
+
recording.generatorWriter = writer;
|
|
118261
|
+
recording.outputVideoTrack = generator;
|
|
118262
|
+
return generator;
|
|
118263
|
+
} catch {
|
|
118264
|
+
recording.processorReader = null;
|
|
118265
|
+
recording.generatorWriter = null;
|
|
118266
|
+
recording.outputVideoTrack = null;
|
|
118267
|
+
return null;
|
|
118268
|
+
}
|
|
118269
|
+
}
|
|
118270
|
+
function startTimestampedVideoPump(recording) {
|
|
118271
|
+
const reader = recording.processorReader;
|
|
118272
|
+
const writer = recording.generatorWriter;
|
|
118273
|
+
if (!reader || !writer) return;
|
|
118274
|
+
const fallbackDuration = Math.round(1e6 / TABLE_RECORDING_CONFIG.frameRate);
|
|
118275
|
+
recording.videoPumpPromise = (async () => {
|
|
118276
|
+
try {
|
|
118277
|
+
while (!recording.stopping) {
|
|
118278
|
+
const { value: frame, done } = await reader.read();
|
|
118279
|
+
if (done || !frame) break;
|
|
118280
|
+
try {
|
|
118281
|
+
const width = frame.displayWidth || frame.codedWidth || recording.sourceWidth;
|
|
118282
|
+
const height = frame.displayHeight || frame.codedHeight || recording.sourceHeight;
|
|
118283
|
+
if (!paintTableRecordingFrame(recording, frame, width, height)) continue;
|
|
118284
|
+
if (writer.desiredSize != null && writer.desiredSize <= 0) continue;
|
|
118285
|
+
const timestamp = Number.isFinite(frame.timestamp) ? frame.timestamp : Math.round(performance.now() * 1e3);
|
|
118286
|
+
const duration = Number.isFinite(frame.duration) && frame.duration > 0 ? frame.duration : fallbackDuration;
|
|
118287
|
+
const outputFrame = new VideoFrame(recording.canvas, { timestamp, duration });
|
|
118288
|
+
try {
|
|
118289
|
+
await writer.write(outputFrame);
|
|
118290
|
+
} finally {
|
|
118291
|
+
outputFrame.close();
|
|
118292
|
+
}
|
|
118293
|
+
} finally {
|
|
118294
|
+
frame.close();
|
|
118295
|
+
}
|
|
118296
|
+
}
|
|
118297
|
+
} catch (error) {
|
|
118298
|
+
if (!recording.stopping && tableRecording === recording) {
|
|
118299
|
+
recording.pipelineError = error;
|
|
118300
|
+
if (recording.recorder?.state !== "inactive") recording.recorder.stop();
|
|
118301
|
+
}
|
|
118302
|
+
}
|
|
118303
|
+
})();
|
|
118304
|
+
}
|
|
118305
|
+
function createFallbackCanvasPipeline(recording) {
|
|
118306
|
+
const stream = recording.canvas.captureStream(TABLE_RECORDING_CONFIG.frameRate);
|
|
118307
|
+
const track = stream.getVideoTracks()[0];
|
|
118308
|
+
if (!track) throw new Error("Could not create the table recording video track");
|
|
118309
|
+
track.contentHint = "detail";
|
|
118310
|
+
recording.canvasStream = stream;
|
|
118311
|
+
recording.outputVideoTrack = track;
|
|
118312
|
+
return track;
|
|
118313
|
+
}
|
|
118314
|
+
function startFallbackVideoPump(recording) {
|
|
118191
118315
|
const frameInterval = 1e3 / TABLE_RECORDING_CONFIG.frameRate;
|
|
118192
118316
|
let previousPaint = -frameInterval;
|
|
118193
118317
|
const loop = (timestamp) => {
|
|
118194
118318
|
if (recording.stopping || tableRecording !== recording) return;
|
|
118195
118319
|
if (timestamp - previousPaint >= frameInterval - 1) {
|
|
118196
|
-
|
|
118320
|
+
const video2 = recording.captureVideo;
|
|
118321
|
+
if (video2?.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
118322
|
+
paintTableRecordingFrame(recording, video2, video2.videoWidth, video2.videoHeight);
|
|
118323
|
+
}
|
|
118197
118324
|
previousPaint = timestamp;
|
|
118198
118325
|
}
|
|
118199
118326
|
recording.animationFrameId = requestAnimationFrame(loop);
|
|
118200
118327
|
};
|
|
118201
|
-
|
|
118328
|
+
const video = recording.captureVideo;
|
|
118329
|
+
if (video?.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
118330
|
+
paintTableRecordingFrame(recording, video, video.videoWidth, video.videoHeight);
|
|
118331
|
+
}
|
|
118202
118332
|
recording.animationFrameId = requestAnimationFrame(loop);
|
|
118203
118333
|
}
|
|
118204
|
-
function
|
|
118334
|
+
function startTableRecordingVideo(recording) {
|
|
118335
|
+
if (recording.processorReader) startTimestampedVideoPump(recording);
|
|
118336
|
+
else startFallbackVideoPump(recording);
|
|
118337
|
+
}
|
|
118338
|
+
function stopTableRecordingVideo(recording) {
|
|
118205
118339
|
if (!recording) return;
|
|
118206
118340
|
if (recording.animationFrameId != null) cancelAnimationFrame(recording.animationFrameId);
|
|
118207
118341
|
recording.animationFrameId = null;
|
|
118342
|
+
try {
|
|
118343
|
+
recording.processorReader?.cancel?.();
|
|
118344
|
+
} catch {
|
|
118345
|
+
}
|
|
118346
|
+
try {
|
|
118347
|
+
recording.generatorWriter?.close?.();
|
|
118348
|
+
} catch {
|
|
118349
|
+
}
|
|
118350
|
+
recording.processorReader = null;
|
|
118351
|
+
recording.generatorWriter = null;
|
|
118208
118352
|
}
|
|
118209
|
-
function disposeTableRecording(recording
|
|
118353
|
+
function disposeTableRecording(recording) {
|
|
118210
118354
|
if (!recording) return;
|
|
118211
|
-
|
|
118355
|
+
stopTableRecordingVideo(recording);
|
|
118212
118356
|
try {
|
|
118213
118357
|
recording.captureVideo?.pause?.();
|
|
118214
118358
|
} catch {
|
|
@@ -118217,28 +118361,28 @@ function disposeTableRecording(recording, { stopCapture = true, stopCanvas = tru
|
|
|
118217
118361
|
recording.captureVideo.srcObject = null;
|
|
118218
118362
|
recording.captureVideo.remove();
|
|
118219
118363
|
}
|
|
118220
|
-
|
|
118221
|
-
if (stopCapture) recording.captureStream?.getTracks?.().forEach((track) => track.stop());
|
|
118222
|
-
try {
|
|
118223
|
-
if (soundMasterGain && recording.audioDestination) soundMasterGain.disconnect(recording.audioDestination);
|
|
118224
|
-
} catch {
|
|
118225
|
-
}
|
|
118364
|
+
recording.canvasStream?.getTracks?.().forEach((track) => track.stop());
|
|
118226
118365
|
try {
|
|
118227
|
-
recording.
|
|
118366
|
+
recording.outputVideoTrack?.stop?.();
|
|
118228
118367
|
} catch {
|
|
118229
118368
|
}
|
|
118369
|
+
recording.captureStream?.getTracks?.().forEach((track) => track.stop());
|
|
118230
118370
|
recording.canvas?.remove?.();
|
|
118231
118371
|
}
|
|
118232
118372
|
function finalizeTableRecording(recording, mimeType) {
|
|
118233
118373
|
const blob = new Blob(recording.chunks, { type: recording.recorder.mimeType || mimeType || "video/webm" });
|
|
118374
|
+
const error = recording.pipelineError;
|
|
118234
118375
|
disposeTableRecording(recording);
|
|
118235
118376
|
if (tableRecording === recording) tableRecording = null;
|
|
118236
118377
|
setRecordButton(false);
|
|
118237
118378
|
if (blob.size) saveRecordingBlob(blob);
|
|
118379
|
+
if (error) showActionToast(`Recording stopped: ${summarizeError(error)}`, "fold");
|
|
118238
118380
|
}
|
|
118239
118381
|
async function startTableRecording() {
|
|
118240
118382
|
if (tableRecording || !els.pokerTable) return;
|
|
118241
|
-
|
|
118383
|
+
const canTimestampVideo = supportsTimestampedVideoPipeline();
|
|
118384
|
+
const canCanvasVideo = typeof HTMLCanvasElement.prototype.captureStream === "function";
|
|
118385
|
+
if (!navigator.mediaDevices?.getDisplayMedia || !globalThis.MediaRecorder || !canTimestampVideo && !canCanvasVideo) {
|
|
118242
118386
|
showActionToast("Table recording is not supported in this browser", "fold");
|
|
118243
118387
|
return;
|
|
118244
118388
|
}
|
|
@@ -118249,12 +118393,15 @@ async function startTableRecording() {
|
|
|
118249
118393
|
captureStream = await navigator.mediaDevices.getDisplayMedia({
|
|
118250
118394
|
video: {
|
|
118251
118395
|
displaySurface: "browser",
|
|
118252
|
-
frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate }
|
|
118396
|
+
frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate },
|
|
118397
|
+
width: { ideal: TABLE_RECORDING_CONFIG.maxWidth },
|
|
118398
|
+
height: { ideal: TABLE_RECORDING_CONFIG.maxHeight }
|
|
118253
118399
|
},
|
|
118254
|
-
audio:
|
|
118400
|
+
audio: true,
|
|
118255
118401
|
preferCurrentTab: true,
|
|
118256
118402
|
selfBrowserSurface: "include",
|
|
118257
|
-
surfaceSwitching: "exclude"
|
|
118403
|
+
surfaceSwitching: "exclude",
|
|
118404
|
+
systemAudio: "exclude"
|
|
118258
118405
|
});
|
|
118259
118406
|
const captureTrack = captureStream.getVideoTracks()[0];
|
|
118260
118407
|
if (!captureTrack) throw new Error("No video track was shared");
|
|
@@ -118262,48 +118409,74 @@ async function startTableRecording() {
|
|
|
118262
118409
|
if (displaySurface && displaySurface !== "browser") {
|
|
118263
118410
|
throw new Error("Please share this browser tab, not a window or the entire screen.");
|
|
118264
118411
|
}
|
|
118412
|
+
try {
|
|
118413
|
+
captureTrack.contentHint = "detail";
|
|
118414
|
+
} catch {
|
|
118415
|
+
}
|
|
118265
118416
|
try {
|
|
118266
118417
|
await captureTrack.applyConstraints?.({
|
|
118267
118418
|
frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate }
|
|
118268
118419
|
});
|
|
118269
118420
|
} catch {
|
|
118270
118421
|
}
|
|
118422
|
+
const audioTrack = captureStream.getAudioTracks()[0] || null;
|
|
118423
|
+
if (audioTrack?.applyConstraints) {
|
|
118424
|
+
try {
|
|
118425
|
+
await audioTrack.applyConstraints({ suppressLocalAudioPlayback: false });
|
|
118426
|
+
} catch {
|
|
118427
|
+
}
|
|
118428
|
+
}
|
|
118271
118429
|
captureVideo = createCaptureVideo(captureStream);
|
|
118272
118430
|
await captureVideo.play().catch(() => {
|
|
118273
118431
|
});
|
|
118274
118432
|
await waitForCaptureVideo(captureVideo, captureTrack);
|
|
118275
|
-
const
|
|
118276
|
-
const
|
|
118277
|
-
const {
|
|
118278
|
-
try {
|
|
118279
|
-
canvasTrack.contentHint = "detail";
|
|
118280
|
-
} catch {
|
|
118281
|
-
}
|
|
118282
|
-
const recordingAudio = recordingAudioTrack();
|
|
118283
|
-
const audioTrack = recordingAudio?.track || null;
|
|
118284
|
-
const recorderStream = audioTrack ? new MediaStream([canvasTrack, audioTrack]) : canvasStream;
|
|
118433
|
+
const sourceWidth = Math.max(2, captureVideo.videoWidth || innerWidth || 2);
|
|
118434
|
+
const sourceHeight = Math.max(2, captureVideo.videoHeight || innerHeight || 2);
|
|
118435
|
+
const { canvas, ctx } = createTableRecordingCanvas(sourceWidth, sourceHeight);
|
|
118285
118436
|
recording = {
|
|
118286
118437
|
recorder: null,
|
|
118438
|
+
recorderStream: null,
|
|
118287
118439
|
captureStream,
|
|
118288
118440
|
captureTrack,
|
|
118289
118441
|
captureVideo,
|
|
118290
118442
|
canvas,
|
|
118291
118443
|
ctx,
|
|
118292
|
-
canvasStream,
|
|
118293
|
-
|
|
118444
|
+
canvasStream: null,
|
|
118445
|
+
outputVideoTrack: null,
|
|
118446
|
+
processorReader: null,
|
|
118447
|
+
generatorWriter: null,
|
|
118448
|
+
videoPumpPromise: null,
|
|
118449
|
+
animationFrameId: null,
|
|
118450
|
+
sourceWidth,
|
|
118451
|
+
sourceHeight,
|
|
118294
118452
|
audioTrack,
|
|
118295
|
-
audioDestination: recordingAudio?.destination || null,
|
|
118296
118453
|
chunks: [],
|
|
118297
118454
|
stopping: false,
|
|
118298
|
-
|
|
118455
|
+
pipelineError: null
|
|
118299
118456
|
};
|
|
118300
|
-
|
|
118457
|
+
let outputVideoTrack = canTimestampVideo ? createTimestampedVideoPipeline(recording) : null;
|
|
118458
|
+
if (outputVideoTrack) {
|
|
118459
|
+
captureVideo.pause();
|
|
118460
|
+
captureVideo.srcObject = null;
|
|
118461
|
+
captureVideo.remove();
|
|
118462
|
+
captureVideo = null;
|
|
118463
|
+
recording.captureVideo = null;
|
|
118464
|
+
} else {
|
|
118465
|
+
if (!canCanvasVideo) throw new Error("This browser cannot create a recording video track");
|
|
118466
|
+
outputVideoTrack = createFallbackCanvasPipeline(recording);
|
|
118467
|
+
}
|
|
118468
|
+
const tracks = [outputVideoTrack];
|
|
118469
|
+
if (audioTrack) tracks.push(audioTrack);
|
|
118470
|
+
const recorderStream = new MediaStream(tracks);
|
|
118471
|
+
recording.recorderStream = recorderStream;
|
|
118472
|
+
const mimeType = recorderMimeType(Boolean(audioTrack));
|
|
118301
118473
|
const videoBitsPerSecond = recordingVideoBitrate({
|
|
118302
118474
|
width: canvas.width,
|
|
118303
118475
|
height: canvas.height,
|
|
118304
118476
|
frameRate: TABLE_RECORDING_CONFIG.frameRate
|
|
118305
118477
|
});
|
|
118306
|
-
const options = { videoBitsPerSecond
|
|
118478
|
+
const options = { videoBitsPerSecond };
|
|
118479
|
+
if (audioTrack) options.audioBitsPerSecond = TABLE_RECORDING_CONFIG.audioBitsPerSecond;
|
|
118307
118480
|
if (mimeType) options.mimeType = mimeType;
|
|
118308
118481
|
const recorder = new MediaRecorder(recorderStream, options);
|
|
118309
118482
|
recording.recorder = recorder;
|
|
@@ -118314,29 +118487,21 @@ async function startTableRecording() {
|
|
|
118314
118487
|
recorder.addEventListener("stop", () => finalizeTableRecording(recording, mimeType), { once: true });
|
|
118315
118488
|
recorder.addEventListener("error", (event) => {
|
|
118316
118489
|
if (tableRecording !== recording) return;
|
|
118490
|
+
recording.pipelineError = event.error || new Error("Recording failed");
|
|
118317
118491
|
recording.stopping = true;
|
|
118318
|
-
|
|
118319
|
-
showActionToast(summarizeError(event.error || new Error("Recording failed")), "fold");
|
|
118492
|
+
stopTableRecordingVideo(recording);
|
|
118320
118493
|
if (recorder.state !== "inactive") recorder.stop();
|
|
118321
|
-
else
|
|
118322
|
-
disposeTableRecording(recording);
|
|
118323
|
-
tableRecording = null;
|
|
118324
|
-
setRecordButton(false);
|
|
118325
|
-
}
|
|
118494
|
+
else finalizeTableRecording(recording, mimeType);
|
|
118326
118495
|
});
|
|
118327
118496
|
captureTrack.addEventListener("ended", () => {
|
|
118328
|
-
if (tableRecording === recording && recorder.state !== "inactive")
|
|
118329
|
-
recording.stopping = true;
|
|
118330
|
-
stopTableRecordingPainter(recording);
|
|
118331
|
-
recorder.stop();
|
|
118332
|
-
}
|
|
118497
|
+
if (tableRecording === recording && recorder.state !== "inactive") stopTableRecording();
|
|
118333
118498
|
}, { once: true });
|
|
118334
|
-
|
|
118335
|
-
|
|
118499
|
+
recorder.start(500);
|
|
118500
|
+
startTableRecordingVideo(recording);
|
|
118336
118501
|
setRecordButton(true);
|
|
118337
118502
|
showActionToast(
|
|
118338
|
-
`Recording \xB7 ${canvas.width}\xD7${canvas.height} \xB7 ${TABLE_RECORDING_CONFIG.frameRate} fps${audioTrack ? "
|
|
118339
|
-
"check"
|
|
118503
|
+
`Recording \xB7 ${canvas.width}\xD7${canvas.height} \xB7 ${TABLE_RECORDING_CONFIG.frameRate} fps \xB7 ${audioTrack ? "tab audio" : "no tab audio"}`,
|
|
118504
|
+
audioTrack ? "check" : "fold"
|
|
118340
118505
|
);
|
|
118341
118506
|
} catch (err) {
|
|
118342
118507
|
if (recording) disposeTableRecording(recording);
|
|
@@ -118356,14 +118521,16 @@ function stopTableRecording() {
|
|
|
118356
118521
|
const recording = tableRecording;
|
|
118357
118522
|
if (!recording || recording.stopping) return;
|
|
118358
118523
|
recording.stopping = true;
|
|
118359
|
-
|
|
118524
|
+
stopTableRecordingVideo(recording);
|
|
118360
118525
|
if (recording.recorder.state !== "inactive") {
|
|
118526
|
+
try {
|
|
118527
|
+
recording.recorder.requestData?.();
|
|
118528
|
+
} catch {
|
|
118529
|
+
}
|
|
118361
118530
|
recording.recorder.stop();
|
|
118362
118531
|
return;
|
|
118363
118532
|
}
|
|
118364
|
-
|
|
118365
|
-
if (tableRecording === recording) tableRecording = null;
|
|
118366
|
-
setRecordButton(false);
|
|
118533
|
+
finalizeTableRecording(recording, recording.recorder.mimeType);
|
|
118367
118534
|
}
|
|
118368
118535
|
function modelForPlayerId(playerId) {
|
|
118369
118536
|
return currentState?.config?.players?.find?.((p) => p.id === playerId)?.model || "";
|
|
@@ -118786,7 +118953,7 @@ els.clearTestsBtn.addEventListener("click", () => {
|
|
|
118786
118953
|
});
|
|
118787
118954
|
els.setupBtn.addEventListener("click", openSetup);
|
|
118788
118955
|
els.closeSetup.addEventListener("click", () => els.setupDialog.close());
|
|
118789
|
-
els.addConnectionBtn.addEventListener("click", () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind:
|
|
118956
|
+
els.addConnectionBtn.addEventListener("click", () => addConnectionRow({ name: `API ${els.connectionsEditor.children.length + 1}`, kind: ARENA_CONFIG.connections.defaultKind, baseUrl: connectionPresetUrl(ARENA_CONFIG.connections.defaultKind) }));
|
|
118790
118957
|
els.pauseBtn.addEventListener("click", () => {
|
|
118791
118958
|
if (!director) return;
|
|
118792
118959
|
currentState?.status === "PAUSED" ? director.resume() : director.pause();
|
|
@@ -118990,7 +119157,6 @@ loadPokerTools().catch((err) => {
|
|
|
118990
119157
|
els.setupError.classList.remove("hidden");
|
|
118991
119158
|
openSetup({ preserveError: true });
|
|
118992
119159
|
});
|
|
118993
|
-
if (!storageGet("pokertoolsArenaBrowserSeen")) storageSet("pokertoolsArenaBrowserSeen", "1");
|
|
118994
119160
|
var resizeTimer = null;
|
|
118995
119161
|
function relayoutForViewport() {
|
|
118996
119162
|
clearTimeout(resizeTimer);
|