Skip to content

Instantly share code, notes, and snippets.

@senko
Created August 8, 2025 07:39
Show Gist options
  • Save senko/c550f0865ca5d6adc6983e21e25cc1eb to your computer and use it in GitHub Desktop.
Save senko/c550f0865ca5d6adc6983e21e25cc1eb to your computer and use it in GitHub Desktop.
GPT-5 nano zero-shotting Minesweeper
<!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