Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Phantom Grid — Visual Memory Game</title> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;800&family=Orbitron:wght@500;700;900&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0d0d1a; | |
| --panel: #141428; | |
| --border: #2a2a50; | |
| --accent: #7b61ff; | |
| --accent-glow: rgba(123,97,255,0.3); | |
| --danger: #ff4d4d; | |
| --success: #4dff88; | |
| --warning: #ffb84d; | |
| --text: #e0e0f0; | |
| --text-dim: #8888aa; | |
| --text-bright: #ffffff; | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'JetBrains Mono', monospace; | |
| min-height: 100vh; | |
| } | |
| .header { | |
| text-align: center; | |
| padding: 24px 16px 16px; | |
| border-bottom: 1px solid var(--border); | |
| background: linear-gradient(180deg, #12122a 0%, var(--bg) 100%); | |
| } | |
| .header h1 { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 900; | |
| font-size: 28px; | |
| letter-spacing: 3px; | |
| background: linear-gradient(135deg, var(--accent), #ff61a6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 4px; | |
| } | |
| .header p { color: var(--text-dim); font-size: 12px; } | |
| .layout { | |
| display: flex; | |
| gap: 16px; | |
| padding: 16px; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| align-items: flex-start; | |
| justify-content: center; | |
| } | |
| .board-panel { | |
| flex: 0 0 auto; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 16px; | |
| min-height: 420px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .board-wrap { position: relative; cursor: crosshair; } | |
| .board-wrap svg { display: block; } | |
| .click-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .click-cell { | |
| position: absolute; | |
| border: 2px solid transparent; | |
| transition: border-color 0.15s; | |
| cursor: pointer; | |
| } | |
| .click-cell:hover { border-color: var(--accent); } | |
| .click-cell.selected { border-color: var(--warning); background: rgba(255,184,77,0.15); } | |
| .selected-label { | |
| position: absolute; | |
| bottom: -22px; | |
| left: 0; | |
| right: 0; | |
| text-align: center; | |
| font-size: 11px; | |
| color: var(--warning); | |
| font-weight: 600; | |
| } | |
| .controls { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| min-width: 300px; | |
| max-width: 380px; | |
| } | |
| .card { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| padding: 14px; | |
| } | |
| .card h3 { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| margin-bottom: 10px; | |
| } | |
| .scenario-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 6px; | |
| } | |
| .scenario-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| padding: 8px 6px; | |
| border-radius: 6px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-align: left; | |
| } | |
| .scenario-btn:hover { border-color: var(--accent); background: var(--accent-glow); } | |
| .scenario-btn.active { border-color: var(--accent); background: var(--accent-glow); color: var(--text-bright); } | |
| .scenario-btn:disabled { opacity: 0.5; cursor: wait; } | |
| .action-row { | |
| display: flex; | |
| gap: 6px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| padding: 8px 14px; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-weight: 600; | |
| white-space: nowrap; | |
| } | |
| .btn:disabled { opacity: 0.4; cursor: not-allowed; } | |
| .btn-reveal { background: #1a3a5c; color: #7bc8ff; border-color: #2a5a8c; } | |
| .btn-reveal:hover:not(:disabled) { background: #2a4a6c; } | |
| .btn-flag { background: #5c1a1a; color: #ff7b7b; border-color: #8c2a2a; } | |
| .btn-flag:hover:not(:disabled) { background: #6c2a2a; } | |
| .btn-inspect { background: #1a3a2a; color: #7bffaa; border-color: #2a5a3a; } | |
| .btn-inspect:hover:not(:disabled) { background: #2a4a3a; } | |
| .btn-viewport { background: #3a2a1a; color: #ffcc7b; border-color: #5a4a2a; } | |
| .btn-viewport:hover:not(:disabled) { background: #4a3a2a; } | |
| .btn-submit { | |
| background: var(--accent); | |
| color: white; | |
| border-color: var(--accent); | |
| width: 100%; | |
| padding: 10px; | |
| font-size: 13px; | |
| letter-spacing: 1px; | |
| } | |
| .btn-submit:hover:not(:disabled) { filter: brightness(1.2); } | |
| .btn-secondary { background: transparent; color: var(--text-dim); border-color: var(--border); } | |
| .btn-secondary:hover:not(:disabled) { color: var(--text); border-color: var(--text-dim); } | |
| .status-bar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; } | |
| .stat { text-align: center; padding: 8px 4px; background: var(--bg); border-radius: 6px; } | |
| .stat-val { font-family: 'Orbitron', sans-serif; font-size: 18px; font-weight: 700; color: var(--text-bright); } | |
| .stat-label { font-size: 9px; color: var(--text-dim); margin-top: 2px; } | |
| .mode-toggle { | |
| display: flex; | |
| gap: 4px; | |
| margin-bottom: 8px; | |
| } | |
| .mode-btn { | |
| flex: 1; | |
| padding: 8px 4px; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| cursor: pointer; | |
| text-align: center; | |
| transition: all 0.2s; | |
| background: transparent; | |
| color: var(--text-dim); | |
| } | |
| .mode-btn.active-reveal { border-color: #2a5a8c; background: #1a3a5c; color: #7bc8ff; } | |
| .mode-btn.active-flag { border-color: #8c2a2a; background: #5c1a1a; color: #ff7b7b; } | |
| .help-text { font-size: 10px; color: var(--text-dim); line-height: 1.5; margin-top: 6px; } | |
| #log { | |
| max-height: 200px; | |
| overflow-y: auto; | |
| font-size: 11px; | |
| line-height: 1.6; | |
| color: var(--text-dim); | |
| background: var(--bg); | |
| border-radius: 6px; | |
| padding: 8px; | |
| } | |
| .log-entry { border-bottom: 1px solid #1a1a30; padding: 2px 0; } | |
| .log-success { color: var(--success) ; } | |
| .log-danger { color: var(--danger) ; } | |
| .log-warn { color: var(--warning) ; } | |
| .log-info { color: var(--accent) ; } | |
| .empty-board { | |
| color: var(--text-dim); | |
| font-size: 14px; | |
| text-align: center; | |
| padding: 80px 20px; | |
| line-height: 2; | |
| } | |
| .flagged-list { | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| background: var(--bg); | |
| border-radius: 6px; | |
| padding: 8px; | |
| min-height: 28px; | |
| word-break: break-all; | |
| } | |
| .game-over-banner { | |
| text-align: center; | |
| padding: 12px; | |
| border-radius: 8px; | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 14px; | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.7; } } | |
| .win { background: rgba(77,255,136,0.15); color: var(--success); border: 1px solid var(--success); } | |
| .lose { background: rgba(255,77,77,0.15); color: var(--danger); border: 1px solid var(--danger); } | |
| .scenario-info { | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 12px 14px; | |
| margin-top: 10px; | |
| font-size: 12px; | |
| line-height: 1.6; | |
| color: var(--text); | |
| max-width: 540px; | |
| width: 100%; | |
| } | |
| .scenario-info .info-title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 10px; | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| color: var(--success); | |
| margin-bottom: 6px; | |
| } | |
| .scenario-info .info-goal { | |
| color: var(--warning); | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| font-size: 11px; | |
| } | |
| .scenario-info .info-text { | |
| color: var(--text-dim); | |
| font-size: 11px; | |
| } | |
| .legend { | |
| display: flex; gap: 12px; flex-wrap: wrap; | |
| justify-content: center; margin-top: 10px; font-size: 10px; | |
| } | |
| .legend-item { display: flex; align-items: center; gap: 4px; } | |
| .legend-swatch { | |
| width: 14px; height: 14px; border-radius: 3px; border: 1px solid #3d3d5a; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <h1>PHANTOM GRID</h1> | |
| <p>Visual Memory Gym — Click cells to reveal or flag. Play server on port 8001.</p> | |
| </div> | |
| <div class="layout"> | |
| <div class="board-panel"> | |
| <div id="board-container"> | |
| <div class="empty-board"> | |
| Select a scenario on the right to begin.<br><br> | |
| <b>How to play:</b><br> | |
| 1. Pick a scenario<br> | |
| 2. Click cells on the board to reveal or flag them<br> | |
| 3. Use signals (numbers) to deduce hazard locations<br> | |
| 4. Flag all hazards, then submit your solution | |
| </div> | |
| </div> | |
| <div class="scenario-info" id="scenario-info" style="display:none"> | |
| <div class="info-title">HOW TO PLAY</div> | |
| <div class="info-goal" id="info-goal"></div> | |
| <div class="info-text" id="info-text"></div> | |
| </div> | |
| <div class="legend" id="legend" style="display:none"> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#2d2d4a"></div> Hidden</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#d0e8ff"></div> Signal</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#e8e8f0"></div> Empty</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#ff4d4d"></div> Hazard</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#ff6b35"></div> Flagged</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#ffd700"></div> Key</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#c8b8e8"></div> Decoy</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#50fa7b"></div> Goal</div> | |
| <div class="legend-item"><div class="legend-swatch" style="background:#111122"></div> Fog</div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <div class="card"> | |
| <h3>Scenario</h3> | |
| <div class="scenario-grid" id="scenario-grid"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>Status</h3> | |
| <div class="status-bar"> | |
| <div class="stat"><div class="stat-val" id="stat-steps">—</div><div class="stat-label">STEPS</div></div> | |
| <div class="stat"><div class="stat-val" id="stat-max">—</div><div class="stat-label">MAX</div></div> | |
| <div class="stat"><div class="stat-val" id="stat-flags">—</div><div class="stat-label">FLAGS</div></div> | |
| <div class="stat"><div class="stat-val" id="stat-revealed">—</div><div class="stat-label">REVEALED</div></div> | |
| </div> | |
| <div id="game-over-slot" style="margin-top:8px"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>Click Mode</h3> | |
| <div class="mode-toggle"> | |
| <button class="mode-btn active-reveal" id="mode-reveal" onclick="setMode('reveal')"> | |
| Reveal | |
| </button> | |
| <button class="mode-btn" id="mode-flag" onclick="setMode('flag')"> | |
| Flag Hazard | |
| </button> | |
| </div> | |
| <p class="help-text" id="mode-help">Click any dark (hidden) cell on the board to reveal it.</p> | |
| </div> | |
| <div class="card"> | |
| <h3>Tools</h3> | |
| <div style="display:flex;flex-direction:column;gap:8px"> | |
| <div class="action-row"> | |
| <button class="btn btn-inspect" onclick="doInspect()">Inspect Region</button> | |
| <button class="btn btn-viewport" onclick="doMoveViewport()">Move Viewport</button> | |
| </div> | |
| <div class="action-row"> | |
| <button class="btn btn-secondary" onclick="doRecall()">Recall Log</button> | |
| <button class="btn btn-secondary" onclick="doGetStatus()">Get Status</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>Flagged Cells <span id="flag-count" style="color:var(--text-dim)">(0)</span></h3> | |
| <div class="flagged-list" id="flagged-list">No cells flagged yet</div> | |
| </div> | |
| <div class="card"> | |
| <button class="btn btn-submit" onclick="doSubmit()">SUBMIT SOLUTION</button> | |
| </div> | |
| <div class="card"> | |
| <h3>Game Log</h3> | |
| <div id="log"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API = 'http://localhost:8001'; | |
| let clickMode = 'reveal'; | |
| let boardWidth = 0; | |
| let boardHeight = 0; | |
| let flaggedCells = []; | |
| let selectedRow = -1; | |
| let selectedCol = -1; | |
| let gameOver = false; | |
| const CELL_SIZE = 48; | |
| const PADDING = 24; | |
| const SCENARIOS = [ | |
| {id:'flash_fade_minefield_7x7', label:'Flash Fade 7x7', desc:'Pattern memory'}, | |
| {id:'directional_trap_8x8', label:'Directional Trap 8x8', desc:'1 hit = fatal'}, | |
| {id:'partial_intel_9x9', label:'Partial Intel 9x9', desc:'Incomplete signals'}, | |
| {id:'delayed_recall_keys_8x8', label:'Delayed Recall 8x8', desc:'Collect 5 keys'}, | |
| {id:'ambiguous_cluster_10x10', label:'Ambiguous Cluster 10x10', desc:'Range signals'}, | |
| {id:'decoy_minefield_8x10', label:'Decoy Minefield 8x10', desc:'4 keys, 8 decoys'}, | |
| {id:'fog_labyrinth_10x10', label:'Fog Labyrinth 10x10', desc:'Viewport radius 2'}, | |
| {id:'fog_key_hunt_8x8', label:'Fog Key Hunt 8x8', desc:'Tiny viewport'}, | |
| {id:'cascading_deduction_11x11', label:'Cascading 11x11', desc:'25 hazards'}, | |
| {id:'safe_zone_identification_9x9', label:'Safe Zone ID 9x9', desc:'Find safe cells'}, | |
| ]; | |
| function initScenarios() { | |
| const grid = document.getElementById('scenario-grid'); | |
| SCENARIOS.forEach(s => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'scenario-btn'; | |
| btn.innerHTML = `${s.label}<br><span style="color:var(--text-dim);font-size:9px">${s.desc}</span>`; | |
| btn.dataset.id = s.id; | |
| btn.onclick = () => loadScenario(s.id); | |
| grid.appendChild(btn); | |
| }); | |
| } | |
| function setMode(mode) { | |
| clickMode = mode; | |
| document.getElementById('mode-reveal').className = 'mode-btn' + (mode === 'reveal' ? ' active-reveal' : ''); | |
| document.getElementById('mode-flag').className = 'mode-btn' + (mode === 'flag' ? ' active-flag' : ''); | |
| document.getElementById('mode-help').textContent = mode === 'reveal' | |
| ? 'Click any dark (hidden) cell on the board to reveal it.' | |
| : 'Click any dark (hidden) cell to flag it as a hazard.'; | |
| } | |
| function log(msg, cls) { | |
| const el = document.getElementById('log'); | |
| const div = document.createElement('div'); | |
| div.className = 'log-entry ' + (cls || ''); | |
| div.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; | |
| el.prepend(div); | |
| } | |
| function updateBoard(svgText) { | |
| if (!svgText) return; | |
| const container = document.getElementById('board-container'); | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'board-wrap'; | |
| wrap.innerHTML = svgText; | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'click-overlay'; | |
| for (let r = 0; r < boardHeight; r++) { | |
| for (let c = 0; c < boardWidth; c++) { | |
| const cell = document.createElement('div'); | |
| cell.className = 'click-cell'; | |
| cell.setAttribute('role', 'button'); | |
| cell.setAttribute('aria-label', `cell ${r} ${c}`); | |
| cell.setAttribute('tabindex', '0'); | |
| cell.style.left = (PADDING + c * CELL_SIZE) + 'px'; | |
| cell.style.top = (PADDING + r * CELL_SIZE) + 'px'; | |
| cell.style.width = CELL_SIZE + 'px'; | |
| cell.style.height = CELL_SIZE + 'px'; | |
| cell.dataset.row = r; | |
| cell.dataset.col = c; | |
| cell.title = `(${r}, ${c})`; | |
| cell.onclick = () => onCellClick(r, c); | |
| overlay.appendChild(cell); | |
| } | |
| } | |
| wrap.appendChild(overlay); | |
| container.innerHTML = ''; | |
| container.appendChild(wrap); | |
| document.getElementById('legend').style.display = 'flex'; | |
| } | |
| function updateStats(status) { | |
| if (!status) return; | |
| document.getElementById('stat-steps').textContent = status.step_count ?? '—'; | |
| document.getElementById('stat-max').textContent = status.max_steps ?? '—'; | |
| document.getElementById('stat-flags').textContent = status.flags_remaining ?? '—'; | |
| document.getElementById('stat-revealed').textContent = status.cells_revealed ?? '—'; | |
| gameOver = !!status.game_over; | |
| } | |
| function updateFlaggedList() { | |
| const el = document.getElementById('flagged-list'); | |
| document.getElementById('flag-count').textContent = `(${flaggedCells.length})`; | |
| if (flaggedCells.length === 0) { el.textContent = 'No cells flagged yet'; return; } | |
| el.textContent = flaggedCells.map(c => `[${c[0]},${c[1]}]`).join(' '); | |
| } | |
| function showGameOver(won, msg) { | |
| document.getElementById('game-over-slot').innerHTML = | |
| `<div class="game-over-banner ${won ? 'win' : 'lose'}">${msg}</div>`; | |
| } | |
| async function api(method, path, body) { | |
| try { | |
| const opts = { method, headers: {'Content-Type': 'application/json'} }; | |
| if (body !== undefined) opts.body = JSON.stringify(body); | |
| const resp = await fetch(API + path, opts); | |
| return await resp.json(); | |
| } catch (e) { | |
| log('Connection error: ' + e.message + ' — is play_server.py running on port 8001?', 'log-danger'); | |
| return { error: e.message }; | |
| } | |
| } | |
| function processResponse(data) { | |
| if (data.error) { log(data.error, 'log-danger'); return; } | |
| if (data.board && data.board.svg) { | |
| updateBoard(data.board.svg); | |
| if (data.board.metadata) { | |
| document.getElementById('stat-revealed').textContent = data.board.metadata.cell_counts?.revealed ?? '—'; | |
| } | |
| } | |
| if (data.status) updateStats(data.status); | |
| return data.action_result; | |
| } | |
| async function loadScenario(id) { | |
| document.querySelectorAll('.scenario-btn').forEach(b => { b.classList.remove('active'); b.disabled = true; }); | |
| const btn = document.querySelector(`[data-id='${id}']`); | |
| if (btn) btn.classList.add('active'); | |
| document.getElementById('game-over-slot').innerHTML = ''; | |
| flaggedCells = []; | |
| updateFlaggedList(); | |
| document.getElementById('log').innerHTML = ''; | |
| gameOver = false; | |
| log(`Loading scenario: ${id}...`, 'log-info'); | |
| const data = await api('POST', '/load', { scenario_id: id }); | |
| document.querySelectorAll('.scenario-btn').forEach(b => b.disabled = false); | |
| if (data.error) { log(data.error, 'log-danger'); return; } | |
| boardWidth = data.status?.board_size ? parseInt(data.status.board_size.split('x')[0]) : 0; | |
| boardHeight = data.status?.board_size ? parseInt(data.status.board_size.split('x')[1]) : 0; | |
| processResponse(data); | |
| const winLabels = { | |
| 'flag_all_hazards': 'Flag all hazards, then submit.', | |
| 'collect_keys': 'Find and reveal all keys to win.', | |
| 'identify_safe_cells': 'Identify all safe (non-hazard) cells, then submit.', | |
| 'reach_goal': 'Reach the goal cell to win.', | |
| }; | |
| const wc = data.status?.win_condition || ''; | |
| const goalText = winLabels[wc] || `Win condition: ${wc}`; | |
| const howTo = data.how_to_play || ''; | |
| const infoEl = document.getElementById('scenario-info'); | |
| if (howTo) { | |
| document.getElementById('info-goal').textContent = goalText; | |
| document.getElementById('info-text').textContent = howTo; | |
| infoEl.style.display = 'block'; | |
| } else { | |
| infoEl.style.display = 'none'; | |
| } | |
| log(`Loaded ${boardWidth}x${boardHeight} board | Type: ${data.status?.scenario_type} | Win: ${data.status?.win_condition} | Max steps: ${data.status?.max_steps}`, 'log-success'); | |
| } | |
| async function onCellClick(row, col) { | |
| if (gameOver) { log('Game is over. Load a new scenario to play again.', 'log-warn'); return; } | |
| if (boardWidth === 0) { log('Load a scenario first.', 'log-warn'); return; } | |
| if (clickMode === 'reveal') { | |
| log(`Revealing (${row}, ${col})...`); | |
| const data = await api('POST', '/reveal', { row, col }); | |
| const result = processResponse(data); | |
| if (!result) return; | |
| if (result.error) { log(result.error, 'log-danger'); return; } | |
| const t = result.type || ''; | |
| if (t === 'hazard') log(`HAZARD at (${row},${col})!${result.game_over ? ' GAME OVER!' : ''}`, 'log-danger'); | |
| else if (t === 'key') log(`KEY found at (${row},${col})!`, 'log-success'); | |
| else if (t === 'signal') log(`Signal at (${row},${col}): ${JSON.stringify(result.value)}`, 'log-info'); | |
| else if (t === 'decoy') log(`Decoy at (${row},${col})`, 'log-warn'); | |
| else if (t === 'goal') log(`GOAL reached at (${row},${col})!`, 'log-success'); | |
| else if (t === 'empty') log(`Empty cell at (${row},${col})`, ''); | |
| else log(`Cell (${row},${col}): ${JSON.stringify(result).slice(0,120)}`, 'log-info'); | |
| if (result.game_over && result.message) { | |
| showGameOver(!!result.message?.includes('win') || !!result.message?.includes('Win'), result.message); | |
| } | |
| } else { | |
| const alreadyFlagged = flaggedCells.some(f => f[0] === row && f[1] === col); | |
| if (alreadyFlagged) { | |
| log(`Unflagging (${row}, ${col})...`); | |
| const data = await api('POST', '/unflag', { row, col }); | |
| const result = processResponse(data); | |
| if (!result) return; | |
| if (result.error) { log(result.error, 'log-danger'); return; } | |
| flaggedCells = flaggedCells.filter(f => !(f[0] === row && f[1] === col)); | |
| updateFlaggedList(); | |
| log(`Unflagged (${row},${col})`, 'log-warn'); | |
| } else { | |
| log(`Flagging (${row}, ${col})...`); | |
| const data = await api('POST', '/flag', { row, col }); | |
| const result = processResponse(data); | |
| if (!result) return; | |
| if (result.error) { log(result.error, 'log-danger'); return; } | |
| flaggedCells.push([row, col]); | |
| updateFlaggedList(); | |
| log(`Flagged (${row},${col}) as hazard | ${result.flags_remaining} flags left`, 'log-warn'); | |
| if (result.game_over && result.message) { | |
| showGameOver(true, result.message); | |
| } | |
| } | |
| } | |
| } | |
| async function doInspect() { | |
| const row = parseInt(prompt('Center row:', '0')); | |
| const col = parseInt(prompt('Center col:', '0')); | |
| const radius = parseInt(prompt('Radius (1-3):', '1')); | |
| if (isNaN(row) || isNaN(col)) return; | |
| log(`Inspecting region around (${row},${col}) r=${radius}...`, 'log-info'); | |
| const data = await api('POST', '/inspect', { center_row: row, center_col: col, radius: radius || 1 }); | |
| const result = processResponse(data); | |
| if (!result) return; | |
| if (result.error) { log(result.error, 'log-danger'); return; } | |
| const cells = result.cells || []; | |
| log(`Inspected ${cells.length} cells`, 'log-info'); | |
| cells.forEach(c => { | |
| if (c.state !== 'hidden' && c.state !== 'fog') { | |
| log(` (${c.row},${c.col}): ${c.state} ${c.content ? JSON.stringify(c.content) : ''}`, 'log-info'); | |
| } | |
| }); | |
| } | |
| async function doMoveViewport() { | |
| const row = parseInt(prompt('Viewport center row:', '0')); | |
| const col = parseInt(prompt('Viewport center col:', '0')); | |
| if (isNaN(row) || isNaN(col)) return; | |
| log(`Moving viewport to (${row},${col})...`, 'log-info'); | |
| const data = await api('POST', '/move_viewport', { row, col }); | |
| const result = processResponse(data); | |
| if (result && result.error) log(result.error, 'log-danger'); | |
| else log(`Viewport moved to (${row},${col})`, 'log-info'); | |
| } | |
| async function doRecall() { | |
| const data = await api('GET', '/recall'); | |
| if (data.error) { log(data.error, 'log-danger'); return; } | |
| const sigs = data.discovered_signals || []; | |
| const mems = data.memory_events || []; | |
| log(`Recall: ${sigs.length} signals, ${mems.length} memory events`, 'log-info'); | |
| sigs.forEach(s => log(` Signal (${s.row},${s.col}): ${s.type} = ${JSON.stringify(s.value)}`, 'log-info')); | |
| mems.slice(-5).forEach(m => log(` Memory: step ${m.step} ${m.event} (${m.row},${m.col})`, 'log-info')); | |
| } | |
| async function doGetStatus() { | |
| const data = await api('GET', '/status'); | |
| if (data.error) { log(data.error, 'log-danger'); return; } | |
| updateStats(data); | |
| 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'); | |
| } | |
| async function doSubmit() { | |
| if (!confirm(`Submit solution with ${flaggedCells.length} flagged cells?`)) return; | |
| log('Submitting solution...', 'log-warn'); | |
| const data = await api('POST', '/submit', { | |
| flagged_positions: flaggedCells, | |
| safe_positions: [], | |
| }); | |
| const result = processResponse(data); | |
| if (!result) return; | |
| if (result.error) { log(result.error, 'log-danger'); return; } | |
| const won = result.correct === true; | |
| const prec = ((result.precision || 0) * 100).toFixed(1); | |
| const rec = ((result.recall || 0) * 100).toFixed(1); | |
| log(`${won ? 'CORRECT!' : 'INCORRECT'} | Precision: ${prec}% | Recall: ${rec}% | Found: ${result.hazards_found||0}/${result.hazards_total||'?'}`, won ? 'log-success' : 'log-danger'); | |
| showGameOver(won, won ? 'YOU WIN!' : `INCORRECT — ${prec}% precision, ${rec}% recall`); | |
| } | |
| initScenarios(); | |
| </script> | |
| </body> | |
| </html> | |