Spaces:
Sleeping
Sleeping
rajan-personal
Add initial HTML structure and styling for Minesweeper OpenEnv game interface
2ce7137 | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Minesweeper OpenEnv - Live Test</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0d1117; color: #c9d1d9; padding: 20px; } | |
| h1 { color: #58a6ff; margin-bottom: 8px; font-size: 1.4em; } | |
| .subtitle { color: #8b949e; margin-bottom: 20px; font-size: 0.9em; } | |
| .controls { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; align-items: center; } | |
| button { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 0.85em; } | |
| button:hover { background: #30363d; border-color: #58a6ff; } | |
| button.active { background: #1f6feb; border-color: #58a6ff; color: #fff; } | |
| button.danger { border-color: #f85149; } | |
| button.danger:hover { background: #da3633; color: #fff; } | |
| select { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 8px 12px; border-radius: 6px; font-family: inherit; } | |
| .grid-container { display: inline-block; background: #161b22; border: 2px solid #30363d; border-radius: 8px; padding: 12px; margin-bottom: 16px; } | |
| .grid { display: inline-grid; gap: 2px; } | |
| .cell { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1em; border-radius: 4px; cursor: pointer; transition: all 0.1s; user-select: none; } | |
| .cell.hidden { background: #30363d; } | |
| .cell.hidden:hover { background: #484f58; } | |
| .cell.revealed { background: #161b22; border: 1px solid #21262d; } | |
| .cell.flagged { background: #6e40154d; border: 1px solid #d29922; } | |
| .cell.mine { background: #f851491a; border: 1px solid #f85149; color: #f85149; } | |
| .cell.n0 { color: transparent; } | |
| .cell.n1 { color: #58a6ff; } | |
| .cell.n2 { color: #3fb950; } | |
| .cell.n3 { color: #f85149; } | |
| .cell.n4 { color: #bc8cff; } | |
| .cell.n5 { color: #d29922; } | |
| .cell.n6 { color: #39d2c0; } | |
| .cell.n7 { color: #c9d1d9; } | |
| .cell.n8 { color: #8b949e; } | |
| .stats { display: flex; gap: 20px; margin-bottom: 16px; flex-wrap: wrap; } | |
| .stat { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px 16px; } | |
| .stat-label { color: #8b949e; font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.5px; } | |
| .stat-value { color: #58a6ff; font-size: 1.3em; font-weight: bold; margin-top: 2px; } | |
| .stat-value.won { color: #3fb950; } | |
| .stat-value.lost { color: #f85149; } | |
| .log { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px; max-height: 250px; overflow-y: auto; font-size: 0.8em; line-height: 1.6; } | |
| .log-entry { padding: 2px 0; border-bottom: 1px solid #21262d; } | |
| .log-entry.start { color: #3fb950; } | |
| .log-entry.step { color: #8b949e; } | |
| .log-entry.end { color: #d29922; font-weight: bold; } | |
| .log-entry.error { color: #f85149; } | |
| .mode-indicator { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 4px 10px; border-radius: 4px; font-size: 0.8em; margin-left: 10px; } | |
| .banner { padding: 12px 16px; border-radius: 6px; margin-bottom: 16px; font-weight: bold; text-align: center; font-size: 1.1em; } | |
| .banner.won { background: #3fb95020; border: 1px solid #3fb950; color: #3fb950; } | |
| .banner.lost { background: #f8514920; border: 1px solid #f85149; color: #f85149; } | |
| .url-input { background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; padding: 8px 12px; border-radius: 6px; font-family: inherit; width: 350px; font-size: 0.85em; } | |
| .section { margin-bottom: 12px; } | |
| .section-title { color: #8b949e; font-size: 0.75em; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; } | |
| .coord-label { position: absolute; font-size: 0.6em; color: #484f58; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Minesweeper OpenEnv <span class="mode-indicator" id="modeLabel">REVEAL</span></h1> | |
| <p class="subtitle">Interactive test client — click cells to play, or let the AI solve it</p> | |
| <div class="controls"> | |
| <input class="url-input" id="baseUrl" value="https://bittu15388-minesweeper-openenv.hf.space" placeholder="Environment URL"> | |
| <select id="taskSelect"> | |
| <option value="easy">Easy (5x5, 3 mines)</option> | |
| <option value="medium">Medium (8x8, 10 mines)</option> | |
| <option value="hard">Hard (10x10, 20 mines)</option> | |
| </select> | |
| <button onclick="resetGame()" id="resetBtn">New Game</button> | |
| <button onclick="toggleMode()" id="modeBtn">Mode: Reveal</button> | |
| <button onclick="healthCheck()">Health Check</button> | |
| </div> | |
| <div id="banner"></div> | |
| <div class="stats" id="statsBar"> | |
| <div class="stat"><div class="stat-label">Score</div><div class="stat-value" id="score">0.00</div></div> | |
| <div class="stat"><div class="stat-label">Revealed</div><div class="stat-value" id="revealed">0/0</div></div> | |
| <div class="stat"><div class="stat-label">Flags</div><div class="stat-value" id="flags">0</div></div> | |
| <div class="stat"><div class="stat-label">Mines</div><div class="stat-value" id="mines">0</div></div> | |
| <div class="stat"><div class="stat-label">Steps</div><div class="stat-value" id="steps">0</div></div> | |
| <div class="stat"><div class="stat-label">Status</div><div class="stat-value" id="status">Ready</div></div> | |
| </div> | |
| <div class="grid-container"> | |
| <div class="grid" id="grid"></div> | |
| </div> | |
| <div class="section"> | |
| <div class="section-title">Action Log</div> | |
| <div class="log" id="log"></div> | |
| </div> | |
| <script> | |
| let mode = 'reveal'; | |
| let gameState = null; | |
| let stepCount = 0; | |
| let gameOver = false; | |
| function getUrl() { return document.getElementById('baseUrl').value.replace(/\/$/, ''); } | |
| function log(msg, cls = 'step') { | |
| const el = document.getElementById('log'); | |
| const entry = document.createElement('div'); | |
| entry.className = 'log-entry ' + cls; | |
| entry.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; | |
| el.prepend(entry); | |
| } | |
| function updateStats(obs) { | |
| document.getElementById('score').textContent = (obs.score || 0).toFixed(4); | |
| document.getElementById('score').className = 'stat-value' + (obs.won ? ' won' : obs.game_over ? ' lost' : ''); | |
| document.getElementById('revealed').textContent = `${obs.cells_revealed}/${obs.total_safe_cells}`; | |
| document.getElementById('flags').textContent = obs.flags_placed; | |
| document.getElementById('mines').textContent = obs.mines_total; | |
| document.getElementById('steps').textContent = stepCount; | |
| const statusEl = document.getElementById('status'); | |
| if (obs.won) { statusEl.textContent = 'WON!'; statusEl.className = 'stat-value won'; } | |
| else if (obs.game_over) { statusEl.textContent = 'GAME OVER'; statusEl.className = 'stat-value lost'; } | |
| else { statusEl.textContent = 'Playing'; statusEl.className = 'stat-value'; } | |
| const banner = document.getElementById('banner'); | |
| if (obs.won) { banner.innerHTML = '<div class="banner won">YOU WIN! All safe cells revealed. Score: ' + obs.score.toFixed(4) + '</div>'; } | |
| else if (obs.game_over) { banner.innerHTML = '<div class="banner lost">BOOM! Hit a mine. Score: ' + obs.score.toFixed(4) + '</div>'; } | |
| else { banner.innerHTML = ''; } | |
| } | |
| function renderGrid(obs) { | |
| const grid = document.getElementById('grid'); | |
| grid.innerHTML = ''; | |
| grid.style.gridTemplateColumns = `repeat(${obs.width}, 40px)`; | |
| for (let y = 0; y < obs.height; y++) { | |
| for (let x = 0; x < obs.width; x++) { | |
| const cell = document.createElement('div'); | |
| const val = obs.grid[y][x]; | |
| cell.className = 'cell'; | |
| if (val === '?') { | |
| cell.classList.add('hidden'); | |
| cell.textContent = ''; | |
| cell.title = `(${x}, ${y}) hidden`; | |
| } else if (val === 'F') { | |
| cell.classList.add('flagged'); | |
| cell.textContent = '\u2691'; | |
| cell.title = `(${x}, ${y}) flagged`; | |
| } else if (val === 'M') { | |
| cell.classList.add('mine'); | |
| cell.textContent = '\uD83D\uDCA3'; | |
| cell.title = `(${x}, ${y}) MINE`; | |
| } else { | |
| cell.classList.add('revealed', 'n' + val); | |
| cell.textContent = val === '0' ? '' : val; | |
| cell.title = `(${x}, ${y}) = ${val}`; | |
| } | |
| if (!gameOver && (val === '?' || val === 'F')) { | |
| cell.onclick = () => doStep(x, y); | |
| } | |
| grid.appendChild(cell); | |
| } | |
| } | |
| gameState = obs; | |
| } | |
| async function healthCheck() { | |
| try { | |
| const r = await fetch(getUrl() + '/health'); | |
| const d = await r.json(); | |
| log(`Health: ${JSON.stringify(d)}`, 'start'); | |
| const r2 = await fetch(getUrl() + '/tasks'); | |
| const t = await r2.json(); | |
| log(`Tasks: ${Object.keys(t.tasks).join(', ')}`, 'start'); | |
| } catch (e) { | |
| log(`Health check failed: ${e.message}`, 'error'); | |
| } | |
| } | |
| async function resetGame() { | |
| const task = document.getElementById('taskSelect').value; | |
| stepCount = 0; | |
| gameOver = false; | |
| document.getElementById('banner').innerHTML = ''; | |
| try { | |
| const r = await fetch(getUrl() + '/reset', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ task_id: task }) | |
| }); | |
| const d = await r.json(); | |
| renderGrid(d.observation); | |
| updateStats(d.observation); | |
| log(`[START] Task: ${task} | Grid: ${d.observation.width}x${d.observation.height} | Mines: ${d.observation.mines_total}`, 'start'); | |
| } catch (e) { | |
| log(`Reset failed: ${e.message}`, 'error'); | |
| } | |
| } | |
| async function doStep(x, y) { | |
| if (gameOver) return; | |
| const action = { action_type: mode, x, y }; | |
| try { | |
| const r = await fetch(getUrl() + '/step', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action }) | |
| }); | |
| const d = await r.json(); | |
| stepCount++; | |
| renderGrid(d.observation); | |
| updateStats(d.observation); | |
| if (d.observation.game_over || d.observation.won) gameOver = true; | |
| log(`[STEP ${stepCount}] ${mode}(${x},${y}) → score: ${d.observation.score.toFixed(4)} | revealed: ${d.observation.cells_revealed}/${d.observation.total_safe_cells}${d.observation.won ? ' | WON!' : d.observation.game_over ? ' | GAME OVER' : ''}`); | |
| } catch (e) { | |
| log(`Step failed: ${e.message}`, 'error'); | |
| } | |
| } | |
| function toggleMode() { | |
| mode = mode === 'reveal' ? 'flag' : 'reveal'; | |
| document.getElementById('modeBtn').textContent = `Mode: ${mode.charAt(0).toUpperCase() + mode.slice(1)}`; | |
| document.getElementById('modeLabel').textContent = mode.toUpperCase(); | |
| } | |
| // Auto health check on load | |
| healthCheck(); | |
| </script> | |
| </body> | |
| </html> | |