Minesweeper
app.unfazed.dev/classic-minesweeper-9x9 open (opens in new tab)classic minesweeper: 9x9 grid with 10 mines, right-click to flag, timer and mine counter, dark minimal look. build it as a single static page and deploy it on unfazed using the mcp tools, then send me the public link.
I’ll build this directly as a static single-page app and deploy it through Unfazed. I’m first loading the deployment tool contract so the service matches what Unfazed expects.
I found the Unfazed static deployment tools. I’m creating a compact index.html with all CSS and JavaScript inline, then I’ll publish it with a stable alias.
create_workspace(name: "classic-minesweeper")
{
"name": "classic-minesweeper"
}{
"created_at": "2026-06-12T04:51:09Z",
"id": "98f148f2-f5f0-45c0-afa8-9809acccd2d6",
"name": "classic-minesweeper",
"updated_at": "2026-06-12T04:51:09Z"
}write_workspace_text_file(workspace_id: "98f148f2-f5f0-45c0-afa8-9809acccd2d6", path: "index.html"…)
{
"workspace_id": "98f148f2-f5f0-45c0-afa8-9809acccd2d6",
"path": "index.html",
"text": "\u003c!doctype html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"utf-8\"\u003e\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n \u003ctitle\u003eClassic Minesweeper\u003c/title\u003e\n \u003cstyle\u003e\n :root {\n color-scheme: dark;\n --bg: #0d0f12;\n --panel: #171a1f;\n --panel-2: #1f242b;\n --cell: #242a32;\n --cell-hover: #2b333d;\n --open: #12151a;\n --line: #343b45;\n --text: #e8edf2;\n --muted: #8f9aa8;\n --accent: #38bdf8;\n --danger: #f87171;\n --flag: #fbbf24;\n --shadow: rgba(0, 0, 0, .32);\n }\n\n * { box-sizing: border-box; }\n\n html, body { height: 100%; }\n\n body {\n margin: 0;\n min-height: 100%;\n display: grid;\n place-items: center;\n background: radial-gradient(circle at 50% 0%, #181d24 0, var(--bg) 42rem);\n color: var(--text);\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n }\n\n main {\n width: min(92vw, 430px);\n padding: 20px;\n border: 1px solid #252b34;\n border-radius: 8px;\n background: rgba(23, 26, 31, .92);\n box-shadow: 0 20px 60px var(--shadow);\n }\n\n header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 14px;\n }\n\n h1 {\n margin: 0;\n font-size: 1.1rem;\n font-weight: 650;\n letter-spacing: 0;\n }\n\n .stats {\n display: flex;\n gap: 8px;\n align-items: center;\n }\n\n .stat, .reset {\n height: 36px;\n min-width: 58px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: #111419;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n }\n\n .reset {\n min-width: 42px;\n cursor: pointer;\n font-size: 1rem;\n }\n\n .reset:hover { background: #1a2028; }\n .reset:active { transform: translateY(1px); }\n\n .board {\n width: 100%;\n aspect-ratio: 1;\n display: grid;\n grid-template-columns: repeat(9, 1fr);\n gap: 4px;\n padding: 4px;\n background: #0b0d10;\n border: 1px solid var(--line);\n border-radius: 8px;\n touch-action: manipulation;\n user-select: none;\n }\n\n .cell {\n width: 100%;\n aspect-ratio: 1;\n display: grid;\n place-items: center;\n border: 1px solid #303844;\n border-radius: 5px;\n background: linear-gradient(180deg, var(--cell), #1d232b);\n color: var(--text);\n cursor: pointer;\n font: 800 clamp(.95rem, 5vw, 1.35rem) / 1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n padding: 0;\n }\n\n .cell:hover { background: var(--cell-hover); }\n .cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\n .cell.open {\n cursor: default;\n border-color: #1d2229;\n background: var(--open);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .02);\n }\n\n .cell.flagged { color: var(--flag); }\n .cell.mine { color: var(--danger); background: #2b1518; border-color: #5c252b; }\n .cell.bad-flag { color: var(--danger); background: #291d12; }\n\n .n1 { color: #60a5fa; }\n .n2 { color: #34d399; }\n .n3 { color: #fb7185; }\n .n4 { color: #a78bfa; }\n .n5 { color: #f59e0b; }\n .n6 { color: #22d3ee; }\n .n7 { color: #e5e7eb; }\n .n8 { color: #9ca3af; }\n\n .status {\n min-height: 24px;\n margin-top: 12px;\n color: var(--muted);\n text-align: center;\n font-size: .92rem;\n }\n\n @media (max-width: 420px) {\n body { place-items: start center; padding-top: 18px; }\n main { width: calc(100vw - 20px); padding: 12px; }\n header { gap: 8px; }\n h1 { font-size: 1rem; }\n .stat, .reset { height: 34px; min-width: 50px; }\n .board { gap: 3px; }\n }\n \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n \u003cmain aria-label=\"Minesweeper game\"\u003e\n \u003cheader\u003e\n \u003ch1\u003eMinesweeper\u003c/h1\u003e\n \u003cdiv class=\"stats\" aria-label=\"Game stats\"\u003e\n \u003cdiv class=\"stat\" id=\"mineCounter\" aria-label=\"Mines remaining\"\u003e010\u003c/div\u003e\n \u003cbutton class=\"reset\" id=\"reset\" type=\"button\" aria-label=\"New game\" title=\"New game\"\u003e↻\u003c/button\u003e\n \u003cdiv class=\"stat\" id=\"timer\" aria-label=\"Timer\"\u003e000\u003c/div\u003e\n \u003c/div\u003e\n \u003c/header\u003e\n \u003csection class=\"board\" id=\"board\" aria-label=\"9 by 9 minesweeper board\"\u003e\u003c/section\u003e\n \u003cdiv class=\"status\" id=\"status\" aria-live=\"polite\"\u003eLeft-click to reveal. Right-click to flag.\u003c/div\u003e\n \u003c/main\u003e\n\n \u003cscript\u003e\n (() =\u003e {\n const SIZE = 9;\n const MINES = 10;\n const boardEl = document.getElementById('board');\n const mineCounterEl = document.getElementById('mineCounter');\n const timerEl = document.getElementById('timer');\n const statusEl = document.getElementById('status');\n const resetEl = document.getElementById('reset');\n\n let cells;\n let minesPlaced;\n let gameOver;\n let revealedCount;\n let flags;\n let seconds;\n let timerId;\n\n function pad(value) {\n return String(Math.min(value, 999)).padStart(3, '0');\n }\n\n function index(row, col) {\n return row * SIZE + col;\n }\n\n function neighbors(cellIndex) {\n const row = Math.floor(cellIndex / SIZE);\n const col = cellIndex % SIZE;\n const result = [];\n for (let dr = -1; dr \u003c= 1; dr++) {\n for (let dc = -1; dc \u003c= 1; dc++) {\n if (!dr \u0026\u0026 !dc) continue;\n const nr = row + dr;\n const nc = col + dc;\n if (nr \u003e= 0 \u0026\u0026 nr \u003c SIZE \u0026\u0026 nc \u003e= 0 \u0026\u0026 nc \u003c SIZE) result.push(index(nr, nc));\n }\n }\n return result;\n }\n\n function startTimer() {\n if (timerId) return;\n timerId = setInterval(() =\u003e {\n seconds += 1;\n timerEl.textContent = pad(seconds);\n }, 1000);\n }\n\n function stopTimer() {\n clearInterval(timerId);\n timerId = null;\n }\n\n function updateMineCounter() {\n mineCounterEl.textContent = pad(MINES - flags);\n }\n\n function placeMines(firstIndex) {\n const blocked = new Set([firstIndex]);\n const pool = [];\n for (let i = 0; i \u003c SIZE * SIZE; i++) {\n if (!blocked.has(i)) pool.push(i);\n }\n for (let i = pool.length - 1; i \u003e 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [pool[i], pool[j]] = [pool[j], pool[i]];\n }\n pool.slice(0, MINES).forEach(i =\u003e cells[i].mine = true);\n cells.forEach((cell, i) =\u003e {\n cell.count = neighbors(i).filter(n =\u003e cells[n].mine).length;\n });\n minesPlaced = true;\n }\n\n function drawCell(i) {\n const cell = cells[i];\n const btn = cell.el;\n btn.className = 'cell';\n btn.textContent = '';\n btn.setAttribute('aria-label', `Cell ${Math.floor(i / SIZE) + 1}, ${i % SIZE + 1}`);\n\n if (cell.open) {\n btn.classList.add('open');\n btn.disabled = true;\n if (cell.mine) {\n btn.classList.add('mine');\n btn.textContent = '*';\n } else if (cell.count \u003e 0) {\n btn.classList.add(`n${cell.count}`);\n btn.textContent = cell.count;\n }\n return;\n }\n\n btn.disabled = false;\n if (cell.flagged) {\n btn.classList.add('flagged');\n btn.textContent = '⚑';\n }\n }\n\n function reveal(i) {\n const cell = cells[i];\n if (cell.open || cell.flagged || gameOver) return;\n if (!minesPlaced) {\n placeMines(i);\n startTimer();\n }\n\n cell.open = true;\n revealedCount += 1;\n\n if (cell.mine) {\n lose(i);\n return;\n }\n\n if (cell.count === 0) {\n neighbors(i).forEach(n =\u003e {\n if (!cells[n].open \u0026\u0026 !cells[n].flagged \u0026\u0026 !cells[n].mine) reveal(n);\n });\n }\n\n drawAll();\n checkWin();\n }\n\n function toggleFlag(i) {\n const cell = cells[i];\n if (cell.open || gameOver) return;\n cell.flagged = !cell.flagged;\n flags += cell.flagged ? 1 : -1;\n updateMineCounter();\n drawCell(i);\n }\n\n function lose(triggerIndex) {\n gameOver = true;\n stopTimer();\n cells.forEach((cell, i) =\u003e {\n if (cell.mine) cell.open = true;\n drawCell(i);\n if (cell.flagged \u0026\u0026 !cell.mine) cell.el.classList.add('bad-flag');\n });\n cells[triggerIndex].el.classList.add('mine');\n statusEl.textContent = 'Mine hit. Start a new game.';\n }\n\n function checkWin() {\n if (revealedCount !== SIZE * SIZE - MINES) return;\n gameOver = true;\n stopTimer();\n cells.forEach((cell, i) =\u003e {\n if (cell.mine \u0026\u0026 !cell.flagged) {\n cell.flagged = true;\n flags += 1;\n }\n drawCell(i);\n });\n updateMineCounter();\n statusEl.textContent = `Cleared in ${seconds} second${seconds === 1 ? '' : 's'}.`;\n }\n\n function drawAll() {\n cells.forEach((_, i) =\u003e drawCell(i));\n }\n\n function newGame() {\n stopTimer();\n cells = [];\n minesPlaced = false;\n gameOver = false;\n revealedCount = 0;\n flags = 0;\n seconds = 0;\n boardEl.replaceChildren();\n timerEl.textContent = '000';\n updateMineCounter();\n statusEl.textContent = 'Left-click to reveal. Right-click to flag.';\n\n for (let i = 0; i \u003c SIZE * SIZE; i++) {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'cell';\n btn.dataset.index = i;\n btn.addEventListener('click', () =\u003e reveal(i));\n btn.addEventListener('contextmenu', event =\u003e {\n event.preventDefault();\n toggleFlag(i);\n });\n cells.push({ el: btn, mine: false, count: 0, open: false, flagged: false });\n boardEl.append(btn);\n }\n drawAll();\n }\n\n resetEl.addEventListener('click', newGame);\n boardEl.addEventListener('contextmenu', event =\u003e event.preventDefault());\n newGame();\n })();\n \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e"
}{
"path": "index.html",
"sha256": "3dfe552fa17567b7d9322de46709fc628bdc9b66062dd9ce558c55765799cbed",
"size": "10.1 KB",
"size_bytes": 10303,
"updated_at": "2026-06-12T04:52:12Z"
}The page is written. I’m deploying it now as static files; the URL should be stable under the alias once the deployment completes.
create_static_deployment(workspace_id: "98f148f2-f5f0-45c0-afa8-9809acccd2d6", …) error
{
"workspace_id": "98f148f2-f5f0-45c0-afa8-9809acccd2d6",
"alias": "classic-minesweeper",
"spa_mode": false
}alias "classic-minesweeper" is already in use
That alias is already taken, so I’m redeploying with a more specific stable alias.
create_static_deployment(workspace_id: "98f148f2-f5f0-45c0-afa8-9809acccd2d6", …)
{
"workspace_id": "98f148f2-f5f0-45c0-afa8-9809acccd2d6",
"alias": "classic-minesweeper-9x9",
"spa_mode": false
}{
"alias": "classic-minesweeper-9x9",
"created_at": "2026-06-12T04:52:21Z",
"files_count": 1,
"id": "b69cc596-ac84-4ac2-a5b5-325670868db7",
"kind": "STATIC",
"service_mode": "ON_DEMAND",
"size": "10.1 KB",
"size_bytes": 10303,
"spa_mode": false,
"status": "READY",
"url": "https://app.unfazed.dev/classic-minesweeper-9x9/",
"workspace_id": "98f148f2-f5f0-45c0-afa8-9809acccd2d6"
}Built and deployed the static Minesweeper page.
Public link: https://app.unfazed.dev/classic-minesweeper-9x9
It includes the requested 9x9 board, 10 mines, right-click flagging, timer, mine counter, reset button, and a dark minimal UI.