Here is a small Slitherlink grid. Click the dashes between the dots to switch edges on and off, and try to draw a single closed loop so that every number matches the count of loop edges around its cell.
<p class="hint">Click the dashes between dots to toggle loop edges. Each number says how many of its four surrounding edges are part of the loop. Build <b>one single closed loop</b> with no branches or crossings that satisfies every clue. Checking a drawing is instant — finding the loop is the hard part.</p>
<div id="grid" class="grid"></div>
<div class="status" id="status">Toggle edges, then check.</div>
<div class="btns">
<button id="check" type="button">Check loop</button>
<button id="solve" type="button">Find the loop</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 .8rem; line-height: 1.45; }
.grid { display: inline-grid; gap: 0; margin: .4rem 0; }
.dot { width: 9px; height: 9px; border-radius: 50%; background: #1d3557; justify-self: center; align-self: center; }
.h-edge, .v-edge { background: transparent; cursor: pointer; transition: background .1s; }
.h-edge { width: 44px; height: 9px; align-self: center; justify-self: center; border-radius: 5px; }
.v-edge { width: 9px; height: 44px; align-self: center; justify-self: center; border-radius: 5px; }
.h-edge:hover, .v-edge:hover { background: #c9ccd1; }
.h-edge.on, .v-edge.on { background: #e63946; }
.cell { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 18px ui-monospace, monospace; color: #1d3557; }
.cell.blank { color: transparent; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 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] ?? ''));
// Slitherlink: on a grid of dots, draw a single closed loop along the dashes.
// Each numbered cell counts how many of its 4 surrounding edges are on the loop.
// A blank cell has no constraint. Checking a candidate is a quick scan; deciding
// whether ANY satisfying single loop exists is NP-complete (Yato & Seta, 2003).
const R = 3, C = 3; // cells: 3x3 -> 4x4 dots
// Clues (null = blank). This board has a unique single-loop solution.
const CLUES = [
[null, 2, null],
[3, 2, 1 ],
[null, 0, null]
];
// Edge model: horizontal edges H[r][c] for r in 0..R (rows of dots), c in 0..C-1.
// vertical edges V[r][c] for r in 0..R-1, c in 0..C.
const H = [], V = [];
for (let r = 0; r <= R; r++) { H[r] = []; for (let c = 0; c < C; c++) H[r][c] = 0; }
for (let r = 0; r < R; r++) { V[r] = []; for (let c = 0; c <= C; c++) V[r][c] = 0; }
function cellEdges(r, c) { // the 4 edges of cell (r,c)
return [['H',r,c],['H',r+1,c],['V',r,c],['V',r,c+1]];
}
function get(t,r,c){ return t==='H'?H[r][c]:V[r][c]; }
function set(t,r,c,v){ if(t==='H')H[r][c]=v; else V[r][c]=v; }
// ---- validity check ----
// (1) every clue equals the number of its on-edges
// (2) every dot touches 0 or 2 on-edges (degree)
// (3) the on-edges form exactly ONE loop (one connected component, no stray)
function dotDegree(r, c) { // dot at (r,c), r in 0..R, c in 0..C
let d = 0;
if (c < C && H[r][c]) d++; // right
if (c > 0 && H[r][c-1]) d++; // left
if (r < R && V[r][c]) d++; // down
if (r > 0 && V[r-1][c]) d++; // up
return d;
}
function valid() {
// clues
for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
if (CLUES[r][c] == null) continue;
const k = cellEdges(r,c).reduce((s,[t,a,b]) => s + get(t,a,b), 0);
if (k !== CLUES[r][c]) return false;
}
// degrees 0 or 2
let onEdges = 0, startDot = null;
for (let r = 0; r <= R; r++) for (let c = 0; c <= C; c++) {
const d = dotDegree(r,c);
if (d !== 0 && d !== 2) return false;
if (d === 2 && !startDot) startDot = [r,c];
}
for (let r = 0; r <= R; r++) for (let c = 0; c < C; c++) onEdges += H[r][c];
for (let r = 0; r < R; r++) for (let c = 0; c <= C; c++) onEdges += V[r][c];
if (onEdges === 0) return false; // empty is not a loop
// single component: walk from startDot, count edges reached
const seen = new Set();
let reached = 0;
const stack = [startDot];
const id = (r,c) => r + ',' + c;
const visitedDots = new Set([id(startDot[0],startDot[1])]);
while (stack.length) {
const [r,c] = stack.pop();
const nbrs = [];
if (c < C && H[r][c]) nbrs.push([r, c+1]);
if (c > 0 && H[r][c-1]) nbrs.push([r, c-1]);
if (r < R && V[r][c]) nbrs.push([r+1, c]);
if (r > 0 && V[r-1][c]) nbrs.push([r-1, c]);
for (const [nr,nc] of nbrs) {
reached++; // counts each edge twice (both ends)
if (!visitedDots.has(id(nr,nc))) { visitedDots.add(id(nr,nc)); stack.push([nr,nc]); }
}
}
// reached/2 = edges in the component; must equal total on-edges => one loop
return reached / 2 === onEdges;
}
// ---- exhaustive solver (the "hard part"): try every subset of edges ----
function allEdges(){ const e=[];
for (let r=0;r<=R;r++) for (let c=0;c<C;c++) e.push(['H',r,c]);
for (let r=0;r<R;r++) for (let c=0;c<=C;c++) e.push(['V',r,c]);
return e; }
function solve() {
const E = allEdges(); let steps = 0, found = null;
for (let mask = 0; mask < (1 << E.length); mask++) { steps++;
E.forEach(([t,r,c],i) => set(t,r,c,(mask >> i) & 1));
if (valid()) { found = mask; break; }
}
return { steps, found, total: 1 << E.length };
}
// ---- rendering ----
const grid = document.getElementById('grid');
function render() {
grid.style.gridTemplateColumns = `repeat(${2*C+1}, auto)`;
grid.innerHTML = '';
for (let gr = 0; gr <= 2*R; gr++) {
for (let gc = 0; gc <= 2*C; gc++) {
const el = document.createElement('div');
const evenR = gr % 2 === 0, evenC = gc % 2 === 0;
if (evenR && evenC) { el.className = 'dot'; }
else if (evenR && !evenC) { // horizontal edge
const r = gr/2, c = (gc-1)/2;
el.className = 'h-edge' + (H[r][c] ? ' on' : '');
el.addEventListener('click', () => { H[r][c] ^= 1; reset(false); render(); });
} else if (!evenR && evenC) { // vertical edge
const r = (gr-1)/2, c = gc/2;
el.className = 'v-edge' + (V[r][c] ? ' on' : '');
el.addEventListener('click', () => { V[r][c] ^= 1; reset(false); render(); });
} else { // cell center (clue)
const r = (gr-1)/2, c = (gc-1)/2; const v = CLUES[r][c];
el.className = 'cell' + (v == null ? ' blank' : '');
el.textContent = v == null ? '·' : v;
}
grid.appendChild(el);
}
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function clearEdges() {
for (let r=0;r<=R;r++) for (let c=0;c<C;c++) H[r][c]=0;
for (let r=0;r<R;r++) for (let c=0;c<=C;c++) V[r][c]=0;
}
function reset(full) { if (full) { clearEdges(); render(); } setStatus(full ? 'Toggle edges, then check.' : '', ''); }
document.getElementById('check').addEventListener('click', () => {
if (valid()) setStatus('✓ One closed loop and every clue satisfied!', 'ok');
else setStatus('✗ Not a valid single loop yet — keep deducing.', 'bad');
});
document.getElementById('solve').addEventListener('click', () => {
const { steps, found, total } = solve();
if (found != null) { render();
setStatus(fmt('Found the loop after checking %steps% of %total% edge subsets.', { steps: steps, total: total }), 'ok');
console.log('Solved by brute force: checked', steps, 'of', total, 'subsets');
} else { clearEdges(); render(); setStatus('No single loop satisfies these clues.', 'bad'); }
});
document.getElementById('reset').addEventListener('click', () => reset(true));
render();
<p class="hint">Click the dashes between dots to toggle loop edges. Each number says how many of its four surrounding edges are part of the loop. Build <b>one single closed loop</b> with no branches or crossings that satisfies every clue. Checking a drawing is instant — finding the loop is the hard part.</p>
<div id="grid" class="grid"></div>
<div class="status" id="status">Toggle edges, then check.</div>
<div class="btns">
<button id="check" type="button">Check loop</button>
<button id="solve" type="button">Find the loop</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 .8rem; line-height: 1.45; }
.grid { display: inline-grid; gap: 0; margin: .4rem 0; }
.dot { width: 9px; height: 9px; border-radius: 50%; background: #1d3557; justify-self: center; align-self: center; }
.h-edge, .v-edge { background: transparent; cursor: pointer; transition: background .1s; }
.h-edge { width: 44px; height: 9px; align-self: center; justify-self: center; border-radius: 5px; }
.v-edge { width: 9px; height: 44px; align-self: center; justify-self: center; border-radius: 5px; }
.h-edge:hover, .v-edge:hover { background: #c9ccd1; }
.h-edge.on, .v-edge.on { background: #e63946; }
.cell { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
font: 700 18px ui-monospace, monospace; color: #1d3557; }
.cell.blank { color: transparent; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 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] ?? ''));
// Slitherlink: on a grid of dots, draw a single closed loop along the dashes.
// Each numbered cell counts how many of its 4 surrounding edges are on the loop.
// A blank cell has no constraint. Checking a candidate is a quick scan; deciding
// whether ANY satisfying single loop exists is NP-complete (Yato & Seta, 2003).
const R = 3, C = 3; // cells: 3x3 -> 4x4 dots
// Clues (null = blank). This board has a unique single-loop solution.
const CLUES = [
[null, 2, null],
[3, 2, 1 ],
[null, 0, null]
];
// Edge model: horizontal edges H[r][c] for r in 0..R (rows of dots), c in 0..C-1.
// vertical edges V[r][c] for r in 0..R-1, c in 0..C.
const H = [], V = [];
for (let r = 0; r <= R; r++) { H[r] = []; for (let c = 0; c < C; c++) H[r][c] = 0; }
for (let r = 0; r < R; r++) { V[r] = []; for (let c = 0; c <= C; c++) V[r][c] = 0; }
function cellEdges(r, c) { // the 4 edges of cell (r,c)
return [['H',r,c],['H',r+1,c],['V',r,c],['V',r,c+1]];
}
function get(t,r,c){ return t==='H'?H[r][c]:V[r][c]; }
function set(t,r,c,v){ if(t==='H')H[r][c]=v; else V[r][c]=v; }
// ---- validity check ----
// (1) every clue equals the number of its on-edges
// (2) every dot touches 0 or 2 on-edges (degree)
// (3) the on-edges form exactly ONE loop (one connected component, no stray)
function dotDegree(r, c) { // dot at (r,c), r in 0..R, c in 0..C
let d = 0;
if (c < C && H[r][c]) d++; // right
if (c > 0 && H[r][c-1]) d++; // left
if (r < R && V[r][c]) d++; // down
if (r > 0 && V[r-1][c]) d++; // up
return d;
}
function valid() {
// clues
for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
if (CLUES[r][c] == null) continue;
const k = cellEdges(r,c).reduce((s,[t,a,b]) => s + get(t,a,b), 0);
if (k !== CLUES[r][c]) return false;
}
// degrees 0 or 2
let onEdges = 0, startDot = null;
for (let r = 0; r <= R; r++) for (let c = 0; c <= C; c++) {
const d = dotDegree(r,c);
if (d !== 0 && d !== 2) return false;
if (d === 2 && !startDot) startDot = [r,c];
}
for (let r = 0; r <= R; r++) for (let c = 0; c < C; c++) onEdges += H[r][c];
for (let r = 0; r < R; r++) for (let c = 0; c <= C; c++) onEdges += V[r][c];
if (onEdges === 0) return false; // empty is not a loop
// single component: walk from startDot, count edges reached
const seen = new Set();
let reached = 0;
const stack = [startDot];
const id = (r,c) => r + ',' + c;
const visitedDots = new Set([id(startDot[0],startDot[1])]);
while (stack.length) {
const [r,c] = stack.pop();
const nbrs = [];
if (c < C && H[r][c]) nbrs.push([r, c+1]);
if (c > 0 && H[r][c-1]) nbrs.push([r, c-1]);
if (r < R && V[r][c]) nbrs.push([r+1, c]);
if (r > 0 && V[r-1][c]) nbrs.push([r-1, c]);
for (const [nr,nc] of nbrs) {
reached++; // counts each edge twice (both ends)
if (!visitedDots.has(id(nr,nc))) { visitedDots.add(id(nr,nc)); stack.push([nr,nc]); }
}
}
// reached/2 = edges in the component; must equal total on-edges => one loop
return reached / 2 === onEdges;
}
// ---- exhaustive solver (the "hard part"): try every subset of edges ----
function allEdges(){ const e=[];
for (let r=0;r<=R;r++) for (let c=0;c<C;c++) e.push(['H',r,c]);
for (let r=0;r<R;r++) for (let c=0;c<=C;c++) e.push(['V',r,c]);
return e; }
function solve() {
const E = allEdges(); let steps = 0, found = null;
for (let mask = 0; mask < (1 << E.length); mask++) { steps++;
E.forEach(([t,r,c],i) => set(t,r,c,(mask >> i) & 1));
if (valid()) { found = mask; break; }
}
return { steps, found, total: 1 << E.length };
}
// ---- rendering ----
const grid = document.getElementById('grid');
function render() {
grid.style.gridTemplateColumns = `repeat(${2*C+1}, auto)`;
grid.innerHTML = '';
for (let gr = 0; gr <= 2*R; gr++) {
for (let gc = 0; gc <= 2*C; gc++) {
const el = document.createElement('div');
const evenR = gr % 2 === 0, evenC = gc % 2 === 0;
if (evenR && evenC) { el.className = 'dot'; }
else if (evenR && !evenC) { // horizontal edge
const r = gr/2, c = (gc-1)/2;
el.className = 'h-edge' + (H[r][c] ? ' on' : '');
el.addEventListener('click', () => { H[r][c] ^= 1; reset(false); render(); });
} else if (!evenR && evenC) { // vertical edge
const r = (gr-1)/2, c = gc/2;
el.className = 'v-edge' + (V[r][c] ? ' on' : '');
el.addEventListener('click', () => { V[r][c] ^= 1; reset(false); render(); });
} else { // cell center (clue)
const r = (gr-1)/2, c = (gc-1)/2; const v = CLUES[r][c];
el.className = 'cell' + (v == null ? ' blank' : '');
el.textContent = v == null ? '·' : v;
}
grid.appendChild(el);
}
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function clearEdges() {
for (let r=0;r<=R;r++) for (let c=0;c<C;c++) H[r][c]=0;
for (let r=0;r<R;r++) for (let c=0;c<=C;c++) V[r][c]=0;
}
function reset(full) { if (full) { clearEdges(); render(); } setStatus(full ? 'Toggle edges, then check.' : '', ''); }
document.getElementById('check').addEventListener('click', () => {
if (valid()) setStatus('✓ One closed loop and every clue satisfied!', 'ok');
else setStatus('✗ Not a valid single loop yet — keep deducing.', 'bad');
});
document.getElementById('solve').addEventListener('click', () => {
const { steps, found, total } = solve();
if (found != null) { render();
setStatus(fmt('Found the loop after checking %steps% of %total% edge subsets.', { steps: steps, total: total }), 'ok');
console.log('Solved by brute force: checked', steps, 'of', total, 'subsets');
} else { clearEdges(); render(); setStatus('No single loop satisfies these clues.', 'bad'); }
});
document.getElementById('reset').addEventListener('click', () => reset(true));
render();
Notice the asymmetry. Checking a finished drawing is effortless: count the edges around each number, confirm every dot has either zero or two edges, and confirm the on-edges form one connected loop. Finding the loop is the hard part — press Find the loop and the computer simply tries every subset of edges. This tiny board already has 24 edges, so brute force walks through 224 ≈ 16 million possibilities; add a few rows and the count explodes.
Comments
Loading comments...