minesweeper-openenv / test.html
rajan-personal
Add initial HTML structure and styling for Minesweeper OpenEnv game interface
2ce7137
<!DOCTYPE html>
<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 &mdash; 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>