Skip to content

Instantly share code, notes, and snippets.

@senko
Created August 8, 2025 07:34
Show Gist options
  • Save senko/5c0fa6f262bf88cbe521c7c9a897e0dd to your computer and use it in GitHub Desktop.
Save senko/5c0fa6f262bf88cbe521c7c9a897e0dd to your computer and use it in GitHub Desktop.
GPT-5 (full) zero-shotting Minesweeper
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Minesweeper โ€” Pure HTML/CSS/JS</title>
<style>
:root{
--bg: #0f1226;
--surface: #171a37;
--surface-2: #1e2249;
--border: rgba(255,255,255,0.08);
--text: #e8ebff;
--muted: #9aa4d6;
--accent: #ff5f87;
--good: #73e2a7;
--bad: #ff6b6b;
--warn: #ffc260;
--shadow: 0 10px 30px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06);
--glow: 0 0 0 0 rgba(255,95,135,0);
--cell-size: 36px;
--cell-radius: 10px;
--gap: 8px;
}
*{ box-sizing: border-box; }
html, body { height: 100%; }
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background: radial-gradient(1200px 800px at 75% -10%, #233 0%, transparent 60%), radial-gradient(1000px 700px at -20% 110%, #27314f 0%, transparent 60%), var(--bg);
color: var(--text);
display:flex;
align-items:center;
justify-content:center;
padding: 24px;
}
.app{
width: min(1000px, 96vw);
}
.panel{
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(0,0,0,0.15)), var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
box-shadow: var(--shadow);
backdrop-filter: blur(6px);
}
.topbar{
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
padding: 14px 16px;
margin-bottom: 14px;
}
.left-group, .right-group{
display:flex;
align-items:center;
gap:10px;
}
.brand{
font-weight:800;
letter-spacing:0.5px;
background: linear-gradient(90deg, #c6d0ff, #a7b2ff 60%, #e8abff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 2px 18px rgba(167,178,255,0.2);
}
.seg{
display:inline-flex;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 999px;
overflow:hidden;
}
.seg button{
all:unset;
cursor:pointer;
padding:8px 12px;
color: var(--muted);
transition: 200ms ease;
position:relative;
}
.seg button:hover{ color: var(--text); }
.seg button.active{
color:#0f1230;
}
.seg button.active::before{
content:"";
position:absolute; inset:2px;
border-radius:999px;
background: linear-gradient(180deg, #e1e6ff, #c8d0ff);
box-shadow: 0 8px 20px rgba(180,190,255,0.35), inset 0 1px 0 rgba(255,255,255,0.6);
z-index:-1;
}
.pill{
display:flex; align-items:center; gap:8px;
padding:8px 12px;
background: var(--surface-2);
border:1px solid var(--border);
border-radius: 999px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.pill strong{ color: var(--text); }
.face{
all:unset;
cursor:pointer;
font-size: 24px;
width: 40px; height: 40px;
display:grid; place-items:center;
border-radius: 12px;
background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.01));
border:1px solid var(--border);
box-shadow: var(--shadow), var(--glow);
transition: transform 120ms ease, box-shadow 240ms ease;
}
.face:active{ transform: translateY(1px); }
.flag-toggle{
all:unset; cursor:pointer;
padding:8px 12px;
border-radius: 999px;
background: var(--surface-2);
border:1px solid var(--border);
color: var(--muted);
display:flex; align-items:center; gap:8px;
transition: 200ms ease;
}
.flag-toggle.active{
box-shadow: 0 0 0 8px rgba(255,95,135,0.08), 0 8px 24px rgba(255,95,135,0.25);
color: var(--text);
border-color: rgba(255,95,135,0.6);
}
.board-wrap{
padding: 14px;
}
#board{
display:grid;
grid-template-columns: repeat(var(--cols), var(--cell-size));
gap: var(--gap);
touch-action: manipulation;
user-select: none;
}
.cell{
width: var(--cell-size);
height: var(--cell-size);
border-radius: var(--cell-radius);
background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(0,0,0,0.15)), #151836;
border:1px solid rgba(255,255,255,0.06);
box-shadow: 0 6px 14px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.08);
display:grid; place-items:center;
font-weight:800;
color: transparent;
position:relative;
cursor: pointer;
transition: transform 120ms ease, background 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
will-change: transform;
}
.cell:active{ transform: translateY(1px) scale(0.98); }
.cell.revealed{
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(0,0,0,0.2)), #10122b;
color: var(--text);
border-color: rgba(255,255,255,0.05);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), inset 0 -20px 40px rgba(0,0,0,0.35);
cursor: default;
transform: none;
}
.cell.revealed.zero{
color: transparent;
}
.cell.number-1{ color: #71a5ff; }
.cell.number-2{ color: #73e2a7; }
.cell.number-3{ color: #ff8a8a; }
.cell.number-4{ color: #c2a4ff; }
.cell.number-5{ color: #ffc260; }
.cell.number-6{ color: #7be7ff; }
.cell.number-7{ color: #b3a089; }
.cell.number-8{ color: #99a7bc; }
.cell.flagged::before,
.cell.flagged::after{
content:"";
position:absolute;
transition: 160ms ease;
}
.cell.flagged::before{ /* pole */
width: 10%;
height: 62%;
background: #e7eaf7;
left: 40%;
top: 18%;
border-radius: 2px;
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.15);
}
.cell.flagged::after{ /* flag */
left: 41%;
top: 20%;
width: 0; height: 0;
border-top: calc(var(--cell-size) * 0.18) solid var(--accent);
border-right: calc(var(--cell-size) * 0.22) solid transparent;
filter: drop-shadow(0 1px 0 rgba(0,0,0,0.25));
}
.cell.mine.revealed{
background: radial-gradient(circle at 50% 45%, #ff708f 0%, #ff416d 18%, #7a1c38 32%, #170912 60%), #0d0f22;
box-shadow: inset 0 0 30px rgba(255,33,84,0.5), 0 0 24px rgba(255,33,84,0.35);
border-color: rgba(255,33,84,0.5);
color: #fff;
}
.cell.mine.revealed::before{
content:"";
position:absolute;
width: 34%;
height: 34%;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #fff 0 4%, #000 12% 100%);
box-shadow:
0 0 0 2px #000,
0 0 14px rgba(0,0,0,0.4);
}
.cell.mine.revealed::after{
content:"";
position:absolute;
width: 64%;
height: 64%;
background: conic-gradient(from 0, #000 0 10%, transparent 10% 20%);
mask: radial-gradient(circle at center, transparent 0 20%, #000 22% 100%);
opacity: 0.8;
}
.cell.bad-flag{
animation: pulseBad 450ms ease;
}
@keyframes pulseBad{
0%{ box-shadow: 0 0 0 0 rgba(255,107,107,0.0); }
50%{ box-shadow: 0 0 0 10px rgba(255,107,107,0.15); }
100%{ box-shadow: 0 0 0 0 rgba(255,107,107,0.0); }
}
.reveal-anim{
animation: popIn 160ms ease-out both;
}
@keyframes popIn{
from{ transform: translateY(2px) scale(0.96); opacity: 0.6; }
to{ transform: none; opacity: 1; }
}
.footer{
display:flex; justify-content:space-between; align-items:center;
color: var(--muted);
margin-top: 12px;
font-size: 12px;
padding: 0 2px;
}
.footer a{ color: #b6c0ff; text-decoration: none; }
.footer a:hover{ text-decoration: underline; }
.inline-inputs{
display:flex; align-items:center; gap:8px;
padding: 6px 8px;
border-radius: 999px;
background: var(--surface-2);
border:1px solid var(--border);
}
.inline-inputs input{
width: 60px;
appearance: none;
-moz-appearance: textfield;
outline:none;
border:none;
padding:6px 8px;
border-radius: 8px;
background: rgba(255,255,255,0.05);
color: var(--text);
font-variant-numeric: tabular-nums;
}
.inline-inputs input::-webkit-outer-spin-button,
.inline-inputs input::-webkit-inner-spin-button{
-webkit-appearance: none;
margin:0;
}
.inline-inputs button{
all:unset;
cursor:pointer;
padding:6px 10px;
border-radius: 8px;
background: linear-gradient(180deg, #e1e6ff, #c8d0ff);
color:#0f1230;
font-weight:700;
border:1px solid rgba(255,255,255,0.55);
box-shadow: 0 8px 20px rgba(180,190,255,0.35);
}
.counter.bad strong{ color: var(--bad); }
.sr-only{
position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden;
}
@media (max-width: 680px){
:root{ --gap: 6px; --cell-radius: 8px; }
.topbar{ flex-wrap: wrap; }
.brand{ font-size: 18px; }
}
</style>
</head>
<body>
<div class="app">
<div class="panel topbar">
<div class="left-group">
<div class="brand">Minesweeper</div>
<div class="seg" id="difficulty">
<button data-preset="beginner" class="active">Beginner</button>
<button data-preset="intermediate">Intermediate</button>
<button data-preset="expert">Expert</button>
</div>
<div class="inline-inputs" title="Custom size">
<label class="sr-only" for="rows">Rows</label>
<input id="rows" type="number" min="5" max="40" value="9">
<label class="sr-only" for="cols">Cols</label>
<input id="cols" type="number" min="5" max="50" value="9">
<label class="sr-only" for="mines">Mines</label>
<input id="mines" type="number" min="1" value="10">
<button id="applyCustom">Go</button>
</div>
</div>
<div class="right-group">
<div class="pill counter" id="mineCounter" aria-live="polite" aria-atomic="true">
๐Ÿ’ฃ <strong>010</strong>
</div>
<button class="face" id="faceBtn" title="New game">๐Ÿ™‚</button>
<div class="pill timer" id="timer" aria-live="polite" aria-atomic="true">
โฑ๏ธ <strong>000</strong>
</div>
<button id="flagMode" class="flag-toggle" title="Toggle Flag mode (mobile friendly)">๐Ÿšฉ Flag</button>
</div>
</div>
<div class="panel board-wrap">
<div id="board" aria-label="Minesweeper board" role="grid"></div>
</div>
<div class="footer">
<div id="status" aria-live="polite">Good luck!</div>
<div id="bestTimes"></div>
</div>
</div>
<script>
(function(){
const boardEl = document.getElementById('board');
const mineCounterEl = document.querySelector('#mineCounter strong');
const mineCounterPill = document.querySelector('#mineCounter');
const timerEl = document.querySelector('#timer strong');
const faceBtn = document.getElementById('faceBtn');
const flagModeBtn = document.getElementById('flagMode');
const statusEl = document.getElementById('status');
const bestTimesEl = document.getElementById('bestTimes');
const diffSeg = document.getElementById('difficulty');
const rowsInput = document.getElementById('rows');
const colsInput = document.getElementById('cols');
const minesInput = document.getElementById('mines');
const applyCustomBtn = document.getElementById('applyCustom');
const PRESETS = {
beginner: { w: 9, h: 9, m: 10, label: 'Beginner' },
intermediate: { w: 16, h: 16, m: 40, label: 'Intermediate' },
expert: { w: 30, h: 16, m: 99, label: 'Expert' }
};
let presetName = 'beginner';
let width = PRESETS[presetName].w;
let height = PRESETS[presetName].h;
let mineCount = PRESETS[presetName].m;
let started = false;
let gameOver = false;
let elapsed = 0;
let timerId = null;
let flagsPlaced = 0;
let revealedCount = 0;
let cells = []; // array of cell objects
let safeStartNeighbors = true; // friendlier start
let longPressTimer = null;
let longPressDidFlag = false;
// Responsive cell sizing
function updateCellSize(){
const wrap = boardEl.parentElement;
const available = wrap.clientWidth - 2 * 14 - (width - 1) * getGapPx(); // padding & gaps
const size = Math.max(22, Math.min(44, Math.floor(available / width)));
document.documentElement.style.setProperty('--cell-size', size + 'px');
boardEl.style.setProperty('--cols', width);
}
function getGapPx(){
const gap = getComputedStyle(document.documentElement).getPropertyValue('--gap').trim();
return Number(gap.replace('px','')) || 8;
}
window.addEventListener('resize', updateCellSize);
// Utils
function idx(r, c){ return r * width + c; }
function inBounds(r, c){ return r >= 0 && r < height && c >= 0 && c < width; }
function neighbors(r, c){
const res = [];
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(inBounds(nr, nc)) res.push(idx(nr, nc));
}
}
return res;
}
function format3(n){ return String(n).padStart(3, '0').slice(-3); }
function setStatus(msg){ statusEl.textContent = msg; }
function setFace(ch){ faceBtn.textContent = ch; }
function setGlow(on){
document.documentElement.style.setProperty('--glow', on ? '0 0 0 10px rgba(115,226,167,0.15)' : '0 0 0 0 rgba(0,0,0,0)');
}
function updateMineCounter(){
const remaining = mineCount - flagsPlaced;
mineCounterEl.textContent = format3(Math.max(-99, Math.min(999, remaining)));
mineCounterPill.classList.toggle('bad', remaining < 0);
}
function resetTimer(){
clearInterval(timerId);
elapsed = 0;
timerEl.textContent = format3(0);
timerId = null;
}
function startTimer(){
if(timerId) return;
timerId = setInterval(()=>{
elapsed++;
timerEl.textContent = format3(elapsed);
}, 1000);
}
function stopTimer(){
if(timerId){ clearInterval(timerId); timerId = null; }
}
function newGame(w, h, m, preset){
width = w; height = h; mineCount = m;
presetName = preset || 'custom';
started = false;
gameOver = false;
flagsPlaced = 0;
revealedCount = 0;
longPressDidFlag = false;
setFace('๐Ÿ™‚');
setGlow(false);
setStatus('Good luck! Tap to reveal, right-click or long-press to flag.');
updateMineCounter();
resetTimer();
buildBoard();
updateCellSize();
saveInputsFromCurrent();
renderBestTimes();
}
function buildBoard(){
boardEl.innerHTML = '';
boardEl.setAttribute('role','grid');
boardEl.setAttribute('aria-rowcount', height);
boardEl.setAttribute('aria-colcount', width);
boardEl.style.setProperty('--cols', width);
cells = new Array(width*height);
const frag = document.createDocumentFragment();
for(let r=0; r<height; r++){
for(let c=0; c<width; c++){
const i = idx(r,c);
const div = document.createElement('button');
div.className = 'cell';
div.setAttribute('role','gridcell');
div.setAttribute('aria-label', 'Hidden');
div.dataset.index = i;
div.dataset.r = r;
div.dataset.c = c;
div.tabIndex = -1;
cells[i] = {
r, c, mine:false, adj:0, revealed:false, flagged:false, el:div
};
frag.appendChild(div);
}
}
boardEl.appendChild(frag);
}
function placeMines(firstIndex){
const forbid = new Set([firstIndex]);
if(safeStartNeighbors){
const { r, c } = cells[firstIndex];
neighbors(r, c).forEach(n => forbid.add(n));
}
const total = width*height;
let available = [];
for(let i=0; i<total; i++){
if(!forbid.has(i)) available.push(i);
}
// shuffle and pick
for(let i=available.length-1; i>0; i--){
const j = Math.floor(Math.random()*(i+1));
[available[i], available[j]] = [available[j], available[i]];
}
const minesToPlace = Math.min(mineCount, available.length);
for(let i=0; i<minesToPlace; i++){
const id = available[i];
cells[id].mine = true;
}
// compute adjacency
for(let i=0; i<cells.length; i++){
const cell = cells[i];
if(cell.mine){ cell.adj = 0; continue; }
let count = 0;
neighbors(cell.r, cell.c).forEach(n => { if(cells[n].mine) count++; });
cell.adj = count;
}
}
function revealCell(i, viaChord=false){
if(gameOver) return;
const cell = cells[i];
if(cell.revealed || cell.flagged) return;
if(!started){
placeMines(i);
started = true;
startTimer();
}
if(cell.mine){
// hit a mine
showMine(i);
endGame(false);
return;
}
doReveal(i);
if(cell.adj === 0){
floodReveal(i);
}
if(viaChord){ pulseNeighbors(i); }
checkWin();
}
function doReveal(i){
const cell = cells[i];
if(cell.revealed) return;
cell.revealed = true;
revealedCount++;
const el = cell.el;
el.classList.add('revealed','reveal-anim');
el.setAttribute('aria-label', cell.adj === 0 ? 'Empty' : (cell.adj + ' adjacent'));
if(cell.adj > 0){
el.textContent = cell.adj;
el.classList.add('number-' + cell.adj);
}else{
el.textContent = '';
el.classList.add('zero');
}
}
function floodReveal(startIndex){
// BFS
const q = [startIndex];
const seen = new Set([startIndex]);
while(q.length){
const i = q.shift();
const cell = cells[i];
if(cell.adj !== 0) continue;
neighbors(cell.r, cell.c).forEach(n=>{
if(seen.has(n)) return;
const nc = cells[n];
if(!nc.revealed && !nc.flagged && !nc.mine){
doReveal(n);
seen.add(n);
if(nc.adj === 0) q.push(n);
}
});
}
}
function showMine(explodedIndex){
cells.forEach((cell, i)=>{
if(cell.mine){
const el = cell.el;
if(!cell.revealed){
el.classList.add('mine','revealed');
el.setAttribute('aria-label', 'Mine');
}
}
if(cell.flagged && !cell.mine){
cell.el.classList.add('bad-flag');
}
});
cells[explodedIndex].el.style.outline = '2px solid rgba(255,33,84,0.7)';
}
function endGame(won){
gameOver = true;
stopTimer();
if(won){
setFace('๐Ÿ˜Ž');
setGlow(true);
setStatus('You win! Time: ' + elapsed + 's');
maybeRecordBest();
// Auto-flag remaining
cells.forEach(cell=>{
if(cell.mine && !cell.flagged){
cell.flagged = true;
flagsPlaced++;
cell.el.classList.add('flagged');
}
});
updateMineCounter();
}else{
setFace('๐Ÿ˜ต');
setGlow(false);
setStatus('Boom! You hit a mine.');
}
}
function checkWin(){
if(revealedCount === width*height - mineCount){
endGame(true);
}
}
function toggleFlag(i){
if(gameOver) return;
const cell = cells[i];
if(cell.revealed) return;
if(!started){
// Allow flagging before start; timer starts on first reveal only
}
cell.flagged = !cell.flagged;
const el = cell.el;
if(cell.flagged){
el.classList.add('flagged');
el.setAttribute('aria-label','Flagged');
flagsPlaced++;
}else{
el.classList.remove('flagged');
el.setAttribute('aria-label','Hidden');
flagsPlaced--;
}
updateMineCounter();
}
function chordReveal(i){
const cell = cells[i];
if(!cell.revealed || cell.adj === 0) return;
const neigh = neighbors(cell.r, cell.c);
const flagged = neigh.filter(n=>cells[n].flagged).length;
if(flagged !== cell.adj){
// suggest with small shake?
return;
}
// reveal all un-flagged neighbors
for(const n of neigh){
if(!cells[n].flagged && !cells[n].revealed){
if(cells[n].mine){
// wrong flags -> lose
showMine(n);
endGame(false);
return;
}
}
}
for(const n of neigh){
if(!cells[n].flagged && !cells[n].revealed){
revealCell(n, true);
}
}
}
function pulseNeighbors(i){
const { r, c } = cells[i];
neighbors(r,c).forEach(n=>{
const el = cells[n].el;
el.classList.add('reveal-anim');
setTimeout(()=>el.classList.remove('reveal-anim'), 180);
});
}
// Events
boardEl.addEventListener('contextmenu', (e)=>{
e.preventDefault();
if(gameOver) return;
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
const i = Number(cellEl.dataset.index);
toggleFlag(i);
});
boardEl.addEventListener('click', (e)=>{
if(gameOver) return;
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
if(cellEl.dataset.ignoreClick === '1'){
cellEl.dataset.ignoreClick = '0';
return;
}
const i = Number(cellEl.dataset.index);
const cell = cells[i];
// If clicking revealed numbered cell, try chord
if(cell.revealed && cell.adj > 0){
chordReveal(i);
return;
}
revealCell(i);
});
// Touch long-press to flag
boardEl.addEventListener('pointerdown', (e)=>{
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
const i = Number(cellEl.dataset.index);
const cell = cells[i];
if(e.pointerType === 'mouse'){
// visual face press on left mouse
if(e.button === 0 && !cell.revealed && !cell.flagged && !gameOver){
setFace('๐Ÿ˜ฎ');
}
return;
}
if(e.pointerType === 'touch'){
// if flag mode toggle is active, let click handler handle reveal,
// and we will toggle flag on tap when flag mode is active by intercepting click
if(flagModeBtn.classList.contains('active')){
// Do nothing here
return;
}
// schedule long press
longPressDidFlag = false;
clearTimeout(longPressTimer);
longPressTimer = setTimeout(()=>{
longPressDidFlag = true;
toggleFlag(i);
cellEl.dataset.ignoreClick = '1'; // prevent following click
}, 350);
}
});
boardEl.addEventListener('pointerup', (e)=>{
setFace(gameOver ? (revealedCount === width*height - mineCount ? '๐Ÿ˜Ž' : '๐Ÿ˜ต') : '๐Ÿ™‚');
if(e.pointerType === 'touch'){
clearTimeout(longPressTimer);
}
});
boardEl.addEventListener('pointercancel', ()=>{ clearTimeout(longPressTimer); });
boardEl.addEventListener('pointermove', ()=>{ /* could cancel long press on move if needed */ });
// Flag mode (for touch)
flagModeBtn.addEventListener('click', ()=>{
const active = flagModeBtn.classList.toggle('active');
setStatus(active ? 'Flag mode: tap to place/remove flags.' : 'Reveal mode: tap to open cells.');
});
// Intercept click when flag mode active: toggle flag instead of reveal
boardEl.addEventListener('mousedown', (e)=>{
if(e.button === 0){
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
if(flagModeBtn.classList.contains('active')){
e.preventDefault();
}
}
}, true);
boardEl.addEventListener('click', (e)=>{
if(flagModeBtn.classList.contains('active')){
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
const i = Number(cellEl.dataset.index);
toggleFlag(i);
e.stopImmediatePropagation();
e.preventDefault();
}
}, true);
// Face/new game
faceBtn.addEventListener('click', ()=>{
newGame(width, height, mineCount, presetName);
});
// Difficulty buttons
diffSeg.addEventListener('click', (e)=>{
const btn = e.target.closest('button[data-preset]');
if(!btn) return;
diffSeg.querySelectorAll('button').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
const key = btn.dataset.preset;
const p = PRESETS[key];
rowsInput.value = p.h;
colsInput.value = p.w;
minesInput.value = p.m;
newGame(p.w, p.h, p.m, key);
});
// Apply custom
applyCustomBtn.addEventListener('click', ()=>{
const h = clamp(parseInt(rowsInput.value||9,10), 5, 40);
const w = clamp(parseInt(colsInput.value||9,10), 5, 50);
const maxMines = Math.max(1, w*h - 1);
const m = clamp(parseInt(minesInput.value||10,10), 1, maxMines);
rowsInput.value = h; colsInput.value = w; minesInput.value = m;
diffSeg.querySelectorAll('button').forEach(b=>b.classList.remove('active'));
newGame(w, h, m, 'custom');
});
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
// Keyboard: Space toggles flag mode, R restarts
document.addEventListener('keydown', (e)=>{
if(e.key.toLowerCase() === 'r'){
newGame(width, height, mineCount, presetName);
}else if(e.key === ' '){
e.preventDefault();
flagModeBtn.click();
}
});
// Save/retrieve best times
function bestKey(){
// Use dimension based key to support custom; presets use label keys too
return `ms-best-${width}x${height}x${mineCount}`;
}
function maybeRecordBest(){
const key = bestKey();
const prev = parseInt(localStorage.getItem(key)||'0',10);
if(prev === 0 || elapsed < prev){
localStorage.setItem(key, String(elapsed));
}
if(PRESETS[presetName]){
const pkey = `ms-best-${presetName}`;
const pprev = parseInt(localStorage.getItem(pkey)||'0',10);
if(pprev === 0 || elapsed < pprev){
localStorage.setItem(pkey, String(elapsed));
}
}
renderBestTimes();
}
function renderBestTimes(){
const parts = [];
['beginner', 'intermediate', 'expert'].forEach(k=>{
const t = localStorage.getItem(`ms-best-${k}`);
parts.push(`${PRESETS[k].label}: ${t ? t + 's' : 'โ€”'}`);
});
bestTimesEl.textContent = 'Best โ€” ' + parts.join(' ยท ');
}
// Accessibility helpers
boardEl.addEventListener('mouseover', (e)=>{
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
const i = Number(cellEl.dataset.index);
const cell = cells[i];
if(cell.revealed && cell.adj > 0){
cellEl.style.outline = '1px solid rgba(255,255,255,0.06)';
}
});
boardEl.addEventListener('mouseout', (e)=>{
const cellEl = e.target.closest('.cell');
if(!cellEl) return;
cellEl.style.outline = '';
});
// Prevent page context menu on long press for mobile within board
boardEl.addEventListener('touchstart', ()=>{}, {passive:true});
// Initialize from default preset
function saveInputsFromCurrent(){
rowsInput.value = height;
colsInput.value = width;
minesInput.value = mineCount;
}
// Start
newGame(width, height, mineCount, presetName);
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment