Here is the endgame everyone gets wrong. The board is carved into a few long chains of boxes, and it is your turn to open one. The greedy instinct says: take every box you can reach. The expert says: take all but the last two, then hand them over.
<p class="hint">The endgame is a set of long <b>chains</b> of boxes. It's <b>your</b>
turn to open the first chain. <b>Greedy</b> takes every box it can reach.
<b>Double-cross</b> takes all but the last two of each chain and hands them back —
keeping the opponent on the hook to open the next chain.</p>
<div class="chains" id="chains"></div>
<div class="score" id="score">You: 0 Opponent: 0</div>
<div class="status" id="status">Pick a strategy and watch the chains fall.</div>
<div class="btns">
<button id="greedy" type="button">Play Greedy</button>
<button id="cross" type="button">Play Double-cross</button>
<button id="reset" type="button" class="ghost">Reset chains</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; }
.chains { display: flex; flex-direction: column; gap: 8px; margin: .4rem 0; }
.chain { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.chain .label { font: 600 12px system-ui; color: #555; width: 64px; }
.box { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 13px ui-monospace, monospace;
background: #dfe5ec; border: 1px solid #c4cdd8; color: #1d3557; }
.box.you { background: #1d3557; border-color: #142844; color: #fff; }
.box.opp { background: #e63946; border-color: #c92f3c; color: #fff; }
.score { font: 700 1rem system-ui; margin: .6rem 0 .2rem; }
.status { font-size: .95rem; font-weight: 600; margin: .3rem 0 .6rem; 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; 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] ?? ''));
// Dots-and-Boxes endgame. The board is reduced to long CHAINS of boxes and it is
// YOU who must open the first one. Whoever opens a chain lets the other player
// take it. The double-cross: when you take a chain, leave the LAST TWO boxes and
// hand them to the opponent, who is then forced to open the next chain for you.
// This "all-but-two" sacrifice is the chain rule that decides the whole endgame.
const CHAINS = [3, 4, 6]; // chain lengths (boxes)
let anim = null;
function render(state){
const c = document.getElementById('chains'); c.innerHTML = '';
CHAINS.forEach((len, ci) => {
const row = document.createElement('div'); row.className = 'chain';
const lab = document.createElement('div'); lab.className = 'label';
lab.textContent = fmt('chain %ci%%v% (%len%)', { ci: ci, v: 1, len: len }); row.appendChild(lab);
for (let i=0;i<len;i++){
const b = document.createElement('div'); b.className = 'box';
const owner = state && state[ci] ? state[ci][i] : null;
if (owner==='you'){ b.classList.add('you'); b.textContent='Y'; }
else if (owner==='opp'){ b.classList.add('opp'); b.textContent='O'; }
row.appendChild(b);
}
c.appendChild(row);
});
}
function setScore(y,o){ document.getElementById('score').textContent =
fmt('You: %y% Opponent: %o%', { y: y, o: o }); }
function setStatus(t,cls){ const s=document.getElementById('status');
s.textContent=t; s.className='status '+cls; }
// Pure simulation: returns {you, opp, owners} for a strategy.
// YOU are forced to open the first chain — the losing seat for a greedy player.
// opener = who must open the current chain. The OTHER player (the "taker") eats
// it. Greedy taker eats every box, then must open the next chain. With the
// double-cross, when YOU are the taker you eat all but the last two and give
// those two to the opener, so the opener stays stuck opening the next chain.
// On the last chain there is nothing left to give back, so you simply take it.
function simulate(strategy){
let opener = 'you', you = 0, opp = 0;
const owners = CHAINS.map(len => Array(len).fill(null));
CHAINS.forEach((len, ci) => {
const taker = opener === 'you' ? 'opp' : 'you';
const last = ci === CHAINS.length - 1;
if (strategy === 'cross' && taker === 'you' && !last){
// double-cross: you eat len-2, hand the last two back to the opener.
for (let i=0;i<len-2;i++) owners[ci][i] = 'you';
owners[ci][len-2] = opener; owners[ci][len-1] = opener;
you += len - 2;
if (opener === 'you') you += 2; else opp += 2;
// opener keeps the move (forced) and must open the next chain.
} else {
// taker eats the whole chain, then must open next (or game ends).
for (let i=0;i<len;i++) owners[ci][i] = taker;
if (taker === 'you') you += len; else opp += len;
opener = taker;
}
});
return { you, opp, owners };
}
function animate(strategy){
if (anim) clearInterval(anim);
const res = simulate(strategy);
const flat = [];
res.owners.forEach((arr,ci)=>arr.forEach((o,i)=>flat.push([ci,i,o])));
const shown = CHAINS.map(len=>Array(len).fill(null));
let y=0,o=0,k=0;
render(shown); setScore(0,0);
anim = setInterval(()=>{
if (k>=flat.length){ clearInterval(anim); anim=null; finish(strategy,res); return; }
const [ci,i,owner]=flat[k++]; shown[ci][i]=owner;
if (owner==='you') y++; else o++;
render(shown); setScore(y,o);
}, 90);
}
function finish(strategy,res){
const win = res.you > res.opp;
if (strategy==='greedy')
setStatus(fmt('Greedy: You %you%, Opponent %opp%. %v%', { you: res.you, opp: res.opp, v: win?'(rare)':'You grabbed early boxes but were forced to open the last big chain — you lose.' }),
win?'ok':'bad');
else
setStatus(fmt('Double-cross: You %you%, Opponent %opp%. %v%', { you: res.you, opp: res.opp, v: win?'You sacrificed two boxes per chain to keep control — you win!':'tie/loss' }),
win?'ok':'bad');
console.log(strategy, res.you, 'vs', res.opp);
}
document.getElementById('greedy').addEventListener('click',()=>animate('greedy'));
document.getElementById('cross').addEventListener('click',()=>animate('cross'));
document.getElementById('reset').addEventListener('click',()=>{
if(anim){clearInterval(anim);anim=null;} render(null); setScore(0,0);
setStatus('Pick a strategy and watch the chains fall.',''); });
render(null); setScore(0,0);
<p class="hint">The endgame is a set of long <b>chains</b> of boxes. It's <b>your</b>
turn to open the first chain. <b>Greedy</b> takes every box it can reach.
<b>Double-cross</b> takes all but the last two of each chain and hands them back —
keeping the opponent on the hook to open the next chain.</p>
<div class="chains" id="chains"></div>
<div class="score" id="score">You: 0 Opponent: 0</div>
<div class="status" id="status">Pick a strategy and watch the chains fall.</div>
<div class="btns">
<button id="greedy" type="button">Play Greedy</button>
<button id="cross" type="button">Play Double-cross</button>
<button id="reset" type="button" class="ghost">Reset chains</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; }
.chains { display: flex; flex-direction: column; gap: 8px; margin: .4rem 0; }
.chain { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.chain .label { font: 600 12px system-ui; color: #555; width: 64px; }
.box { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 13px ui-monospace, monospace;
background: #dfe5ec; border: 1px solid #c4cdd8; color: #1d3557; }
.box.you { background: #1d3557; border-color: #142844; color: #fff; }
.box.opp { background: #e63946; border-color: #c92f3c; color: #fff; }
.score { font: 700 1rem system-ui; margin: .6rem 0 .2rem; }
.status { font-size: .95rem; font-weight: 600; margin: .3rem 0 .6rem; 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; 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] ?? ''));
// Dots-and-Boxes endgame. The board is reduced to long CHAINS of boxes and it is
// YOU who must open the first one. Whoever opens a chain lets the other player
// take it. The double-cross: when you take a chain, leave the LAST TWO boxes and
// hand them to the opponent, who is then forced to open the next chain for you.
// This "all-but-two" sacrifice is the chain rule that decides the whole endgame.
const CHAINS = [3, 4, 6]; // chain lengths (boxes)
let anim = null;
function render(state){
const c = document.getElementById('chains'); c.innerHTML = '';
CHAINS.forEach((len, ci) => {
const row = document.createElement('div'); row.className = 'chain';
const lab = document.createElement('div'); lab.className = 'label';
lab.textContent = fmt('chain %ci%%v% (%len%)', { ci: ci, v: 1, len: len }); row.appendChild(lab);
for (let i=0;i<len;i++){
const b = document.createElement('div'); b.className = 'box';
const owner = state && state[ci] ? state[ci][i] : null;
if (owner==='you'){ b.classList.add('you'); b.textContent='Y'; }
else if (owner==='opp'){ b.classList.add('opp'); b.textContent='O'; }
row.appendChild(b);
}
c.appendChild(row);
});
}
function setScore(y,o){ document.getElementById('score').textContent =
fmt('You: %y% Opponent: %o%', { y: y, o: o }); }
function setStatus(t,cls){ const s=document.getElementById('status');
s.textContent=t; s.className='status '+cls; }
// Pure simulation: returns {you, opp, owners} for a strategy.
// YOU are forced to open the first chain — the losing seat for a greedy player.
// opener = who must open the current chain. The OTHER player (the "taker") eats
// it. Greedy taker eats every box, then must open the next chain. With the
// double-cross, when YOU are the taker you eat all but the last two and give
// those two to the opener, so the opener stays stuck opening the next chain.
// On the last chain there is nothing left to give back, so you simply take it.
function simulate(strategy){
let opener = 'you', you = 0, opp = 0;
const owners = CHAINS.map(len => Array(len).fill(null));
CHAINS.forEach((len, ci) => {
const taker = opener === 'you' ? 'opp' : 'you';
const last = ci === CHAINS.length - 1;
if (strategy === 'cross' && taker === 'you' && !last){
// double-cross: you eat len-2, hand the last two back to the opener.
for (let i=0;i<len-2;i++) owners[ci][i] = 'you';
owners[ci][len-2] = opener; owners[ci][len-1] = opener;
you += len - 2;
if (opener === 'you') you += 2; else opp += 2;
// opener keeps the move (forced) and must open the next chain.
} else {
// taker eats the whole chain, then must open next (or game ends).
for (let i=0;i<len;i++) owners[ci][i] = taker;
if (taker === 'you') you += len; else opp += len;
opener = taker;
}
});
return { you, opp, owners };
}
function animate(strategy){
if (anim) clearInterval(anim);
const res = simulate(strategy);
const flat = [];
res.owners.forEach((arr,ci)=>arr.forEach((o,i)=>flat.push([ci,i,o])));
const shown = CHAINS.map(len=>Array(len).fill(null));
let y=0,o=0,k=0;
render(shown); setScore(0,0);
anim = setInterval(()=>{
if (k>=flat.length){ clearInterval(anim); anim=null; finish(strategy,res); return; }
const [ci,i,owner]=flat[k++]; shown[ci][i]=owner;
if (owner==='you') y++; else o++;
render(shown); setScore(y,o);
}, 90);
}
function finish(strategy,res){
const win = res.you > res.opp;
if (strategy==='greedy')
setStatus(fmt('Greedy: You %you%, Opponent %opp%. %v%', { you: res.you, opp: res.opp, v: win?'(rare)':'You grabbed early boxes but were forced to open the last big chain — you lose.' }),
win?'ok':'bad');
else
setStatus(fmt('Double-cross: You %you%, Opponent %opp%. %v%', { you: res.you, opp: res.opp, v: win?'You sacrificed two boxes per chain to keep control — you win!':'tie/loss' }),
win?'ok':'bad');
console.log(strategy, res.you, 'vs', res.opp);
}
document.getElementById('greedy').addEventListener('click',()=>animate('greedy'));
document.getElementById('cross').addEventListener('click',()=>animate('cross'));
document.getElementById('reset').addEventListener('click',()=>{
if(anim){clearInterval(anim);anim=null;} render(null); setScore(0,0);
setStatus('Pick a strategy and watch the chains fall.',''); });
render(null); setScore(0,0);
Play Greedy and watch yourself win the early chains — then get forced to open the final, biggest chain and lose. Play Double-cross and you sacrifice two boxes in each chain to keep handing the move back to your opponent, so they are the one forced to open the next chain. Two given-away boxes buy you a whole chain. This is the chain rule, and counting the chains decides the winner before a single box is closed.
Comments
Loading comments...