Created
August 8, 2025 07:39
-
-
Save senko/c550f0865ca5d6adc6983e21e25cc1eb to your computer and use it in GitHub Desktop.
GPT-5 nano zero-shotting Minesweeper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>Minesweeper Mini</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<style> | |
:root{ | |
--bg: #0f1220; | |
--panel: #181a2a; | |
--panel2: #1e2030; | |
--text: #e8eaf6; | |
--muted: #a7b0d6; | |
--accent: #4cd964; | |
--red: #ff5c5c; | |
--tile: #cbd5e1; | |
--tile-rev: #e9f0ff; | |
--shadow: 0 6px 20px rgba(0,0,0,.25); | |
} | |
*{box-sizing:border-box} | |
html,body{height:100%} | |
body{ | |
margin:0; | |
font-family: Inter, ui-sans-serif, system-ui, -apple-system; | |
background: radial-gradient(circle at 20% -10%, rgba(76,217,100,.15), transparent 40%), | |
radial-gradient(circle at 80% 0%, rgba(0,0,0,.2), transparent 40%), | |
var(--bg); | |
color:var(--text); | |
display:flex; | |
align-items:center; | |
justify-content:center; | |
padding:20px; | |
} | |
.wrapper{ | |
width: min(1100px, 92vw); | |
} | |
.header{ | |
display:flex; | |
justify-content:space-between; | |
align-items:stretch; | |
gap:12px; | |
margin-bottom:14px; | |
} | |
.panel{ | |
background: linear-gradient(#23263a, #171a2d); | |
border-radius:14px; | |
padding:12px; | |
box-shadow: var(--shadow); | |
border:1px solid rgba(255,255,255,.05); | |
} | |
.controls{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; } | |
.btn{ | |
background: linear-gradient(#2a2f49,#1a1f34); | |
color:white; | |
border:none; | |
padding:10px 14px; | |
border-radius:10px; | |
cursor:pointer; | |
font-weight:600; | |
letter-spacing:.4px; | |
transition: transform .1s ease, background .2s ease; | |
} | |
.btn:hover{ transform: translateY(-1px); background: linear-gradient(#333b66,#1f2550); } | |
.btn:active{ transform: translateY(1px) scale(.98); } | |
select, .number{ | |
appearance:none; | |
background:#1e2030; | |
border:1px solid #2b2e46; | |
color:#e8eaf6; | |
padding:10px 12px; | |
border-radius:8px; | |
font-weight:600; | |
} | |
.status{ | |
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; | |
font-weight:700; | |
letter-spacing:.5px; | |
} | |
.board-wrap{ | |
display:grid; | |
place-items:center; | |
} | |
.board{ | |
display:grid; | |
grid-gap:6px; | |
padding:14px; | |
background: linear-gradient(#20243a, #11152b); | |
border-radius:14px; | |
border:1px solid rgba(255,255,255,.08); | |
box-shadow: inset 0 0 0 2px rgba(0,0,0,.25); | |
} | |
.cell{ | |
width: 34px; | |
height: 34px; | |
border-radius:6px; | |
display:flex; | |
align-items:center; | |
justify-content:center; | |
font-weight:700; | |
font-size:14px; | |
user-select:none; | |
cursor:pointer; | |
position:relative; | |
border:1px solid rgba(0,0,0,.15); | |
background: linear-gradient(#dbe5f9, #cbd8f4); | |
color:#0b2745; | |
} | |
.cell.revealed{ | |
background: #e9eefc; | |
border:1px solid #9fb2d9; | |
cursor:default; | |
color:#102a43; | |
} | |
.cell.mine{ | |
background: #ff5c5c; | |
color:white; | |
text-shadow: 0 1px 0 rgba(0,0,0,.2); | |
} | |
.cell.flagged{ | |
background: #fff3c4; | |
color:#2d2d2d; | |
} | |
.cell .num{ | |
font-weight:900; | |
text-shadow: 0 1px 0 rgba(255,255,255,.6); | |
} | |
.grid-sizer{ width:100%; height:auto; } | |
/* responsive sizing for larger boards (up to 30x16) */ | |
@media (min-width: 860px){ | |
.cell{ width: 38px; height:38px; font-size:14px; } | |
} | |
@media (min-width: 1100px){ | |
.cell{ width: 40px; height:40px; font-size:15px; } | |
} | |
.overlay{ | |
position: fixed; | |
inset: 0; | |
display:flex; | |
align-items:center; | |
justify-content:center; | |
background: rgba(0,0,0,.6); | |
z-index: 999; | |
} | |
.modal{ | |
background: #1b1e32; | |
padding: 24px; | |
border-radius: 12px; | |
text-align:center; | |
border:1px solid #2b2f55; | |
min-width: 260px; | |
} | |
.modal h2{ margin:0 0 12px; } | |
.modal p{ color:#cbd5e1; margin:0 0 10px; } | |
.modal .row{ display:flex; justify-content:center; gap:8px; margin-top:6px; } | |
.tiny{ font-size:12px; color:#a7b0d6; margin-top:6px; display:block; } | |
/* subtle decorative dot indicators for status bar */ | |
.status-dot{ width:8px; height:8px; border-radius:50%; display:inline-block; margin-right:6px; vertical-align:middle; } | |
.dot-ok{ background:var(--accent); } | |
.dot-warn{ background:#f7c223; } | |
.dot-red{ background:#ff5c5c; } | |
</style> | |
</head> | |
<body> | |
<div class="wrapper"> | |
<div class="header panel" aria-label="Minesweeper header"> | |
<div class="controls" id="controls-left"> | |
<button class="btn" id="newBtn" title="New game (N)">New Game</button> | |
<select id="difficulty" aria-label="Difficulty"> | |
<option value="easy" selected>Easy 9x9 (10 mines)</option> | |
<option value="medium">Medium 16x16 (40 mines)</option> | |
<option value="hard">Hard 30x16 (99 mines)</option> | |
</select> | |
</div> | |
<div class="status" id="status" aria-live="polite" style="display:flex; align-items:center; gap:8px;"> | |
<span class="status-dot dot-ok" id="dot"></span> | |
<span id="statusText" style="font-family: ui-monospace, monospace; font-weight:700;">Ready</span> | |
</div> | |
<div class="controls" id="controls-right" style="gap:6px;"> | |
<span class="tiny" style="margin-right:8px;">Mines Left</span> | |
<span id="minesLeft" class="status" style="min-width:60px; text-align:right;">0</span> | |
<span class="tiny" style="margin-left:8px; margin-right:4px;">⏱</span> | |
<span id="timer" class="status" style="min-width:60px; text-align:right;">00:00</span> | |
</div> | |
</div> | |
<div class="board-wrap panel" id="boardPanel" aria-label="Game board"> | |
<div id="board" class="board" role="grid" aria-label="Minesweeper board"></div> | |
</div> | |
<p class="tiny" style="text-align:center; margin-top:8px;"> | |
Pro tip: Left-click to reveal, Right-click to flag. First click is safe. | |
</p> | |
</div> | |
<script> | |
// Minesweeper clone in plain JS/HTML/CSS | |
// Board configuration by difficulty | |
const DIFFICULTIES = { | |
easy: { rows: 9, cols: 9, mines: 10 }, | |
medium:{ rows: 16, cols: 16, mines: 40 }, | |
hard: { rows: 16, cols: 30, mines: 99 } | |
}; | |
// State | |
let board = []; | |
let rows = 9, cols = 9, minesCount = 10; | |
let firstClick = true; | |
let revealedCount = 0; | |
let flags = 0; | |
let timerId = null; | |
let seconds = 0; | |
let gameOver = false; | |
// DOM | |
const boardEl = document.getElementById('board'); | |
const newBtn = document.getElementById('newBtn'); | |
const diffSel = document.getElementById('difficulty'); | |
const statusText = document.getElementById('statusText'); | |
const minesLeftEl = document.getElementById('minesLeft'); | |
const timerEl = document.getElementById('timer'); | |
const dotEl = document.getElementById('dot'); | |
const overlay = document.createElement('div'); | |
overlay.className = 'overlay'; | |
overlay.style.display = 'none'; | |
overlay.innerHTML = ` | |
<div class="modal" role="dialog" aria-label="Game result"> | |
<h2 id="modalTitle">Game Over</h2> | |
<p id="modalMsg">You hit a mine. Try again!</p> | |
<div class="row"> | |
<button class="btn" id="modalRestart">Play Again</button> | |
<button class="btn" id="modalClose">Close</button> | |
</div> | |
</div> | |
`; | |
document.body.appendChild(overlay); | |
function setDifficulty(key){ | |
const d = DIFFICULTIES[key]; | |
rows = d.rows; | |
cols = d.cols; | |
minesCount = d.mines; | |
} | |
function formatTime(s){ | |
const m = Math.floor(s/60); | |
const sec = s % 60; | |
return String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0'); | |
} | |
function resetBoard(playFirstSafe = false){ | |
// adjust grid | |
board = []; | |
firstClick = true; | |
revealedCount = 0; | |
flags = 0; | |
seconds = 0; | |
gameOver = false; | |
timerEl.textContent = "00:00"; | |
statusText.textContent = "Ready"; | |
dotEl.className = 'status-dot dot-ok'; | |
if (timerId) clearInterval(timerId); | |
timerId = null; | |
// reset DOM | |
boardEl.innerHTML = ''; | |
boardEl.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
boardEl.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
board.length = 0; | |
// create empty cells | |
for (let r = 0; r < rows; r++){ | |
const row = []; | |
for (let c = 0; c < cols; c++){ | |
const cell = { | |
r, c, | |
mine: false, | |
revealed: false, | |
flagged: false, | |
neighbor: 0 | |
}; | |
row.push(cell); | |
} | |
board.push(row); | |
} | |
// render cells | |
boardEl.style.gridTemplateRows = `repeat(${rows}, 1fr)`; | |
boardEl.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; | |
boardEl.style.gap = '6px'; | |
for (let r = 0; r < rows; r++){ | |
for (let c = 0; c < cols; c++){ | |
const cellEl = document.createElement('div'); | |
cellEl.className = 'cell'; | |
cellEl.setAttribute('data-r', r); | |
cellEl.setAttribute('data-c', c); | |
cellEl.setAttribute('role','gridcell'); | |
cellEl.setAttribute('aria-label','Hidden cell'); | |
// attach events | |
cellEl.addEventListener('click', onLeftClick); | |
cellEl.addEventListener('contextmenu', onRightClick); | |
// append | |
boardEl.appendChild(cellEl); | |
} | |
} | |
} | |
function placeMines(safeR, safeC){ | |
// place mines randomly, excluding the first clicked cell and its neighbors for safety | |
const total = rows * cols; | |
const exclude = new Set(); | |
for (let dr=-1; dr<=1; dr++){ | |
for (let dc=-1; dc<=1; dc++){ | |
const nr = safeR + dr; | |
const nc = safeC + dc; | |
if (nr>=0 && nr<rows && nc>=0 && nc<cols){ | |
exclude.add(nr*cols + nc); | |
} | |
} | |
} | |
let placed = 0; | |
while (placed < minesCount){ | |
const idx = Math.floor(Math.random() * total); | |
if (exclude.has(idx)) continue; | |
const r = Math.floor(idx / cols); | |
const c = idx % cols; | |
if (!board[r][c].mine){ | |
board[r][c].mine = true; | |
placed++; | |
} | |
} | |
// compute neighbor counts | |
for (let r=0; r<rows; r++){ | |
for (let c=0; c<cols; c++){ | |
if (board[r][c].mine) continue; | |
let count = 0; | |
for (let dr=-1; dr<=1; dr++){ | |
for (let dc=-1; dc<=1; dc++){ | |
if (dr===0 && dc===0) continue; | |
const nr = r+dr, nc = c+dc; | |
if (nr>=0 && nr<rows && nc>=0 && nc<cols){ | |
if (board[nr][nc].mine) count++; | |
} | |
} | |
} | |
board[r][c].neighbor = count; | |
} | |
} | |
} | |
function revealCell(r,c){ | |
const cell = board[r][c]; | |
const cellEl = getCellEl(r,c); | |
if (cell.revealed || cell.flagged) return; | |
cell.revealed = true; | |
revealedCount++; | |
cellEl.classList.add('revealed'); | |
cellEl.setAttribute('aria-label', `Revealed cell ${r+1}, ${c+1}`); | |
if (cell.mine){ | |
cellEl.classList.add('mine'); | |
cellEl.textContent = '💥'; | |
endGame(false); | |
return; | |
} | |
// show number or empty | |
if (cell.neighbor > 0){ | |
cellEl.innerHTML = `<span class="num" style="color:${numColor(cell.neighbor)}">${cell.neighbor}</span>`; | |
} else { | |
// empty: flood fill neighbors | |
floodReveal(r,c); | |
} | |
// check win | |
if (revealedCount === rows*cols - minesCount){ | |
endGame(true); | |
} | |
} | |
function floodReveal(r,c){ | |
const q = [[r,c]]; | |
while (q.length){ | |
const [cr,cc] = q.pop(); | |
for (let dr=-1; dr<=1; dr++){ | |
for (let dc=-1; dc<=1; dc++){ | |
if (dr===0 && dc===0) continue; | |
const nr = cr+dr, nc = cc+dc; | |
if (nr<0 || nr>=rows || nc<0 || nc>=cols) continue; | |
const ncell = board[nr][nc]; | |
if (ncell.revealed || ncell.flagged) continue; | |
ncell.revealed = true; | |
revealedCount++; | |
const nEl = getCellEl(nr,nc); | |
nEl.classList.add('revealed'); | |
nEl.setAttribute('aria-label', `Revealed cell ${nr+1}, ${nc+1}`); | |
if (ncell.mine){ | |
// should not happen in flood for safe first click, | |
// but guard anyway | |
nEl.classList.add('mine'); | |
nEl.textContent = '💥'; | |
} else if (ncell.neighbor > 0){ | |
nEl.innerHTML = `<span class="num" style="color:${numColor(ncell.neighbor)}">${ncell.neighbor}</span>`; | |
} else { | |
// expand further | |
q.push([nr,nc]); | |
} | |
} | |
} | |
} | |
} | |
function getCellEl(r,c){ | |
const index = r * cols + c; | |
return boardEl.children[index]; | |
} | |
function numColor(n){ | |
// color per number | |
const colors = { | |
1: '#1b5e20', | |
2: '#1565c0', | |
3: '#c62828', | |
4: '#351c75', | |
5: '#7a1a1a', | |
6: '#00695c', | |
7: '#1a237e', | |
8: '#2d2d2d' | |
}; | |
return colors[n] || '#000'; | |
} | |
function onLeftClick(e){ | |
if (gameOver) return; | |
const el = e.currentTarget; | |
const r = parseInt(el.dataset.r, 10); | |
const c = parseInt(el.dataset.c, 10); | |
if (board[r][c].revealed || board[r][c].flagged) return; | |
if (firstClick){ | |
// place mines after first click, ensuring first is safe | |
placeMines(r,c); | |
firstClick = false; | |
// start timer | |
timerId = setInterval(() => { | |
seconds++; | |
timerEl.textContent = formatTime(seconds); | |
}, 1000); | |
statusText.textContent = "In progress"; | |
dotEl.className = 'status-dot dot-ok'; | |
} | |
revealCell(r,c); | |
// if first click was on mine due to safety, handle; we ensure first isn't mine | |
} | |
function onRightClick(e){ | |
e.preventDefault(); | |
if (gameOver) return; | |
const el = e.currentTarget; | |
const r = parseInt(el.dataset.r, 10); | |
const c = parseInt(el.dataset.c, 10); | |
const cell = board[r][c]; | |
if (cell.revealed) return; | |
cell.flagged = !cell.flagged; | |
if (cell.flagged){ | |
el.classList.add('flagged'); | |
el.textContent = '⚑'; | |
flags++; | |
} else { | |
el.classList.remove('flagged'); | |
el.textContent = ''; | |
flags--; | |
} | |
const minesLeft = minesCount - flags; | |
minesLeftEl.textContent = String(minesLeft); | |
} | |
function endGame(won){ | |
gameOver = true; | |
if (timerId) clearInterval(timerId); | |
timerId = null; | |
// reveal all mines | |
for (let r=0; r<rows; r++){ | |
for (let c=0; c<cols; c++){ | |
const cell = board[r][c]; | |
const el = getCellEl(r,c); | |
if (cell.mine && !cell.revealed){ | |
el.classList.add('revealed','mine'); | |
el.textContent = '💣'; | |
} | |
} | |
} | |
if (won){ | |
statusText.textContent = "YouWin!"; | |
dotEl.className = 'status-dot dot-ok'; | |
overlay.style.display = 'flex'; | |
document.getElementById('modalTitle').textContent = 'You Win!'; | |
document.getElementById('modalMsg').textContent = 'All safe cells revealed. Nice job!'; | |
} else { | |
statusText.textContent = "Boom!"; | |
dotEl.className = 'status-dot dot-red'; | |
overlay.style.display = 'flex'; | |
document.getElementById('modalTitle').textContent = 'Game Over'; | |
document.getElementById('modalMsg').textContent = 'You hit a mine. Better luck next time!'; | |
} | |
} | |
// overlay modal handlers | |
overlay.addEventListener('click', (e) => { | |
// clicking outside modal should not close | |
if (e.target === overlay) return; | |
}); | |
// modal buttons | |
document.addEventListener('click', (e) => { | |
if (e.target && e.target.id === 'modalRestart'){ | |
overlay.style.display = 'none'; | |
startNewGame(); | |
} else if (e.target && e.target.id === 'modalClose'){ | |
overlay.style.display = 'none'; | |
} | |
}); | |
function startNewGame(){ | |
// reset counts | |
firstClick = true; | |
revealedCount = 0; | |
flags = 0; | |
seconds = 0; | |
timerEl.textContent = "00:00"; | |
minesLeftEl.textContent = String(minesCount); | |
gameOver = false; | |
statusText.textContent = "Ready"; | |
dotEl.className = 'status-dot dot-ok'; | |
if (timerId) clearInterval(timerId); | |
timerId = null; | |
// rebuild board data and DOM | |
resetBoard(); | |
// ensure mines count displayed | |
// (minesLeft computed after first click) | |
} | |
// Initialize | |
function init(){ | |
// default difficulty | |
setDifficulty(diffSel.value); | |
document.addEventListener('keydown', (e) => { | |
if (e.key.toLowerCase() === 'n'){ | |
startNewGame(); | |
} | |
}); | |
diffSel.addEventListener('change', () => { | |
setDifficulty(diffSel.value); | |
startNewGame(); | |
}); | |
newBtn.addEventListener('click', startNewGame); | |
resetBoard(); | |
// update mines left display | |
minesLeftEl.textContent = String(minesCount); | |
// accessibility: announce | |
statusText.textContent = "Ready"; | |
} | |
// kick off | |
init(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment