Here is a small Flood-It board. You always control the top-left cell. Click a color below to flood your corner region into it; the connected blob touching your corner takes that color and absorbs matching neighbors. Win by making every cell the same color in as few moves as you can.
<p class="hint">You control the <b>top-left</b> cell. Click a color to flood your corner region into it — the connected blob touching the corner takes that color and swallows matching neighbors. Make the <b>whole board one color</b> in as few moves as you can, then compare against the optimal solver.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Moves: 0</div>
<div class="palette" id="palette"></div>
<div class="btns">
<button id="optimal" type="button">Show optimal</button>
<button id="reset" type="button" class="ghost">Reset</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: 3px; margin: .4rem 0; }
.cell { width: 46px; height: 46px; border-radius: 6px; border: 1px solid rgba(0,0,0,.12); }
.cell.corner { box-shadow: inset 0 0 0 3px #1d3557; }
.c0 { background: #e63946; } .c1 { background: #2a9d8f; } .c2 { background: #f4a261; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.palette { display: flex; gap: .5rem; margin: .3rem 0 .6rem; }
.swatch { width: 40px; height: 40px; border-radius: 8px; cursor: pointer;
border: 2px solid rgba(0,0,0,.15); transition: transform .08s; }
.swatch:hover { transform: scale(1.08); }
.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] ?? ''));
// Flood-It: control the top-left corner; each move recolors the whole connected
// region touching the corner. Win when the board is one color, in as few moves
// as possible. Replaying/verifying a sequence is cheap; finding the SHORTEST one
// is NP-hard for >= 3 colors (Arthur, Clifford, Jalsenius, Montanaro & Sach, 2010).
// The "Show optimal" button does an exact breadth-first search over board states.
const N = 5, COLORS = 3;
const PALETTE = ['#e63946', '#2a9d8f', '#f4a261'];
// Fixed start board (verified: optimal = 5 moves, greedy = 6).
const START = [[2,0,2,0,1],[1,0,1,0,1],[2,2,1,2,0],[1,2,2,1,0],[1,0,1,1,1]];
let board, moves;
const clone = b => b.map(r => r.slice());
// Flood the corner region to color `col`; returns a new board.
function flood(b, col){
const nb = clone(b), start = b[0][0];
if (start === col) return nb;
const stack = [[0,0]], seen = new Set(['0,0']), region = [];
while (stack.length){
const [r,c] = stack.pop(); region.push([r,c]);
for (const [dr,dc] of [[1,0],[-1,0],[0,1],[0,-1]]){
const nr=r+dr, nc=c+dc;
if (nr>=0&&nr<N&&nc>=0&&nc<N && !seen.has(nr+','+nc) && b[nr][nc]===start){
seen.add(nr+','+nc); stack.push([nr,nc]);
}
}
}
for (const [r,c] of region) nb[r][c] = col;
return nb;
}
function isWon(b){ const f=b[0][0]; for(let r=0;r<N;r++)for(let c=0;c<N;c++) if(b[r][c]!==f) return false; return true; }
const keyOf = b => b.map(r=>r.join('')).join('|');
// Exact optimal move count via BFS over states (the slow but honest way).
function optimalMoves(start){
if (isWon(start)) return 0;
const seen = new Set([keyOf(start)]);
let frontier = [start], depth = 0;
while (frontier.length){
depth++; const next = [];
for (const st of frontier){
const cur = st[0][0];
for (let col=0; col<COLORS; col++){
if (col===cur) continue;
const nb = flood(st, col);
if (isWon(nb)) return depth;
const k = keyOf(nb);
if (!seen.has(k)){ seen.add(k); next.push(nb); }
}
}
frontier = next;
}
return -1;
}
function setStatus(t, cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }
function render(){
const el = document.getElementById('board'); el.innerHTML='';
for (let r=0;r<N;r++) for (let c=0;c<N;c++){
const d=document.createElement('div');
d.className='cell c'+board[r][c]+((r===0&&c===0)?' corner':'');
el.appendChild(d);
}
}
function move(col){
if (col===board[0][0] || isWon(board)) return;
board = flood(board, col); moves++;
render();
if (isWon(board)){
const opt = optimalMoves(START);
if (moves===opt) setStatus(fmt('Solved in %moves% moves — that is optimal! 🎉', { moves: moves }),'ok');
else setStatus(fmt('Solved in %moves% moves. Optimal is %opt%.', { moves: moves, opt: opt }), moves<=opt?'ok':'bad');
console.log('Solved in', moves, 'moves; optimal =', opt);
} else setStatus(fmt('Moves: %moves%', { moves: moves }));
}
function showOptimal(){
const opt = optimalMoves(START);
setStatus(fmt('The fewest moves possible for this board is %opt%.', { opt: opt }),'ok');
console.log('Optimal (BFS over board states):', opt, 'moves');
}
function reset(){ board=clone(START); moves=0; render(); setStatus('Moves:0'); }
const pal = document.getElementById('palette');
PALETTE.forEach((hex,i)=>{ const s=document.createElement('div'); s.className='swatch'; s.style.background=hex;
s.title=fmt('Color %i%%v%', { i: i, v: 1 }); s.addEventListener('click',()=>move(i)); pal.appendChild(s); });
document.getElementById('optimal').addEventListener('click', showOptimal);
document.getElementById('reset').addEventListener('click', reset);
reset();
<p class="hint">You control the <b>top-left</b> cell. Click a color to flood your corner region into it — the connected blob touching the corner takes that color and swallows matching neighbors. Make the <b>whole board one color</b> in as few moves as you can, then compare against the optimal solver.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Moves: 0</div>
<div class="palette" id="palette"></div>
<div class="btns">
<button id="optimal" type="button">Show optimal</button>
<button id="reset" type="button" class="ghost">Reset</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: 3px; margin: .4rem 0; }
.cell { width: 46px; height: 46px; border-radius: 6px; border: 1px solid rgba(0,0,0,.12); }
.cell.corner { box-shadow: inset 0 0 0 3px #1d3557; }
.c0 { background: #e63946; } .c1 { background: #2a9d8f; } .c2 { background: #f4a261; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.palette { display: flex; gap: .5rem; margin: .3rem 0 .6rem; }
.swatch { width: 40px; height: 40px; border-radius: 8px; cursor: pointer;
border: 2px solid rgba(0,0,0,.15); transition: transform .08s; }
.swatch:hover { transform: scale(1.08); }
.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] ?? ''));
// Flood-It: control the top-left corner; each move recolors the whole connected
// region touching the corner. Win when the board is one color, in as few moves
// as possible. Replaying/verifying a sequence is cheap; finding the SHORTEST one
// is NP-hard for >= 3 colors (Arthur, Clifford, Jalsenius, Montanaro & Sach, 2010).
// The "Show optimal" button does an exact breadth-first search over board states.
const N = 5, COLORS = 3;
const PALETTE = ['#e63946', '#2a9d8f', '#f4a261'];
// Fixed start board (verified: optimal = 5 moves, greedy = 6).
const START = [[2,0,2,0,1],[1,0,1,0,1],[2,2,1,2,0],[1,2,2,1,0],[1,0,1,1,1]];
let board, moves;
const clone = b => b.map(r => r.slice());
// Flood the corner region to color `col`; returns a new board.
function flood(b, col){
const nb = clone(b), start = b[0][0];
if (start === col) return nb;
const stack = [[0,0]], seen = new Set(['0,0']), region = [];
while (stack.length){
const [r,c] = stack.pop(); region.push([r,c]);
for (const [dr,dc] of [[1,0],[-1,0],[0,1],[0,-1]]){
const nr=r+dr, nc=c+dc;
if (nr>=0&&nr<N&&nc>=0&&nc<N && !seen.has(nr+','+nc) && b[nr][nc]===start){
seen.add(nr+','+nc); stack.push([nr,nc]);
}
}
}
for (const [r,c] of region) nb[r][c] = col;
return nb;
}
function isWon(b){ const f=b[0][0]; for(let r=0;r<N;r++)for(let c=0;c<N;c++) if(b[r][c]!==f) return false; return true; }
const keyOf = b => b.map(r=>r.join('')).join('|');
// Exact optimal move count via BFS over states (the slow but honest way).
function optimalMoves(start){
if (isWon(start)) return 0;
const seen = new Set([keyOf(start)]);
let frontier = [start], depth = 0;
while (frontier.length){
depth++; const next = [];
for (const st of frontier){
const cur = st[0][0];
for (let col=0; col<COLORS; col++){
if (col===cur) continue;
const nb = flood(st, col);
if (isWon(nb)) return depth;
const k = keyOf(nb);
if (!seen.has(k)){ seen.add(k); next.push(nb); }
}
}
frontier = next;
}
return -1;
}
function setStatus(t, cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+(cls||''); }
function render(){
const el = document.getElementById('board'); el.innerHTML='';
for (let r=0;r<N;r++) for (let c=0;c<N;c++){
const d=document.createElement('div');
d.className='cell c'+board[r][c]+((r===0&&c===0)?' corner':'');
el.appendChild(d);
}
}
function move(col){
if (col===board[0][0] || isWon(board)) return;
board = flood(board, col); moves++;
render();
if (isWon(board)){
const opt = optimalMoves(START);
if (moves===opt) setStatus(fmt('Solved in %moves% moves — that is optimal! 🎉', { moves: moves }),'ok');
else setStatus(fmt('Solved in %moves% moves. Optimal is %opt%.', { moves: moves, opt: opt }), moves<=opt?'ok':'bad');
console.log('Solved in', moves, 'moves; optimal =', opt);
} else setStatus(fmt('Moves: %moves%', { moves: moves }));
}
function showOptimal(){
const opt = optimalMoves(START);
setStatus(fmt('The fewest moves possible for this board is %opt%.', { opt: opt }),'ok');
console.log('Optimal (BFS over board states):', opt, 'moves');
}
function reset(){ board=clone(START); moves=0; render(); setStatus('Moves:0'); }
const pal = document.getElementById('palette');
PALETTE.forEach((hex,i)=>{ const s=document.createElement('div'); s.className='swatch'; s.style.background=hex;
s.title=fmt('Color %i%%v%', { i: i, v: 1 }); s.addEventListener('click',()=>move(i)); pal.appendChild(s); });
document.getElementById('optimal').addEventListener('click', showOptimal);
document.getElementById('reset').addEventListener('click', reset);
reset();
Comments
Loading comments...