Spaces:
Sleeping
Sleeping
| """Custom web interface for the Minesweeper environment.""" | |
| MINESWEEPER_HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Minesweeper</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 20px; } | |
| h1 { margin-bottom: 10px; font-size: 24px; } | |
| .status-bar { display: flex; gap: 20px; margin-bottom: 15px; font-size: 14px; color: #aaa; } | |
| .status-bar span { background: #16213e; padding: 6px 14px; border-radius: 6px; } | |
| .status-bar .val { color: #e94560; font-weight: 600; } | |
| #board { display: inline-grid; gap: 2px; background: #16213e; padding: 6px; border-radius: 8px; margin-bottom: 15px; } | |
| .cell { width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; border-radius: 4px; cursor: pointer; user-select: none; transition: background 0.1s; } | |
| .cell.unrevealed { background: #0f3460; } | |
| .cell.unrevealed:hover { background: #1a5276; } | |
| .cell.revealed { background: #2c3e6d; cursor: default; } | |
| .cell.flag { background: #e94560; } | |
| .cell.mine { background: #c0392b; } | |
| .cell.n1 { color: #3498db; } .cell.n2 { color: #2ecc71; } .cell.n3 { color: #e74c3c; } | |
| .cell.n4 { color: #9b59b6; } .cell.n5 { color: #e67e22; } .cell.n6 { color: #1abc9c; } | |
| .cell.n7 { color: #34495e; } .cell.n8 { color: #95a5a6; } | |
| .controls { display: flex; gap: 10px; margin-bottom: 15px; align-items: center; } | |
| .btn { padding: 10px 24px; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity 0.15s; } | |
| .btn:hover { opacity: 0.85; } | |
| .btn-reset { background: #e94560; color: white; } | |
| .btn-mode { background: #0f3460; color: white; min-width: 140px; } | |
| .btn-mode.flagging { background: #e94560; } | |
| #message { font-size: 18px; font-weight: 600; min-height: 28px; margin-bottom: 10px; } | |
| .reward-display { font-size: 13px; color: #888; margin-bottom: 10px; } | |
| .won { color: #2ecc71; } | |
| .lost { color: #e74c3c; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Minesweeper</h1> | |
| <div id="message">Click Reset to start</div> | |
| <div class="reward-display" id="reward"></div> | |
| <div class="status-bar"> | |
| <span>Mines: <span class="val" id="mines">-</span></span> | |
| <span>Flags: <span class="val" id="flags">-</span></span> | |
| <span>Revealed: <span class="val" id="revealed">-</span></span> | |
| </div> | |
| <div id="board"></div> | |
| <div class="controls"> | |
| <button class="btn btn-reset" id="reset-btn">Reset</button> | |
| <button class="btn btn-mode" id="mode-btn">Mode: Reveal</button> | |
| </div> | |
| <script> | |
| let mode = 'reveal'; | |
| let gameOver = false; | |
| document.getElementById('reset-btn').addEventListener('click', doReset); | |
| document.getElementById('mode-btn').addEventListener('click', () => { | |
| mode = mode === 'reveal' ? 'flag' : 'reveal'; | |
| const btn = document.getElementById('mode-btn'); | |
| btn.textContent = 'Mode: ' + (mode === 'reveal' ? 'Reveal' : 'Flag'); | |
| btn.classList.toggle('flagging', mode === 'flag'); | |
| }); | |
| async function doReset() { | |
| gameOver = false; | |
| document.getElementById('message').textContent = 'Resetting...'; | |
| document.getElementById('message').className = ''; | |
| try { | |
| const r = await fetch('/reset', { method: 'POST', headers: {'Content-Type':'application/json'} }); | |
| const data = await r.json(); | |
| renderBoard(data); | |
| document.getElementById('message').textContent = 'Click a cell to play!'; | |
| } catch(e) { document.getElementById('message').textContent = 'Error: ' + e.message; } | |
| } | |
| async function doStep(row, col) { | |
| if (gameOver) return; | |
| try { | |
| const r = await fetch('/step', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({ action: { row, col, action_type: mode } }) | |
| }); | |
| const data = await r.json(); | |
| renderBoard(data); | |
| } catch(e) { document.getElementById('message').textContent = 'Error: ' + e.message; } | |
| } | |
| function renderBoard(data) { | |
| const obs = data.observation || {}; | |
| const board = obs.board || []; | |
| const rows = board.length; | |
| const cols = rows > 0 ? board[0].length : 0; | |
| document.getElementById('mines').textContent = obs.num_mines ?? '-'; | |
| document.getElementById('flags').textContent = obs.flags_placed ?? '-'; | |
| document.getElementById('revealed').textContent = obs.cells_revealed ?? '-'; | |
| if (data.reward !== null && data.reward !== undefined) { | |
| document.getElementById('reward').textContent = 'Last reward: ' + data.reward; | |
| } | |
| const status = obs.game_status; | |
| if (status === 'won') { | |
| document.getElementById('message').textContent = 'You won!'; | |
| document.getElementById('message').className = 'won'; | |
| gameOver = true; | |
| } else if (status === 'lost') { | |
| document.getElementById('message').textContent = 'Game over - hit a mine!'; | |
| document.getElementById('message').className = 'lost'; | |
| gameOver = true; | |
| } | |
| const el = document.getElementById('board'); | |
| el.style.gridTemplateColumns = 'repeat(' + cols + ', 52px)'; | |
| el.innerHTML = ''; | |
| for (let r = 0; r < rows; r++) { | |
| for (let c = 0; c < cols; c++) { | |
| const v = board[r][c]; | |
| const cell = document.createElement('div'); | |
| cell.className = 'cell'; | |
| if (v === -1) { | |
| cell.classList.add('unrevealed'); | |
| cell.addEventListener('click', () => doStep(r, c)); | |
| } else if (v === 'F') { | |
| cell.classList.add('flag'); | |
| cell.textContent = '\\u{1f6a9}'; | |
| cell.addEventListener('click', () => doStep(r, c)); | |
| } else if (v === '*') { | |
| cell.classList.add('mine'); | |
| cell.textContent = '\\u{1f4a3}'; | |
| } else { | |
| cell.classList.add('revealed'); | |
| if (v > 0) { cell.textContent = v; cell.classList.add('n' + v); } | |
| } | |
| el.appendChild(cell); | |
| } | |
| } | |
| } | |
| // Auto-reset on load | |
| doReset(); | |
| </script> | |
| </body> | |
| </html>""" | |