Click any peg to select it, then click a highlighted hole to jump. Watch the invariant value in the top bar: it never changes. Then click Check center target to see whether the one-peg-at-center goal is consistent with the invariant.
<!-- Hint paragraph + invariant display bar + board + controls -->
<p class="hint">Each cell is labeled <b>0</b> (blue) or <b>1</b> (amber) — a cell gets label 1 only when both its row AND column index are not multiples of 3. The <b>invariant</b> is the XOR (parity) of labels of all pegs on the board: it never changes no matter how you jump. Click a peg to select it, then click a green-outlined hole to jump.</p>
<div class="top-bar">
<div class="inv-display">
<span class="inv-label">Invariant (XOR of labels):</span>
<span id="inv-value" class="inv-badge" aria-live="polite">0</span>
</div>
<div class="legend">
<span class="leg-chip leg-zero">Label 0</span>
<span class="leg-chip leg-one">Label 1</span>
</div>
</div>
<div id="board" class="board" role="grid" aria-label="Peg solitaire board, 33 holes in a cross shape"></div>
<div id="status" class="status" aria-live="polite">Click a peg to select it, then click a highlighted hole to jump.</div>
<div class="btns">
<button id="btn-target" type="button">Check center target</button>
<button id="btn-reset" type="button" class="ghost">Reset board</button>
</div>
/* Peg solitaire invariant demo — styles */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.hint { font-size: .85rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.top-bar { display: flex; align-items: center; justify-content: space-between;
margin-bottom: .5rem; flex-wrap: wrap; gap: .4rem; }
.inv-label { font-size: .82rem; color: #555; margin-right: .3rem; }
.inv-badge { font: 700 1rem ui-monospace, monospace;
padding: .15rem .55rem; border-radius: 6px; min-width: 2rem;
display: inline-block; text-align: center; transition: background .2s; }
.inv-badge.inv-0 { background: #d4f0d4; color: #1a5c1a; border: 1px solid #8ecb8e; }
.inv-badge.inv-1 { background: #ffe8c0; color: #7a4500; border: 1px solid #e0b870; }
.legend { display: flex; gap: .4rem; }
.leg-chip { font: 600 .78rem system-ui; padding: .18rem .45rem; border-radius: 5px;
border: 1px solid #bbb; }
.leg-zero { background: #e8eef3; color: #1d3557; border-color: #c2cedb; }
.leg-one { background: #ffe8c0; color: #7a4500; border-color: #e0b870; }
/* 7x7 grid; only the 33 cross-shaped holes are rendered */
.board { display: grid; grid-template-columns: repeat(7, 40px);
grid-template-rows: repeat(7, 40px); gap: 3px; margin: .25rem 0; }
.cell { width: 40px; height: 40px; border-radius: 7px; display: flex;
align-items: center; justify-content: center;
user-select: none; position: relative; }
.cell.empty-border { background: transparent; }
.cell.hole-0 { background: #e8eef3; border: 1px solid #c2cedb; }
.cell.hole-1 { background: #f8f0e0; border: 1px solid #d8c898; }
.cell.peg-0 { background: #1d3557; border: 2px solid #14273e; cursor: pointer; }
.cell.peg-1 { background: #c07000; border: 2px solid #9a5a00; cursor: pointer; }
.cell.peg-0::after, .cell.peg-1::after { content: "●"; font-size: 1.25rem;
color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-52%); }
.cell.selected { outline: 3px solid #e63946; outline-offset: -2px; }
.cell.landing { outline: 3px solid #0a7d33; outline-offset: -2px; cursor: pointer; }
.cell.landing::after { content: "○" !important; font-size: 1.2rem;
color: #0a7d33 !important; }
.status { font-size: .9rem; font-weight: 600; margin: .4rem 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557;
color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Standard English peg solitaire board — 33 holes in a cross shape
const VALID = new Set([
'0,2','0,3','0,4',
'1,2','1,3','1,4',
'2,0','2,1','2,2','2,3','2,4','2,5','2,6',
'3,0','3,1','3,2','3,3','3,4','3,5','3,6',
'4,0','4,1','4,2','4,3','4,4','4,5','4,6',
'5,2','5,3','5,4',
'6,2','6,3','6,4'
]);
const key = (r,c) => r+','+c;
// GF(2) invariant: label(r,c) = 1 iff r%3 != 0 AND c%3 != 0; else 0
// Every legal jump XORs labels of start, mid, end — net XOR = label(start)^label(mid)^label(end) = 0 for all jumps
const label = (r,c) => ((r%3!==0)&&(c%3!==0)) ? 1 : 0;
// Invariant = XOR of labels of all occupied cells (= parity of count of label-1 pegs)
function invariant(ps) {
let v = 0;
for (const k of ps) { const [r,c] = k.split(',').map(Number); v ^= label(r,c); }
return v;
}
// Standard starting position — all 33 holes filled except the center (3,3)
const pegs = new Set([...VALID]);
pegs.delete('3,3'); // Standard start — center hole empty
let selected = null;
let landings = [];
// A legal jump goes 2 cells orthogonally over an occupied cell into an empty cell
function validLandings(r, c) {
const out = [];
for (const [dr,dc] of [[-2,0],[2,0],[0,-2],[0,2]]) {
const lr=r+dr, lc=c+dc, mr=r+dr/2, mc=c+dc/2;
const lk=key(lr,lc), mk=key(mr,mc);
if (VALID.has(lk) && !pegs.has(lk) && pegs.has(mk)) out.push([lr,lc]);
}
return out;
}
function updateInvDisplay() {
const v = invariant(pegs);
const el = document.getElementById('inv-value');
el.textContent = v;
el.className = 'inv-badge inv-' + v;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.innerHTML = t; s.className = 'status ' + cls;
}
function render() {
const board = document.getElementById('board');
board.innerHTML = '';
for (let r=0; r<7; r++) for (let c=0; c<7; c++) {
const d = document.createElement('div');
const k = key(r,c);
if (!VALID.has(k)) { d.className = 'cell empty-border'; board.appendChild(d); continue; }
const lbl = label(r,c);
const isPeg = pegs.has(k);
const isSel = selected === k;
const isLand = landings.some(([lr,lc]) => lr===r && lc===c);
if (isLand) {
d.className = 'cell landing hole-'+lbl;
d.title = 'Jump here';
d.addEventListener('click', () => doJump(r,c));
} else if (isPeg) {
d.className = 'cell peg-'+lbl + (isSel ? ' selected' : '');
d.title = i18nFmt('Label %lbl% — click to select', { lbl: lbl });
d.addEventListener('click', () => selectPeg(r,c));
} else {
d.className = 'cell hole-'+lbl;
}
board.appendChild(d);
}
updateInvDisplay();
}
function selectPeg(r, c) {
const k = key(r,c);
if (selected === k) { selected=null; landings=[]; render(); setStatus('Click a peg to select it, then click a highlighted hole to jump.',''); return; }
selected = k;
landings = validLandings(r, c);
render();
if (landings.length === 0) setStatus('No legal jumps from this peg.', 'info');
else setStatus(i18nFmt('%n% landing hole(s) available — click a highlighted hole to jump.', { n: landings.length }), 'info');
}
function doJump(lr, lc) {
if (!selected) return;
const [sr,sc] = selected.split(',').map(Number);
const mr = (sr+lr)/2, mc = (sc+lc)/2;
pegs.delete(selected); pegs.delete(key(mr,mc)); pegs.add(key(lr,lc));
selected = null; landings = [];
render();
setStatus(i18nFmt('Jumped! %remaining% peg(s) left. Invariant still %inv%.', { remaining: pegs.size, inv: invariant(pegs) }), 'info');
}
function checkTarget() {
// Classic goal — one peg remaining at the center (3,3)
// Center (3,3): r=3, c=3; both 3%3=0, so label=0. Invariant of target = 0.
const targetLabel = 0; // center (3,3) has label 0 — parity of one label-0 peg = 0
const cur = invariant(pegs);
if (cur === targetLabel) setStatus('Invariant = 0 matches the center-peg target (invariant 0) — not ruled out by this invariant!', 'ok');
else setStatus('Invariant mismatch — the center-peg target has invariant 0, but current board has invariant 1. The center goal is provably unreachable from here!', 'bad');
}
function resetBoard() {
pegs.clear();
for (const k of VALID) pegs.add(k);
pegs.delete('3,3');
selected = null; landings = [];
render(); setStatus('Click a peg to select it, then click a highlighted hole to jump.', '');
}
document.getElementById('btn-target').addEventListener('click', checkTarget);
document.getElementById('btn-reset').addEventListener('click', resetBoard);
render();
setStatus('Click a peg to select it, then click a highlighted hole to jump.', '');
<!-- Hint paragraph + invariant display bar + board + controls -->
<p class="hint">Each cell is labeled <b>0</b> (blue) or <b>1</b> (amber) — a cell gets label 1 only when both its row AND column index are not multiples of 3. The <b>invariant</b> is the XOR (parity) of labels of all pegs on the board: it never changes no matter how you jump. Click a peg to select it, then click a green-outlined hole to jump.</p>
<div class="top-bar">
<div class="inv-display">
<span class="inv-label">Invariant (XOR of labels):</span>
<span id="inv-value" class="inv-badge" aria-live="polite">0</span>
</div>
<div class="legend">
<span class="leg-chip leg-zero">Label 0</span>
<span class="leg-chip leg-one">Label 1</span>
</div>
</div>
<div id="board" class="board" role="grid" aria-label="Peg solitaire board, 33 holes in a cross shape"></div>
<div id="status" class="status" aria-live="polite">Click a peg to select it, then click a highlighted hole to jump.</div>
<div class="btns">
<button id="btn-target" type="button">Check center target</button>
<button id="btn-reset" type="button" class="ghost">Reset board</button>
</div>
/* Peg solitaire invariant demo — styles */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.hint { font-size: .85rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.top-bar { display: flex; align-items: center; justify-content: space-between;
margin-bottom: .5rem; flex-wrap: wrap; gap: .4rem; }
.inv-label { font-size: .82rem; color: #555; margin-right: .3rem; }
.inv-badge { font: 700 1rem ui-monospace, monospace;
padding: .15rem .55rem; border-radius: 6px; min-width: 2rem;
display: inline-block; text-align: center; transition: background .2s; }
.inv-badge.inv-0 { background: #d4f0d4; color: #1a5c1a; border: 1px solid #8ecb8e; }
.inv-badge.inv-1 { background: #ffe8c0; color: #7a4500; border: 1px solid #e0b870; }
.legend { display: flex; gap: .4rem; }
.leg-chip { font: 600 .78rem system-ui; padding: .18rem .45rem; border-radius: 5px;
border: 1px solid #bbb; }
.leg-zero { background: #e8eef3; color: #1d3557; border-color: #c2cedb; }
.leg-one { background: #ffe8c0; color: #7a4500; border-color: #e0b870; }
/* 7x7 grid; only the 33 cross-shaped holes are rendered */
.board { display: grid; grid-template-columns: repeat(7, 40px);
grid-template-rows: repeat(7, 40px); gap: 3px; margin: .25rem 0; }
.cell { width: 40px; height: 40px; border-radius: 7px; display: flex;
align-items: center; justify-content: center;
user-select: none; position: relative; }
.cell.empty-border { background: transparent; }
.cell.hole-0 { background: #e8eef3; border: 1px solid #c2cedb; }
.cell.hole-1 { background: #f8f0e0; border: 1px solid #d8c898; }
.cell.peg-0 { background: #1d3557; border: 2px solid #14273e; cursor: pointer; }
.cell.peg-1 { background: #c07000; border: 2px solid #9a5a00; cursor: pointer; }
.cell.peg-0::after, .cell.peg-1::after { content: "●"; font-size: 1.25rem;
color: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-52%); }
.cell.selected { outline: 3px solid #e63946; outline-offset: -2px; }
.cell.landing { outline: 3px solid #0a7d33; outline-offset: -2px; cursor: pointer; }
.cell.landing::after { content: "○" !important; font-size: 1.2rem;
color: #0a7d33 !important; }
.status { font-size: .9rem; font-weight: 600; margin: .4rem 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557;
color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Standard English peg solitaire board — 33 holes in a cross shape
const VALID = new Set([
'0,2','0,3','0,4',
'1,2','1,3','1,4',
'2,0','2,1','2,2','2,3','2,4','2,5','2,6',
'3,0','3,1','3,2','3,3','3,4','3,5','3,6',
'4,0','4,1','4,2','4,3','4,4','4,5','4,6',
'5,2','5,3','5,4',
'6,2','6,3','6,4'
]);
const key = (r,c) => r+','+c;
// GF(2) invariant: label(r,c) = 1 iff r%3 != 0 AND c%3 != 0; else 0
// Every legal jump XORs labels of start, mid, end — net XOR = label(start)^label(mid)^label(end) = 0 for all jumps
const label = (r,c) => ((r%3!==0)&&(c%3!==0)) ? 1 : 0;
// Invariant = XOR of labels of all occupied cells (= parity of count of label-1 pegs)
function invariant(ps) {
let v = 0;
for (const k of ps) { const [r,c] = k.split(',').map(Number); v ^= label(r,c); }
return v;
}
// Standard starting position — all 33 holes filled except the center (3,3)
const pegs = new Set([...VALID]);
pegs.delete('3,3'); // Standard start — center hole empty
let selected = null;
let landings = [];
// A legal jump goes 2 cells orthogonally over an occupied cell into an empty cell
function validLandings(r, c) {
const out = [];
for (const [dr,dc] of [[-2,0],[2,0],[0,-2],[0,2]]) {
const lr=r+dr, lc=c+dc, mr=r+dr/2, mc=c+dc/2;
const lk=key(lr,lc), mk=key(mr,mc);
if (VALID.has(lk) && !pegs.has(lk) && pegs.has(mk)) out.push([lr,lc]);
}
return out;
}
function updateInvDisplay() {
const v = invariant(pegs);
const el = document.getElementById('inv-value');
el.textContent = v;
el.className = 'inv-badge inv-' + v;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.innerHTML = t; s.className = 'status ' + cls;
}
function render() {
const board = document.getElementById('board');
board.innerHTML = '';
for (let r=0; r<7; r++) for (let c=0; c<7; c++) {
const d = document.createElement('div');
const k = key(r,c);
if (!VALID.has(k)) { d.className = 'cell empty-border'; board.appendChild(d); continue; }
const lbl = label(r,c);
const isPeg = pegs.has(k);
const isSel = selected === k;
const isLand = landings.some(([lr,lc]) => lr===r && lc===c);
if (isLand) {
d.className = 'cell landing hole-'+lbl;
d.title = 'Jump here';
d.addEventListener('click', () => doJump(r,c));
} else if (isPeg) {
d.className = 'cell peg-'+lbl + (isSel ? ' selected' : '');
d.title = i18nFmt('Label %lbl% — click to select', { lbl: lbl });
d.addEventListener('click', () => selectPeg(r,c));
} else {
d.className = 'cell hole-'+lbl;
}
board.appendChild(d);
}
updateInvDisplay();
}
function selectPeg(r, c) {
const k = key(r,c);
if (selected === k) { selected=null; landings=[]; render(); setStatus('Click a peg to select it, then click a highlighted hole to jump.',''); return; }
selected = k;
landings = validLandings(r, c);
render();
if (landings.length === 0) setStatus('No legal jumps from this peg.', 'info');
else setStatus(i18nFmt('%n% landing hole(s) available — click a highlighted hole to jump.', { n: landings.length }), 'info');
}
function doJump(lr, lc) {
if (!selected) return;
const [sr,sc] = selected.split(',').map(Number);
const mr = (sr+lr)/2, mc = (sc+lc)/2;
pegs.delete(selected); pegs.delete(key(mr,mc)); pegs.add(key(lr,lc));
selected = null; landings = [];
render();
setStatus(i18nFmt('Jumped! %remaining% peg(s) left. Invariant still %inv%.', { remaining: pegs.size, inv: invariant(pegs) }), 'info');
}
function checkTarget() {
// Classic goal — one peg remaining at the center (3,3)
// Center (3,3): r=3, c=3; both 3%3=0, so label=0. Invariant of target = 0.
const targetLabel = 0; // center (3,3) has label 0 — parity of one label-0 peg = 0
const cur = invariant(pegs);
if (cur === targetLabel) setStatus('Invariant = 0 matches the center-peg target (invariant 0) — not ruled out by this invariant!', 'ok');
else setStatus('Invariant mismatch — the center-peg target has invariant 0, but current board has invariant 1. The center goal is provably unreachable from here!', 'bad');
}
function resetBoard() {
pegs.clear();
for (const k of VALID) pegs.add(k);
pegs.delete('3,3');
selected = null; landings = [];
render(); setStatus('Click a peg to select it, then click a highlighted hole to jump.', '');
}
document.getElementById('btn-target').addEventListener('click', checkTarget);
document.getElementById('btn-reset').addEventListener('click', resetBoard);
render();
setStatus('Click a peg to select it, then click a highlighted hole to jump.', '');
Comments
Loading comments...