Here is a small board with the mines hidden. Every number is a clue — it counts the mines in the eight squares around it. Click the gray cells to place mines until every clue is satisfied.
<p class="hint">The numbers are clues: each says how many mines touch it (including diagonals). Click the gray cells to place mines so that <b>every</b> number is satisfied. Checking a board is instant — finding a valid layout is the hard part.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Place mines, then check.</div>
<div class="btns">
<button id="check" type="button">Check board</button>
<button id="find" type="button">Find a valid layout</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.board { display: grid; grid-template-columns: repeat(5, 46px); gap: 4px; margin: .4rem 0; }
.cell { width: 46px; height: 46px; display: flex; align-items: center; justify-content: center;
font: 700 18px ui-monospace, monospace; border-radius: 8px; user-select: none; }
.clue { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.clue.zero { color: transparent; }
.cand { background: #c9ccd1; border: 1px solid #adb1b8; cursor: pointer; transition: all .1s; }
.cand:hover { background: #bcc0c6; }
.cand.mine { background: #e63946; border-color: #c92f3c; color: #fff; }
.cand.mine::after { content: "✹"; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Minesweeper "consistency": given the clue numbers, is there a placement of
// mines on the covered (gray) cells so that every clue equals the count of
// mines around it? Checking one layout is a quick scan; finding one in general
// is NP-complete (Kaye, 2000) — brute force tries 2^(covered cells).
const N = 5;
// The 5 covered candidate cells (row,col). The intended solution marks 3 of them.
const CAND = [[1,1],[1,3],[2,2],[3,2],[3,3]];
const SOLUTION = new Set(['1,1','1,3','3,2']); // mines; (2,2) and (3,3) are safe
const candKey = (r,c) => r + ',' + c;
const isCand = (r,c) => CAND.some(([a,b]) => a===r && b===c);
// Clue value for every non-candidate cell = mines (from SOLUTION) in its 8-neighborhood.
function neighbors(r,c){ const o=[]; for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){
if(!dr&&!dc)continue; const nr=r+dr,nc=c+dc; if(nr>=0&&nr<N&&nc>=0&&nc<N)o.push([nr,nc]); } return o; }
const CLUE = {};
for(let r=0;r<N;r++)for(let c=0;c<N;c++) if(!isCand(r,c))
CLUE[candKey(r,c)] = neighbors(r,c).filter(([a,b])=>SOLUTION.has(candKey(a,b))).length;
const placed = new Set(); // player's mines
function render(){
const b = document.getElementById('board'); b.innerHTML='';
for(let r=0;r<N;r++)for(let c=0;c<N;c++){
const d=document.createElement('div'); const k=candKey(r,c);
if(isCand(r,c)){
d.className='cell cand'+(placed.has(k)?' mine':'');
d.addEventListener('click',()=>{ placed.has(k)?placed.delete(k):placed.add(k); render(); setStatus('Place mines, then check.',''); });
} else {
const v=CLUE[k]; d.className='cell clue'+(v===0?' zero':''); d.textContent=v;
}
b.appendChild(d);
}
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
// A layout is valid iff every clue equals the mines around it.
function valid(mines){
for(const k in CLUE){ const [r,c]=k.split(',').map(Number);
const cnt=neighbors(r,c).filter(([a,b])=>mines.has(candKey(a,b))).length;
if(cnt!==CLUE[k]) return false; }
return true;
}
function check(){
if(valid(placed)) setStatus('✓ Every clue is satisfied — valid layout!','ok');
else setStatus('✗ Some clue is wrong. Keep deducing.','bad');
}
function find(){
let steps=0, sol=null;
for(let mask=0; mask<(1<<CAND.length); mask++){ steps++;
const m=new Set(); CAND.forEach(([r,c],i)=>{ if(mask&(1<<i)) m.add(candKey(r,c)); });
if(valid(m)){ sol=m; break; } }
if(sol){ placed.clear(); sol.forEach(k=>placed.add(k)); render();
setStatus(fmt('Found a valid layout after checking %steps% of %v%.', { steps: steps, v: 1<<CAND.length }),'ok');
console.log('Solution mines:', [...sol].join(' '), '| checked', steps, 'layouts'); }
else setStatus('No layout satisfies these clues.','bad');
}
document.getElementById('check').addEventListener('click',check);
document.getElementById('find').addEventListener('click',find);
document.getElementById('reset').addEventListener('click',()=>{ placed.clear(); render(); setStatus('Place mines, then check.',''); });
render();
<p class="hint">The numbers are clues: each says how many mines touch it (including diagonals). Click the gray cells to place mines so that <b>every</b> number is satisfied. Checking a board is instant — finding a valid layout is the hard part.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Place mines, then check.</div>
<div class="btns">
<button id="check" type="button">Check board</button>
<button id="find" type="button">Find a valid layout</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.board { display: grid; grid-template-columns: repeat(5, 46px); gap: 4px; margin: .4rem 0; }
.cell { width: 46px; height: 46px; display: flex; align-items: center; justify-content: center;
font: 700 18px ui-monospace, monospace; border-radius: 8px; user-select: none; }
.clue { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.clue.zero { color: transparent; }
.cand { background: #c9ccd1; border: 1px solid #adb1b8; cursor: pointer; transition: all .1s; }
.cand:hover { background: #bcc0c6; }
.cand.mine { background: #e63946; border-color: #c92f3c; color: #fff; }
.cand.mine::after { content: "✹"; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Minesweeper "consistency": given the clue numbers, is there a placement of
// mines on the covered (gray) cells so that every clue equals the count of
// mines around it? Checking one layout is a quick scan; finding one in general
// is NP-complete (Kaye, 2000) — brute force tries 2^(covered cells).
const N = 5;
// The 5 covered candidate cells (row,col). The intended solution marks 3 of them.
const CAND = [[1,1],[1,3],[2,2],[3,2],[3,3]];
const SOLUTION = new Set(['1,1','1,3','3,2']); // mines; (2,2) and (3,3) are safe
const candKey = (r,c) => r + ',' + c;
const isCand = (r,c) => CAND.some(([a,b]) => a===r && b===c);
// Clue value for every non-candidate cell = mines (from SOLUTION) in its 8-neighborhood.
function neighbors(r,c){ const o=[]; for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){
if(!dr&&!dc)continue; const nr=r+dr,nc=c+dc; if(nr>=0&&nr<N&&nc>=0&&nc<N)o.push([nr,nc]); } return o; }
const CLUE = {};
for(let r=0;r<N;r++)for(let c=0;c<N;c++) if(!isCand(r,c))
CLUE[candKey(r,c)] = neighbors(r,c).filter(([a,b])=>SOLUTION.has(candKey(a,b))).length;
const placed = new Set(); // player's mines
function render(){
const b = document.getElementById('board'); b.innerHTML='';
for(let r=0;r<N;r++)for(let c=0;c<N;c++){
const d=document.createElement('div'); const k=candKey(r,c);
if(isCand(r,c)){
d.className='cell cand'+(placed.has(k)?' mine':'');
d.addEventListener('click',()=>{ placed.has(k)?placed.delete(k):placed.add(k); render(); setStatus('Place mines, then check.',''); });
} else {
const v=CLUE[k]; d.className='cell clue'+(v===0?' zero':''); d.textContent=v;
}
b.appendChild(d);
}
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
// A layout is valid iff every clue equals the mines around it.
function valid(mines){
for(const k in CLUE){ const [r,c]=k.split(',').map(Number);
const cnt=neighbors(r,c).filter(([a,b])=>mines.has(candKey(a,b))).length;
if(cnt!==CLUE[k]) return false; }
return true;
}
function check(){
if(valid(placed)) setStatus('✓ Every clue is satisfied — valid layout!','ok');
else setStatus('✗ Some clue is wrong. Keep deducing.','bad');
}
function find(){
let steps=0, sol=null;
for(let mask=0; mask<(1<<CAND.length); mask++){ steps++;
const m=new Set(); CAND.forEach(([r,c],i)=>{ if(mask&(1<<i)) m.add(candKey(r,c)); });
if(valid(m)){ sol=m; break; } }
if(sol){ placed.clear(); sol.forEach(k=>placed.add(k)); render();
setStatus(fmt('Found a valid layout after checking %steps% of %v%.', { steps: steps, v: 1<<CAND.length }),'ok');
console.log('Solution mines:', [...sol].join(' '), '| checked', steps, 'layouts'); }
else setStatus('No layout satisfies these clues.','bad');
}
document.getElementById('check').addEventListener('click',check);
document.getElementById('find').addEventListener('click',find);
document.getElementById('reset').addEventListener('click',()=>{ placed.clear(); render(); setStatus('Place mines, then check.',''); });
render();
Notice the asymmetry. Checking a finished board is effortless: walk over each number and count its neighbors. Finding a layout that satisfies all the clues at once is the hard part — press Find a valid layout and the computer simply tries every combination of mines. With five covered cells that's 32 possibilities; add more covered cells and the count doubles each time.
Comments
Loading comments...