Here is a Chomp board. Click any cookie to eat it along with everything above and to its right. The poison cookie sits in the top-left corner — don't be the one who eats it.
<p class="hint">Click any cookie to eat it together with every cookie <b>above and to the right</b> of it. The <b>poison</b> cookie is the top-left one — whoever eats it loses. The computer moves first and plays <b>perfectly</b>, so the theorem says it always wins.</p>
<div id="board" class="board"></div>
<div class="status" id="status">The computer (player 1) is thinking…</div>
<div class="btns">
<button id="reset" type="button">New game</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(4, 52px); gap: 5px; margin: .4rem 0; }
.cell { width: 52px; height: 52px; display: flex; align-items: center; justify-content: center;
font-size: 24px; border-radius: 50%; user-select: none; transition: all .12s; }
.cookie { background: #d8a45a; border: 2px solid #b9842f; cursor: pointer; }
.cookie:hover { background: #e6b873; transform: scale(1.06); }
.cookie.preview { background: #f0c89a; border-style: dashed; }
.poison { background: #2b2b2b; border: 2px solid #000; color: #e63946; cursor: pointer; }
.poison::after { content: "☠"; }
.gone { background: transparent; border: 2px dashed #e2e2e2; cursor: default; }
.gone:hover { transform: none; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.win { color: #0a7d33; }
.status.lose { 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; }
// Chomp on a 4x4 grid. Cell (r,c): r = row (0 = top), c = column (0 = left).
// (0,0) is POISON. A move at (r,c) eats every cell (i,j) with i>=r AND j>=c.
// Whoever is forced to eat (0,0) loses. Gale (1974) proved the FIRST player wins on any rectangle bigger than 1x1 — by a nonconstructive strategy-stealing
// argument that never names a move. Here we just brute-force the small game tree, so the computer (player 1) provably never loses.
const M = 4, N = 4;
// Position = boolean grid of which cells remain (true = present).
function fullBoard() { return Array.from({length:M}, () => Array(N).fill(true)); }
function clone(b) { return b.map(row => row.slice()); }
function eat(b, r, c) { const nb = clone(b);
for (let i=r;i<M;i++) for (let j=c;j<N;j++) nb[i][j]=false; return nb; }
function cells(b) { const out=[]; for (let i=0;i<M;i++) for (let j=0;j<N;j++) if (b[i][j]) out.push([i,j]); return out; }
function key(b) { return b.map(row => row.map(x=>x?1:0).join('')).join(''); }
// win(b) = true if the player TO MOVE on b can force a win.
// Terminal: only the poison cell (0,0) remains -> player to move must eat it -> loses.
const memo = new Map();
function win(b) {
const cs = cells(b);
if (cs.length === 1 && b[0][0]) return false; // only poison left -> to-move loses
const k = key(b); if (memo.has(k)) return memo.get(k);
let res = false;
for (const [r,c] of cs) {
const nb = eat(b, r, c);
if (cells(nb).length === 0) continue; // ate the poison too -> this move loses
if (!win(nb)) { res = true; break; } // opponent loses -> winning move
}
memo.set(k, res); return res;
}
// The computer's perfect move: pick any move leaving the opponent in a losing spot.
function bestMove(b) {
for (const [r,c] of cells(b)) {
const nb = eat(b, r, c);
if (cells(nb).length === 0) continue;
if (!win(nb)) return [r,c];
}
return cells(b)[0]; // unreachable when computer is in a winning position
}
let board, over;
const boardEl = document.getElementById('board');
const statusEl = document.getElementById('status');
function setStatus(t, cls) { statusEl.textContent = t; statusEl.className = 'status ' + (cls||''); }
function render() {
boardEl.innerHTML = '';
for (let i=0;i<M;i++) for (let j=0;j<N;j++) {
const d = document.createElement('div');
if (!board[i][j]) { d.className = 'cell gone'; }
else if (i===0 && j===0) {
d.className = 'cell poison';
if (!over) d.addEventListener('click', () => humanMove(0,0));
} else {
d.className = 'cell cookie';
if (!over) {
d.addEventListener('mouseenter', () => preview(i,j,true));
d.addEventListener('mouseleave', () => preview(i,j,false));
d.addEventListener('click', () => humanMove(i,j));
}
}
boardEl.appendChild(d);
}
}
function preview(r,c,on) {
const idx = (i,j) => i*N + j;
for (let i=r;i<M;i++) for (let j=c;j<N;j++) if (board[i][j]) {
const el = boardEl.children[idx(i,j)];
if (!el.classList.contains('poison')) el.classList.toggle('preview', on);
}
}
function humanMove(r,c) {
if (over) return;
if (r===0 && c===0) { board = eat(board,0,0); over=true; render(); setStatus('You ate the poison — you lose. The theorem held.', 'lose'); return; }
board = eat(board, r, c);
render();
// did the human's move leave only poison for the computer? then computer is forced to eat it.
if (cells(board).length === 1) { over=true; setStatus('Only poison remains for the computer — you win! (It misplayed, or you found a flaw.)', 'win'); return; }
setTimeout(computerMove, 350);
}
function computerMove() {
const [r,c] = bestMove(board);
board = eat(board, r, c);
render();
if (cells(board).length === 1) { over=true; setStatus('Only the poison is left for you. The computer wins — exactly as proven.', 'lose'); return; }
setStatus('Your turn. Eat a cookie (not the poison!).', '');
}
function reset() {
board = fullBoard(); over=false;
render();
setStatus('The computer (player 1) opens…', '');
setTimeout(computerMove, 400);
}
document.getElementById('reset').addEventListener('click', reset);
reset();
<p class="hint">Click any cookie to eat it together with every cookie <b>above and to the right</b> of it. The <b>poison</b> cookie is the top-left one — whoever eats it loses. The computer moves first and plays <b>perfectly</b>, so the theorem says it always wins.</p>
<div id="board" class="board"></div>
<div class="status" id="status">The computer (player 1) is thinking…</div>
<div class="btns">
<button id="reset" type="button">New game</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(4, 52px); gap: 5px; margin: .4rem 0; }
.cell { width: 52px; height: 52px; display: flex; align-items: center; justify-content: center;
font-size: 24px; border-radius: 50%; user-select: none; transition: all .12s; }
.cookie { background: #d8a45a; border: 2px solid #b9842f; cursor: pointer; }
.cookie:hover { background: #e6b873; transform: scale(1.06); }
.cookie.preview { background: #f0c89a; border-style: dashed; }
.poison { background: #2b2b2b; border: 2px solid #000; color: #e63946; cursor: pointer; }
.poison::after { content: "☠"; }
.gone { background: transparent; border: 2px dashed #e2e2e2; cursor: default; }
.gone:hover { transform: none; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.win { color: #0a7d33; }
.status.lose { 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; }
// Chomp on a 4x4 grid. Cell (r,c): r = row (0 = top), c = column (0 = left).
// (0,0) is POISON. A move at (r,c) eats every cell (i,j) with i>=r AND j>=c.
// Whoever is forced to eat (0,0) loses. Gale (1974) proved the FIRST player wins on any rectangle bigger than 1x1 — by a nonconstructive strategy-stealing
// argument that never names a move. Here we just brute-force the small game tree, so the computer (player 1) provably never loses.
const M = 4, N = 4;
// Position = boolean grid of which cells remain (true = present).
function fullBoard() { return Array.from({length:M}, () => Array(N).fill(true)); }
function clone(b) { return b.map(row => row.slice()); }
function eat(b, r, c) { const nb = clone(b);
for (let i=r;i<M;i++) for (let j=c;j<N;j++) nb[i][j]=false; return nb; }
function cells(b) { const out=[]; for (let i=0;i<M;i++) for (let j=0;j<N;j++) if (b[i][j]) out.push([i,j]); return out; }
function key(b) { return b.map(row => row.map(x=>x?1:0).join('')).join(''); }
// win(b) = true if the player TO MOVE on b can force a win.
// Terminal: only the poison cell (0,0) remains -> player to move must eat it -> loses.
const memo = new Map();
function win(b) {
const cs = cells(b);
if (cs.length === 1 && b[0][0]) return false; // only poison left -> to-move loses
const k = key(b); if (memo.has(k)) return memo.get(k);
let res = false;
for (const [r,c] of cs) {
const nb = eat(b, r, c);
if (cells(nb).length === 0) continue; // ate the poison too -> this move loses
if (!win(nb)) { res = true; break; } // opponent loses -> winning move
}
memo.set(k, res); return res;
}
// The computer's perfect move: pick any move leaving the opponent in a losing spot.
function bestMove(b) {
for (const [r,c] of cells(b)) {
const nb = eat(b, r, c);
if (cells(nb).length === 0) continue;
if (!win(nb)) return [r,c];
}
return cells(b)[0]; // unreachable when computer is in a winning position
}
let board, over;
const boardEl = document.getElementById('board');
const statusEl = document.getElementById('status');
function setStatus(t, cls) { statusEl.textContent = t; statusEl.className = 'status ' + (cls||''); }
function render() {
boardEl.innerHTML = '';
for (let i=0;i<M;i++) for (let j=0;j<N;j++) {
const d = document.createElement('div');
if (!board[i][j]) { d.className = 'cell gone'; }
else if (i===0 && j===0) {
d.className = 'cell poison';
if (!over) d.addEventListener('click', () => humanMove(0,0));
} else {
d.className = 'cell cookie';
if (!over) {
d.addEventListener('mouseenter', () => preview(i,j,true));
d.addEventListener('mouseleave', () => preview(i,j,false));
d.addEventListener('click', () => humanMove(i,j));
}
}
boardEl.appendChild(d);
}
}
function preview(r,c,on) {
const idx = (i,j) => i*N + j;
for (let i=r;i<M;i++) for (let j=c;j<N;j++) if (board[i][j]) {
const el = boardEl.children[idx(i,j)];
if (!el.classList.contains('poison')) el.classList.toggle('preview', on);
}
}
function humanMove(r,c) {
if (over) return;
if (r===0 && c===0) { board = eat(board,0,0); over=true; render(); setStatus('You ate the poison — you lose. The theorem held.', 'lose'); return; }
board = eat(board, r, c);
render();
// did the human's move leave only poison for the computer? then computer is forced to eat it.
if (cells(board).length === 1) { over=true; setStatus('Only poison remains for the computer — you win! (It misplayed, or you found a flaw.)', 'win'); return; }
setTimeout(computerMove, 350);
}
function computerMove() {
const [r,c] = bestMove(board);
board = eat(board, r, c);
render();
if (cells(board).length === 1) { over=true; setStatus('Only the poison is left for you. The computer wins — exactly as proven.', 'lose'); return; }
setStatus('Your turn. Eat a cookie (not the poison!).', '');
}
function reset() {
board = fullBoard(); over=false;
render();
setStatus('The computer (player 1) opens…', '');
setTimeout(computerMove, 400);
}
document.getElementById('reset').addEventListener('click', reset);
reset();
The computer always moves first and plays perfectly, so it should win every game. On this small board it finds its moves by searching the full game tree. Now the unsettling part: the strategy-stealing proof guarantees the first player wins on every rectangle, but it does so without computing anything — it never hands you the winning move. For boards just slightly larger, no one has ever written that strategy down.
Comments
Loading comments...