huzzle-labs/visual_memory
0
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="UTF-8">5<meta name="viewport" content="width=device-width, initial-scale=1.0">6<title>Phantom Grid — Visual Memory Game</title>7<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;800&family=Orbitron:wght@500;700;900&display=swap" rel="stylesheet">8<style>9:root {10 --bg: #0d0d1a;11 --panel: #141428;12 --border: #2a2a50;13 --accent: #7b61ff;14 --accent-glow: rgba(123,97,255,0.3);15 --danger: #ff4d4d;16 --success: #4dff88;17 --warning: #ffb84d;18 --text: #e0e0f0;19 --text-dim: #8888aa;20 --text-bright: #ffffff;21}22* { margin:0; padding:0; box-sizing:border-box; }23body {24 background: var(--bg);25 color: var(--text);26 font-family: 'JetBrains Mono', monospace;27 min-height: 100vh;28}29.header {30 text-align: center;31 padding: 24px 16px 16px;32 border-bottom: 1px solid var(--border);33 background: linear-gradient(180deg, #12122a 0%, var(--bg) 100%);34}35.header h1 {36 font-family: 'Orbitron', sans-serif;37 font-weight: 900;38 font-size: 28px;39 letter-spacing: 3px;40 background: linear-gradient(135deg, var(--accent), #ff61a6);41 -webkit-background-clip: text;42 -webkit-text-fill-color: transparent;43 margin-bottom: 4px;44}45.header p { color: var(--text-dim); font-size: 12px; }46.layout {47 display: flex;48 gap: 16px;49 padding: 16px;50 max-width: 1400px;51 margin: 0 auto;52 align-items: flex-start;53 justify-content: center;54}55.board-panel {56 flex: 0 0 auto;57 background: var(--panel);58 border: 1px solid var(--border);59 border-radius: 12px;60 padding: 16px;61 min-height: 420px;62 display: flex;63 flex-direction: column;64 align-items: center;65 justify-content: center;66 position: relative;67}68.board-wrap { position: relative; cursor: crosshair; }69.board-wrap svg { display: block; }70.click-overlay {71 position: absolute;72 top: 0;73 left: 0;74 width: 100%;75 height: 100%;76}77.click-cell {78 position: absolute;79 border: 2px solid transparent;80 transition: border-color 0.15s;81 cursor: pointer;82}83.click-cell:hover { border-color: var(--accent); }84.click-cell.selected { border-color: var(--warning); background: rgba(255,184,77,0.15); }85.selected-label {86 position: absolute;87 bottom: -22px;88 left: 0;89 right: 0;90 text-align: center;91 font-size: 11px;92 color: var(--warning);93 font-weight: 600;94}95.controls {96 flex: 1;97 display: flex;98 flex-direction: column;99 gap: 12px;100 min-width: 300px;101 max-width: 380px;102}103.card {104 background: var(--panel);105 border: 1px solid var(--border);106 border-radius: 10px;107 padding: 14px;108}109.card h3 {110 font-family: 'Orbitron', sans-serif;111 font-size: 11px;112 font-weight: 700;113 letter-spacing: 2px;114 text-transform: uppercase;115 color: var(--accent);116 margin-bottom: 10px;117}118.scenario-grid {119 display: grid;120 grid-template-columns: 1fr 1fr;121 gap: 6px;122}123.scenario-btn {124 background: transparent;125 border: 1px solid var(--border);126 color: var(--text);127 padding: 8px 6px;128 border-radius: 6px;129 font-family: 'JetBrains Mono', monospace;130 font-size: 10px;131 cursor: pointer;132 transition: all 0.2s;133 text-align: left;134}135.scenario-btn:hover { border-color: var(--accent); background: var(--accent-glow); }136.scenario-btn.active { border-color: var(--accent); background: var(--accent-glow); color: var(--text-bright); }137.scenario-btn:disabled { opacity: 0.5; cursor: wait; }138.action-row {139 display: flex;140 gap: 6px;141 align-items: center;142 flex-wrap: wrap;143}144.btn {145 padding: 8px 14px;146 border: 1px solid var(--border);147 border-radius: 6px;148 font-family: 'JetBrains Mono', monospace;149 font-size: 11px;150 cursor: pointer;151 transition: all 0.2s;152 font-weight: 600;153 white-space: nowrap;154}155.btn:disabled { opacity: 0.4; cursor: not-allowed; }156.btn-reveal { background: #1a3a5c; color: #7bc8ff; border-color: #2a5a8c; }157.btn-reveal:hover:not(:disabled) { background: #2a4a6c; }158.btn-flag { background: #5c1a1a; color: #ff7b7b; border-color: #8c2a2a; }159.btn-flag:hover:not(:disabled) { background: #6c2a2a; }160.btn-inspect { background: #1a3a2a; color: #7bffaa; border-color: #2a5a3a; }161.btn-inspect:hover:not(:disabled) { background: #2a4a3a; }162.btn-viewport { background: #3a2a1a; color: #ffcc7b; border-color: #5a4a2a; }163.btn-viewport:hover:not(:disabled) { background: #4a3a2a; }164.btn-submit {165 background: var(--accent);166 color: white;167 border-color: var(--accent);168 width: 100%;169 padding: 10px;170 font-size: 13px;171 letter-spacing: 1px;172}173.btn-submit:hover:not(:disabled) { filter: brightness(1.2); }174.btn-secondary { background: transparent; color: var(--text-dim); border-color: var(--border); }175.btn-secondary:hover:not(:disabled) { color: var(--text); border-color: var(--text-dim); }176.status-bar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }177.stat { text-align: center; padding: 8px 4px; background: var(--bg); border-radius: 6px; }178.stat-val { font-family: 'Orbitron', sans-serif; font-size: 18px; font-weight: 700; color: var(--text-bright); }179.stat-label { font-size: 9px; color: var(--text-dim); margin-top: 2px; }180.mode-toggle {181 display: flex;182 gap: 4px;183 margin-bottom: 8px;184}185.mode-btn {186 flex: 1;187 padding: 8px 4px;188 border: 1px solid var(--border);189 border-radius: 6px;190 font-family: 'JetBrains Mono', monospace;191 font-size: 11px;192 cursor: pointer;193 text-align: center;194 transition: all 0.2s;195 background: transparent;196 color: var(--text-dim);197}198.mode-btn.active-reveal { border-color: #2a5a8c; background: #1a3a5c; color: #7bc8ff; }199.mode-btn.active-flag { border-color: #8c2a2a; background: #5c1a1a; color: #ff7b7b; }200.help-text { font-size: 10px; color: var(--text-dim); line-height: 1.5; margin-top: 6px; }201#log {202 max-height: 200px;203 overflow-y: auto;204 font-size: 11px;205 line-height: 1.6;206 color: var(--text-dim);207 background: var(--bg);208 border-radius: 6px;209 padding: 8px;210}211.log-entry { border-bottom: 1px solid #1a1a30; padding: 2px 0; }212.log-success { color: var(--success) !important; }213.log-danger { color: var(--danger) !important; }214.log-warn { color: var(--warning) !important; }215.log-info { color: var(--accent) !important; }216.empty-board {217 color: var(--text-dim);218 font-size: 14px;219 text-align: center;220 padding: 80px 20px;221 line-height: 2;222}223.flagged-list {224 font-size: 11px;225 color: var(--text-dim);226 background: var(--bg);227 border-radius: 6px;228 padding: 8px;229 min-height: 28px;230 word-break: break-all;231}232.game-over-banner {233 text-align: center;234 padding: 12px;235 border-radius: 8px;236 font-family: 'Orbitron', sans-serif;237 font-size: 14px;238 font-weight: 700;239 letter-spacing: 2px;240 animation: pulse 1.5s infinite;241}242@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.7; } }243.win { background: rgba(77,255,136,0.15); color: var(--success); border: 1px solid var(--success); }244.lose { background: rgba(255,77,77,0.15); color: var(--danger); border: 1px solid var(--danger); }245.scenario-info {246 background: var(--bg);247 border: 1px solid var(--border);248 border-radius: 8px;249 padding: 12px 14px;250 margin-top: 10px;251 font-size: 12px;252 line-height: 1.6;253 color: var(--text);254 max-width: 540px;255 width: 100%;256}257.scenario-info .info-title {258 font-family: 'Orbitron', sans-serif;259 font-size: 10px;260 font-weight: 700;261 letter-spacing: 2px;262 text-transform: uppercase;263 color: var(--success);264 margin-bottom: 6px;265}266.scenario-info .info-goal {267 color: var(--warning);268 font-weight: 600;269 margin-bottom: 4px;270 font-size: 11px;271}272.scenario-info .info-text {273 color: var(--text-dim);274 font-size: 11px;275}276.legend {277 display: flex; gap: 12px; flex-wrap: wrap;278 justify-content: center; margin-top: 10px; font-size: 10px;279}280.legend-item { display: flex; align-items: center; gap: 4px; }281.legend-swatch {282 width: 14px; height: 14px; border-radius: 3px; border: 1px solid #3d3d5a;283}284</style>285</head>286<body>287<div class="header">288 <h1>PHANTOM GRID</h1>289 <p>Visual Memory Gym — Click cells to reveal or flag. Play server on port 8001.</p>290</div>291<div class="layout">292 <div class="board-panel">293 <div id="board-container">294 <div class="empty-board">295 Select a scenario on the right to begin.<br><br>296 <b>How to play:</b><br>297 1. Pick a scenario<br>298 2. Click cells on the board to reveal or flag them<br>299 3. Use signals (numbers) to deduce hazard locations<br>300 4. Flag all hazards, then submit your solution301 </div>302 </div>303 <div class="scenario-info" id="scenario-info" style="display:none">304 <div class="info-title">HOW TO PLAY</div>305 <div class="info-goal" id="info-goal"></div>306 <div class="info-text" id="info-text"></div>307 </div>308 <div class="legend" id="legend" style="display:none">309 <div class="legend-item"><div class="legend-swatch" style="background:#2d2d4a"></div> Hidden</div>310 <div class="legend-item"><div class="legend-swatch" style="background:#d0e8ff"></div> Signal</div>311 <div class="legend-item"><div class="legend-swatch" style="background:#e8e8f0"></div> Empty</div>312 <div class="legend-item"><div class="legend-swatch" style="background:#ff4d4d"></div> Hazard</div>313 <div class="legend-item"><div class="legend-swatch" style="background:#ff6b35"></div> Flagged</div>314 <div class="legend-item"><div class="legend-swatch" style="background:#ffd700"></div> Key</div>315 <div class="legend-item"><div class="legend-swatch" style="background:#c8b8e8"></div> Decoy</div>316 <div class="legend-item"><div class="legend-swatch" style="background:#50fa7b"></div> Goal</div>317 <div class="legend-item"><div class="legend-swatch" style="background:#111122"></div> Fog</div>318 </div>319 </div>320 <div class="controls">321 <div class="card">322 <h3>Scenario</h3>323 <div class="scenario-grid" id="scenario-grid"></div>324 </div>325 <div class="card">326 <h3>Status</h3>327 <div class="status-bar">328 <div class="stat"><div class="stat-val" id="stat-steps">—</div><div class="stat-label">STEPS</div></div>329 <div class="stat"><div class="stat-val" id="stat-max">—</div><div class="stat-label">MAX</div></div>330 <div class="stat"><div class="stat-val" id="stat-flags">—</div><div class="stat-label">FLAGS</div></div>331 <div class="stat"><div class="stat-val" id="stat-revealed">—</div><div class="stat-label">REVEALED</div></div>332 </div>333 <div id="game-over-slot" style="margin-top:8px"></div>334 </div>335 <div class="card">336 <h3>Click Mode</h3>337 <div class="mode-toggle">338 <button class="mode-btn active-reveal" id="mode-reveal" onclick="setMode('reveal')">339 Reveal340 </button>341 <button class="mode-btn" id="mode-flag" onclick="setMode('flag')">342 Flag Hazard343 </button>344 </div>345 <p class="help-text" id="mode-help">Click any dark (hidden) cell on the board to reveal it.</p>346 </div>347 <div class="card">348 <h3>Tools</h3>349 <div style="display:flex;flex-direction:column;gap:8px">350 <div class="action-row">351 <button class="btn btn-inspect" onclick="doInspect()">Inspect Region</button>352 <button class="btn btn-viewport" onclick="doMoveViewport()">Move Viewport</button>353 </div>354 <div class="action-row">355 <button class="btn btn-secondary" onclick="doRecall()">Recall Log</button>356 <button class="btn btn-secondary" onclick="doGetStatus()">Get Status</button>357 </div>358 </div>359 </div>360 <div class="card">361 <h3>Flagged Cells <span id="flag-count" style="color:var(--text-dim)">(0)</span></h3>362 <div class="flagged-list" id="flagged-list">No cells flagged yet</div>363 </div>364 <div class="card">365 <button class="btn btn-submit" onclick="doSubmit()">SUBMIT SOLUTION</button>366 </div>367 <div class="card">368 <h3>Game Log</h3>369 <div id="log"></div>370 </div>371 </div>372</div>373 374<script>375const API = 'http://localhost:8001';376let clickMode = 'reveal';377let boardWidth = 0;378let boardHeight = 0;379let flaggedCells = [];380let selectedRow = -1;381let selectedCol = -1;382let gameOver = false;383 384const CELL_SIZE = 48;385const PADDING = 24;386 387const SCENARIOS = [388 {id:'flash_fade_minefield_7x7', label:'Flash Fade 7x7', desc:'Pattern memory'},389 {id:'directional_trap_8x8', label:'Directional Trap 8x8', desc:'1 hit = fatal'},390 {id:'partial_intel_9x9', label:'Partial Intel 9x9', desc:'Incomplete signals'},391 {id:'delayed_recall_keys_8x8', label:'Delayed Recall 8x8', desc:'Collect 5 keys'},392 {id:'ambiguous_cluster_10x10', label:'Ambiguous Cluster 10x10', desc:'Range signals'},393 {id:'decoy_minefield_8x10', label:'Decoy Minefield 8x10', desc:'4 keys, 8 decoys'},394 {id:'fog_labyrinth_10x10', label:'Fog Labyrinth 10x10', desc:'Viewport radius 2'},395 {id:'fog_key_hunt_8x8', label:'Fog Key Hunt 8x8', desc:'Tiny viewport'},396 {id:'cascading_deduction_11x11', label:'Cascading 11x11', desc:'25 hazards'},397 {id:'safe_zone_identification_9x9', label:'Safe Zone ID 9x9', desc:'Find safe cells'},398];399 400function initScenarios() {401 const grid = document.getElementById('scenario-grid');402 SCENARIOS.forEach(s => {403 const btn = document.createElement('button');404 btn.className = 'scenario-btn';405 btn.innerHTML = `${s.label}<br><span style="color:var(--text-dim);font-size:9px">${s.desc}</span>`;406 btn.dataset.id = s.id;407 btn.onclick = () => loadScenario(s.id);408 grid.appendChild(btn);409 });410}411 412function setMode(mode) {413 clickMode = mode;414 document.getElementById('mode-reveal').className = 'mode-btn' + (mode === 'reveal' ? ' active-reveal' : '');415 document.getElementById('mode-flag').className = 'mode-btn' + (mode === 'flag' ? ' active-flag' : '');416 document.getElementById('mode-help').textContent = mode === 'reveal'417 ? 'Click any dark (hidden) cell on the board to reveal it.'418 : 'Click any dark (hidden) cell to flag it as a hazard.';419}420 421function log(msg, cls) {422 const el = document.getElementById('log');423 const div = document.createElement('div');424 div.className = 'log-entry ' + (cls || '');425 div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;426 el.prepend(div);427}428 429function updateBoard(svgText) {430 if (!svgText) return;431 const container = document.getElementById('board-container');432 const wrap = document.createElement('div');433 wrap.className = 'board-wrap';434 wrap.innerHTML = svgText;435 436 const overlay = document.createElement('div');437 overlay.className = 'click-overlay';438 439 for (let r = 0; r < boardHeight; r++) {440 for (let c = 0; c < boardWidth; c++) {441 const cell = document.createElement('div');442 cell.className = 'click-cell';443 cell.setAttribute('role', 'button');444 cell.setAttribute('aria-label', `cell ${r} ${c}`);445 cell.setAttribute('tabindex', '0');446 cell.style.left = (PADDING + c * CELL_SIZE) + 'px';447 cell.style.top = (PADDING + r * CELL_SIZE) + 'px';448 cell.style.width = CELL_SIZE + 'px';449 cell.style.height = CELL_SIZE + 'px';450 cell.dataset.row = r;451 cell.dataset.col = c;452 cell.title = `(${r}, ${c})`;453 cell.onclick = () => onCellClick(r, c);454 overlay.appendChild(cell);455 }456 }457 458 wrap.appendChild(overlay);459 container.innerHTML = '';460 container.appendChild(wrap);461 document.getElementById('legend').style.display = 'flex';462}463 464function updateStats(status) {465 if (!status) return;466 document.getElementById('stat-steps').textContent = status.step_count ?? '—';467 document.getElementById('stat-max').textContent = status.max_steps ?? '—';468 document.getElementById('stat-flags').textContent = status.flags_remaining ?? '—';469 document.getElementById('stat-revealed').textContent = status.cells_revealed ?? '—';470 gameOver = !!status.game_over;471}472 473function updateFlaggedList() {474 const el = document.getElementById('flagged-list');475 document.getElementById('flag-count').textContent = `(${flaggedCells.length})`;476 if (flaggedCells.length === 0) { el.textContent = 'No cells flagged yet'; return; }477 el.textContent = flaggedCells.map(c => `[${c[0]},${c[1]}]`).join(' ');478}479 480function showGameOver(won, msg) {481 document.getElementById('game-over-slot').innerHTML =482 `<div class="game-over-banner ${won ? 'win' : 'lose'}">${msg}</div>`;483}484 485async function api(method, path, body) {486 try {487 const opts = { method, headers: {'Content-Type': 'application/json'} };488 if (body !== undefined) opts.body = JSON.stringify(body);489 const resp = await fetch(API + path, opts);490 return await resp.json();491 } catch (e) {492 log('Connection error: ' + e.message + ' — is play_server.py running on port 8001?', 'log-danger');493 return { error: e.message };494 }495}496 497function processResponse(data) {498 if (data.error) { log(data.error, 'log-danger'); return; }499 if (data.board && data.board.svg) {500 updateBoard(data.board.svg);501 if (data.board.metadata) {502 document.getElementById('stat-revealed').textContent = data.board.metadata.cell_counts?.revealed ?? '—';503 }504 }505 if (data.status) updateStats(data.status);506 return data.action_result;507}508 509async function loadScenario(id) {510 document.querySelectorAll('.scenario-btn').forEach(b => { b.classList.remove('active'); b.disabled = true; });511 const btn = document.querySelector(`[data-id='${id}']`);512 if (btn) btn.classList.add('active');513 document.getElementById('game-over-slot').innerHTML = '';514 flaggedCells = [];515 updateFlaggedList();516 document.getElementById('log').innerHTML = '';517 gameOver = false;518 519 log(`Loading scenario: ${id}...`, 'log-info');520 const data = await api('POST', '/load', { scenario_id: id });521 522 document.querySelectorAll('.scenario-btn').forEach(b => b.disabled = false);523 524 if (data.error) { log(data.error, 'log-danger'); return; }525 boardWidth = data.status?.board_size ? parseInt(data.status.board_size.split('x')[0]) : 0;526 boardHeight = data.status?.board_size ? parseInt(data.status.board_size.split('x')[1]) : 0;527 processResponse(data);528 529 const winLabels = {530 'flag_all_hazards': 'Flag all hazards, then submit.',531 'collect_keys': 'Find and reveal all keys to win.',532 'identify_safe_cells': 'Identify all safe (non-hazard) cells, then submit.',533 'reach_goal': 'Reach the goal cell to win.',534 };535 const wc = data.status?.win_condition || '';536 const goalText = winLabels[wc] || `Win condition: ${wc}`;537 const howTo = data.how_to_play || '';538 539 const infoEl = document.getElementById('scenario-info');540 if (howTo) {541 document.getElementById('info-goal').textContent = goalText;542 document.getElementById('info-text').textContent = howTo;543 infoEl.style.display = 'block';544 } else {545 infoEl.style.display = 'none';546 }547 548 log(`Loaded ${boardWidth}x${boardHeight} board | Type: ${data.status?.scenario_type} | Win: ${data.status?.win_condition} | Max steps: ${data.status?.max_steps}`, 'log-success');549}550 551async function onCellClick(row, col) {552 if (gameOver) { log('Game is over. Load a new scenario to play again.', 'log-warn'); return; }553 if (boardWidth === 0) { log('Load a scenario first.', 'log-warn'); return; }554 555 if (clickMode === 'reveal') {556 log(`Revealing (${row}, ${col})...`);557 const data = await api('POST', '/reveal', { row, col });558 const result = processResponse(data);559 if (!result) return;560 if (result.error) { log(result.error, 'log-danger'); return; }561 562 const t = result.type || '';563 if (t === 'hazard') log(`HAZARD at (${row},${col})!${result.game_over ? ' GAME OVER!' : ''}`, 'log-danger');564 else if (t === 'key') log(`KEY found at (${row},${col})!`, 'log-success');565 else if (t === 'signal') log(`Signal at (${row},${col}): ${JSON.stringify(result.value)}`, 'log-info');566 else if (t === 'decoy') log(`Decoy at (${row},${col})`, 'log-warn');567 else if (t === 'goal') log(`GOAL reached at (${row},${col})!`, 'log-success');568 else if (t === 'empty') log(`Empty cell at (${row},${col})`, '');569 else log(`Cell (${row},${col}): ${JSON.stringify(result).slice(0,120)}`, 'log-info');570 571 if (result.game_over && result.message) {572 showGameOver(!!result.message?.includes('win') || !!result.message?.includes('Win'), result.message);573 }574 575 } else {576 const alreadyFlagged = flaggedCells.some(f => f[0] === row && f[1] === col);577 if (alreadyFlagged) {578 log(`Unflagging (${row}, ${col})...`);579 const data = await api('POST', '/unflag', { row, col });580 const result = processResponse(data);581 if (!result) return;582 if (result.error) { log(result.error, 'log-danger'); return; }583 flaggedCells = flaggedCells.filter(f => !(f[0] === row && f[1] === col));584 updateFlaggedList();585 log(`Unflagged (${row},${col})`, 'log-warn');586 } else {587 log(`Flagging (${row}, ${col})...`);588 const data = await api('POST', '/flag', { row, col });589 const result = processResponse(data);590 if (!result) return;591 if (result.error) { log(result.error, 'log-danger'); return; }592 flaggedCells.push([row, col]);593 updateFlaggedList();594 log(`Flagged (${row},${col}) as hazard | ${result.flags_remaining} flags left`, 'log-warn');595 if (result.game_over && result.message) {596 showGameOver(true, result.message);597 }598 }599 }600}601 602async function doInspect() {603 const row = parseInt(prompt('Center row:', '0'));604 const col = parseInt(prompt('Center col:', '0'));605 const radius = parseInt(prompt('Radius (1-3):', '1'));606 if (isNaN(row) || isNaN(col)) return;607 log(`Inspecting region around (${row},${col}) r=${radius}...`, 'log-info');608 const data = await api('POST', '/inspect', { center_row: row, center_col: col, radius: radius || 1 });609 const result = processResponse(data);610 if (!result) return;611 if (result.error) { log(result.error, 'log-danger'); return; }612 const cells = result.cells || [];613 log(`Inspected ${cells.length} cells`, 'log-info');614 cells.forEach(c => {615 if (c.state !== 'hidden' && c.state !== 'fog') {616 log(` (${c.row},${c.col}): ${c.state} ${c.content ? JSON.stringify(c.content) : ''}`, 'log-info');617 }618 });619}620 621async function doMoveViewport() {622 const row = parseInt(prompt('Viewport center row:', '0'));623 const col = parseInt(prompt('Viewport center col:', '0'));624 if (isNaN(row) || isNaN(col)) return;625 log(`Moving viewport to (${row},${col})...`, 'log-info');626 const data = await api('POST', '/move_viewport', { row, col });627 const result = processResponse(data);628 if (result && result.error) log(result.error, 'log-danger');629 else log(`Viewport moved to (${row},${col})`, 'log-info');630}631 632async function doRecall() {633 const data = await api('GET', '/recall');634 if (data.error) { log(data.error, 'log-danger'); return; }635 const sigs = data.discovered_signals || [];636 const mems = data.memory_events || [];637 log(`Recall: ${sigs.length} signals, ${mems.length} memory events`, 'log-info');638 sigs.forEach(s => log(` Signal (${s.row},${s.col}): ${s.type} = ${JSON.stringify(s.value)}`, 'log-info'));639 mems.slice(-5).forEach(m => log(` Memory: step ${m.step} ${m.event} (${m.row},${m.col})`, 'log-info'));640}641 642async function doGetStatus() {643 const data = await api('GET', '/status');644 if (data.error) { log(data.error, 'log-danger'); return; }645 updateStats(data);646 log(`Step ${data.step_count}/${data.max_steps} | Flags: ${data.flags_placed}/${data.flags_remaining + data.flags_placed} | Hits: ${data.hazard_hits} | ${data.game_over ? 'GAME OVER' : 'Active'} | Win: ${data.win_condition}`, 'log-info');647}648 649async function doSubmit() {650 if (!confirm(`Submit solution with ${flaggedCells.length} flagged cells?`)) return;651 log('Submitting solution...', 'log-warn');652 const data = await api('POST', '/submit', {653 flagged_positions: flaggedCells,654 safe_positions: [],655 });656 const result = processResponse(data);657 if (!result) return;658 if (result.error) { log(result.error, 'log-danger'); return; }659 const won = result.correct === true;660 const prec = ((result.precision || 0) * 100).toFixed(1);661 const rec = ((result.recall || 0) * 100).toFixed(1);662 log(`${won ? 'CORRECT!' : 'INCORRECT'} | Precision: ${prec}% | Recall: ${rec}% | Found: ${result.hazards_found||0}/${result.hazards_total||'?'}`, won ? 'log-success' : 'log-danger');663 showGameOver(won, won ? 'YOU WIN!' : `INCORRECT — ${prec}% precision, ${rec}% recall`);664}665 666initScenarios();667</script>668</body>669</html>670 