CoolFace
Apppublic

huzzle-labs/visual_memory

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
play.html670 linesDownload Raw Back to root
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