1// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2// botDevPanel.js â In-browser dev panel for bot simulation 3// 4// Adds a debug panel (hidden by default) that lets developers: 5// ⢠Launch N runs with chosen profile/skill/difficulty 6// ⢠See real-time progress bar 7// ⢠View aggregated metrics and balance alerts 8// ⢠Export results as JSON/CSV download 9// 10// This module imports the simulation engine dynamically 11// and runs everything client-side with NO server dependency. 12// 13// Enable via URL param: ?devbot=1 or console: BotDevPanel.show() 14// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 15 16const BotDevPanel = (() => { 17 let _panel = null; 18 let _running = false; 19 let _worker = null; 20 21 // ââ Create/show the panel ââ 22 function show() { 23 if (_panel) { _panel.style.display = "flex"; return; } 24 _buildUI(); 25 _panel.style.display = "flex"; 26 } 27 28 function hide() { 29 if (_panel) _panel.style.display = "none"; 30 } 31 32 function toggle() { 33 if (_panel && _panel.style.display !== "none") hide(); 34 else show(); 35 } 36 37 // ââ Build the dev panel UI ââ 38 function _buildUI() { 39 _panel = document.createElement("div"); 40 _panel.id = "botDevPanel"; 41 _panel.innerHTML = ` 42 <div class="bdp-header"> 43 <span>ð¤ Bot Dev Panel</span> 44 <button class="bdp-close" title="Cerrar">â</button> 45 </div> 46 <div class="bdp-body"> 47 <div class="bdp-row"> 48 <label>Runs:</label> 49 <input type="number" id="bdpRuns" value="100" min="1" max="10000"> 50 </div> 51 <div class="bdp-row"> 52 <label>Perfiles:</label> 53 <select id="bdpProfiles" multiple size="4"> 54 <option value="muro" selected>ð§± Muro</option> 55 <option value="velocista" selected>â¡ Velocista</option> 56 <option value="mercader" selected>ð° Mercader</option> 57 <option value="pianista" selected>ð¹ Pianista</option> 58 <option value="novato">ð£ Novato</option> 59 <option value="veterano">ð Veterano</option> 60 <option value="dios">ð Dios</option> 61 <option value="aleatorio">ð² Aleatorio</option> 62 </select> 63 </div> 64 <div class="bdp-row"> 65 <label>Skill override:</label> 66 <select id="bdpSkill"> 67 <option value="">â desde perfil â</option> 68 <option value="beginner">Beginner</option> 69 <option value="casual">Casual</option> 70 <option value="average">Average</option> 71 <option value="skilled">Skilled</option> 72 <option value="expert">Expert</option> 73 <option value="godlike">Godlike</option> 74 </select> 75 </div> 76 <div class="bdp-row"> 77 <label>Modo:</label> 78 <select id="bdpMode"> 79 <option value="party">Party</option> 80 <option value="competitive">Competitive</option> 81 </select> 82 </div> 83 <div class="bdp-row"> 84 <label>Dificultad:</label> 85 <select id="bdpDiff"> 86 <option value="easy">Easy</option> 87 <option value="normal" selected>Normal</option> 88 <option value="hard">Hard</option> 89 <option value="insane">Insane</option> 90 </select> 91 </div> 92 <div class="bdp-row"> 93 <label>Seed:</label> 94 <input type="text" id="bdpSeed" placeholder="random" value=""> 95 </div> 96 <div class="bdp-actions"> 97 <button id="bdpRunBtn" class="bdp-btn bdp-btn--primary">â¶ Ejecutar</button> 98 <button id="bdpStopBtn" class="bdp-btn" disabled>â¹ Parar</button> 99 <button id="bdpExportJson" class="bdp-btn" disabled>ð¾ JSON</button> 100 <button id="bdpExportCsv" class="bdp-btn" disabled>ð CSV</button> 101 </div> 102 <div class="bdp-progress" style="display:none"> 103 <div class="bdp-progress-bar"><div class="bdp-progress-fill" id="bdpProgressFill"></div></div>
104 <span id="bdpProgressText">0%</span> 105 </div> 106 <div class="bdp-results" id="bdpResults" style="display:none"></div> 107 <div class="bdp-alerts" id="bdpAlerts" style="display:none"></div> 108 </div> 109 `; 110 111 document.body.appendChild(_panel); 112 113 // Events 114 _panel.querySelector(".bdp-close").addEventListener("click", hide); 115 document.getElementById("bdpRunBtn").addEventListener("click", _startSimulation); 116 document.getElementById("bdpStopBtn").addEventListener("click", _stopSimulation); 117 document.getElementById("bdpExportJson").addEventListener("click", _exportJSON); 118 document.getElementById("bdpExportCsv").addEventListener("click", _exportCSV); 119 } 120 121 // ââ Run simulation ââ 122 let _lastResult = null; 123 124 async function _startSimulation() { 125 if (_running) return; 126 _running = true; 127 128 const runBtn = document.getElementById("bdpRunBtn"); 129 const stopBtn = document.getElementById("bdpStopBtn"); 130 const exportJsonBtn = document.getElementById("bdpExportJson"); 131 const exportCsvBtn = document.getElementById("bdpExportCsv"); 132 const progressDiv = _panel.querySelector(".bdp-progress"); 133 const resultsDiv = document.getElementById("bdpResults"); 134 const alertsDiv = document.getElementById("bdpAlerts"); 135 136 runBtn.disabled = true; 137 stopBtn.disabled = false; 138 exportJsonBtn.disabled = true; 139 exportCsvBtn.disabled = true; 140 progressDiv.style.display = "flex"; 141 resultsDiv.style.display = "none"; 142 alertsDiv.style.display = "none"; 143 resultsDiv.innerHTML = ""; 144 alertsDiv.innerHTML = ""; 145 146 const numRuns = parseInt(document.getElementById("bdpRuns").value, 10) || 100; 147 const profileSel = document.getElementById("bdpProfiles"); 148 const profiles = [...profileSel.selectedOptions].map(o => o.value); 149 const skill = document.getElementById("bdpSkill").value || null; 150 const mode = document.getElementById("bdpMode").value; 151 const diff = document.getElementById("bdpDiff").value; 152 const seed = document.getElementById("bdpSeed").value || null; 153 154 try { 155 // Dynamic import of ESM simulation modules 156 const { runBatch } = await import('./bot/botSim.mjs'); 157 158 const startMs = performance.now(); 159 160 const result = runBatch({ 161 runs: numRuns, 162 profiles, 163 skillTiers: skill ? profiles.map(() => skill) : null, 164 mode, 165 difficultyPreset: diff, 166 seed, 167 verbose: false, 168 onProgress: (completed, total) => { 169 const pct = Math.round(completed / total * 100); 170 document.getElementById("bdpProgressFill").style.width = pct + "%"; 171 document.getElementById("bdpProgressText").textContent = `${pct}% (${completed}/${total})`; 172 }, 173 }); 174 175 const elapsed = Math.round(performance.now() - startMs); 176 _lastResult = result; 177 178 // Show results 179 _renderResults(result.aggregated, elapsed, numRuns); 180 _renderAlerts(result.alerts); 181 182 exportJsonBtn.disabled = false; 183 exportCsvBtn.disabled = false; 184 } catch (err) { 185 resultsDiv.style.display = "block"; 186 resultsDiv.innerHTML = `<div class="bdp-error">â Error: ${err.message}</div>`; 187 console.error("Bot simulation error:", err); 188 } 189 190 _running = false; 191 runBtn.disabled = false; 192 stopBtn.disabled = true; 193 } 194 195 function _stopSimulation() { 196 // For now, can't cancel sync execution â placeholder 197 _running = false; 198 } 199 200 function _renderResults(agg, elapsedMs, numRuns) { 201 const div = document.getElementById("bdpResults"); 202 div.style.display = "block"; 203 204 const p = agg.progression; 205 const e = agg.economy; 206 const perf = agg.performance; 207 208 let html = `<h4>ð Resultados (${numRuns} runs, ${elapsedMs}ms)</h4>`; 209 html += `<div class="bdp-stats-grid">`; 210 html += _statCard("Pisos", p.floors.avg, `med=${p.floors.med} Ï=${p.floors.std}`); 211 html += _statCard("Score", p.score.avg, `med=${p.score.med} p90=${p.score.p90}`); 212 html += _statCard("Win Rate", (perf.winRate.avg * 100).toFixed(1) + "%", `p10=${(perf.winRate.p10 * 100).toFixed(0)}% p90=${(perf.winRate.p90 * 100).toFixed(0)}%`); 213 html += _statCard("Max Racha", perf.maxStreak.avg, `max=${perf.maxStreak.max}`); 214 html += _statCard("Coins Ganados", e.coinsEarned.avg, `Ï=${e.coinsEarned.std}`); 215 html += _statCard("Coins Gastados", e.coinsSpent.avg, ``); 216 html += `</div>`; 217 218 // Floor survival sparkline 219 html += `<h4>ð Supervivencia por piso</h4><div class="bdp-floor-bars">`; 220 for (const fs of (agg.floorProgression || []).slice(0, 15)) { 221 const h = Math.round(fs.survivalPct * 100); 222 html += `<div class="bdp-floor-bar" title="Piso ${fs.floor}: ${h}% superv, ${(fs.flawlessPct * 100).toFixed(0)}% flawless" style="height:${h}%"><span>${fs.floor}</span></div>`; 223 } 224 html += `</div>`; 225 226 // Top death mutators 227 html += `<h4>ð Top causas de muerte</h4><div class="bdp-death-list">`; 228 for (const m of (agg.mutators?.deathCauses || []).slice(0, 5)) { 229 html += `<div class="bdp-death-row"><span>${m.id}</span><div class="bdp-death-bar" style="width:${m.pct}%"></div><span>${m.pct}%</span></div>`; 230 } 231 html += `</div>`; 232 233 div.innerHTML = html; 234 } 235 236 function _statCard(label, value, sub) { 237 return `<div class="bdp-stat-card"><div class="bdp-stat-val">${value}</div><div class="bdp-stat-label">${label}</div><div class="bdp-stat-sub">${sub}</div></div>`; 238 } 239 240 function _renderAlerts(alerts) { 241 const div = document.getElementById("bdpAlerts"); 242 if (alerts.length === 0) { 243 div.style.display = "block"; 244 div.innerHTML = `<div class="bdp-alert bdp-alert--ok">â Sin alertas de balance</div>`; 245 return; 246 } 247 div.style.display = "block"; 248 let html = `<h4>â ï¸ Alertas (${alerts.length})</h4>`; 249 for (const a of alerts) { 250 const cls = a.level === "alert" ? "bdp-alert--danger" : a.level === "warning" ? "bdp-alert--warn" : "bdp-alert--info"; 251 html += `<div class="bdp-alert ${cls}">[${a.type}] ${a.message}</div>`; 252 } 253 div.innerHTML = html; 254 } 255 256 // ââ Export ââ 257 function _exportJSON() { 258 if (!_lastResult) return; 259 _download("twister-bot-results.json", _lastResult.json, "application/json"); 260 } 261 262 function _exportCSV() { 263 if (!_lastResult) return; 264 _download("twister-bot-results.csv", _lastResult.csv, "text/csv"); 265 } 266 267 function _download(filename, content, mime) { 268 const blob = new Blob([content], { type: mime }); 269 const url = URL.createObjectURL(blob); 270 const a = document.createElement("a"); 271 a.href = url; a.download = filename; 272 document.body.appendChild(a); a.click(); 273 document.body.removeChild(a); 274 URL.revokeObjectURL(url); 275 } 276 277 // ââ Auto-init if URL param present ââ 278 if (typeof window !== 'undefined') { 279 const params = new URLSearchParams(window.location.search); 280 if (params.get("devbot") === "1") { 281 if (document.readyState === "loading") { 282 document.addEventListener("DOMContentLoaded", show); 283 } else { 284 show(); 285 } 286 } 287 } 288 289 return { show, hide, toggle }; 290})(); 291 292// Expose globally for console access 293if (typeof window !== 'undefined') { 294 window.BotDevPanel = BotDevPanel; 295}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.