Click the lights to set up any pattern you like. Each press toggles a cell and its orthogonal neighbors — that is the real Lights Out rule. Then press Solve and watch the puzzle collapse.
<p class="hint">Click a light to toggle it <b>and its four neighbors</b> — the real Lights Out rule. Then press <b>Solve</b>: a linear-algebra engine over GF(2) finds which buttons to press (highlighted) to turn the whole board off, or proves no solution exists.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Toggle some lights, then Solve.</div>
<div class="btns">
<button id="solve" type="button">Solve</button>
<button id="rand" type="button">Random board</button>
<button id="clear" type="button" class="ghost">All off</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, 50px); gap: 5px; margin: .4rem 0; }
.cell { width: 50px; height: 50px; border-radius: 9px; cursor: pointer; position: relative;
border: 1px solid #adb1b8; background: #2b2f36; transition: all .12s; }
.cell.on { background: #ffd23f; border-color: #e0b400; box-shadow: 0 0 8px rgba(255,210,63,.6); }
.cell:hover { transform: scale(1.05); }
.cell.press::after { content: "↧"; position: absolute; inset: 0; display: flex;
align-items: center; justify-content: center; font: 800 22px system-ui, sans-serif;
color: #e63946; }
.cell.on.press::after { color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 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] ?? ''));
// Lights Out as linear algebra over GF(2). Pressing a button toggles it and its
// 4 orthogonal neighbors. Toggling = addition mod 2 (XOR); pressing twice cancels;
// order is irrelevant. So solving "turn every light off" is a linear system
// A x = b over GF(2),
// where x[i] = press button i (0/1), A[i] has 1s for button i's toggle set, and
// b = current light state. Gaussian elimination mod 2 solves it in polynomial time
// and decides solvability exactly (a 0 = 1 row means no solution).
const N = 5, M = N * N;
let lights = new Array(M).fill(0); // current on/off state
let press = new Array(M).fill(0); // highlighted solution
const idx = (r, c) => r * N + c;
function toggleSet(i) { // button i toggles itself + orthogonal neighbors
const r = (i / N) | 0, c = i % N, s = [i];
if (r > 0) s.push(idx(r - 1, c));
if (r < N - 1) s.push(idx(r + 1, c));
if (c > 0) s.push(idx(r, c - 1));
if (c < N - 1) s.push(idx(r, c + 1));
return s;
}
// Press matrix A: column i = which lights button i flips.
const A = Array.from({ length: M }, () => new Array(M).fill(0));
for (let i = 0; i < M; i++) for (const j of toggleSet(i)) A[j][i] = 1;
// Solve A x = b over GF(2). Returns x array, or null if inconsistent.
function solveGF2(b) {
// Augmented rows: each row = M coefficient bits + 1 rhs bit, packed in an array.
const rows = [];
for (let i = 0; i < M; i++) rows.push(A[i].slice().concat(b[i]));
const where = new Array(M).fill(-1);
let row = 0;
for (let col = 0; col < M && row < M; col++) {
let piv = -1;
for (let r = row; r < M; r++) if (rows[r][col]) { piv = r; break; }
if (piv === -1) continue;
[rows[row], rows[piv]] = [rows[piv], rows[row]];
for (let r = 0; r < M; r++)
if (r !== row && rows[r][col])
for (let k = col; k <= M; k++) rows[r][k] ^= rows[row][k];
where[col] = row; row++;
}
// Inconsistency: a row 0...0 | 1 means no solution.
for (let r = 0; r < M; r++) {
let allZero = true;
for (let k = 0; k < M; k++) if (rows[r][k]) { allZero = false; break; }
if (allZero && rows[r][M]) return null;
}
const x = new Array(M).fill(0);
for (let col = 0; col < M; col++) if (where[col] !== -1) x[col] = rows[where[col]][M];
return x;
}
function render() {
const b = document.getElementById('board'); b.innerHTML = '';
for (let i = 0; i < M; i++) {
const d = document.createElement('div');
d.className = 'cell' + (lights[i] ? ' on' : '') + (press[i] ? ' press' : '');
d.addEventListener('click', () => {
for (const j of toggleSet(i)) lights[j] ^= 1; // a click presses button i
press = new Array(M).fill(0);
render(); setStatus('Toggle some lights, then Solve.', '');
});
b.appendChild(d);
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
document.getElementById('solve').addEventListener('click', () => {
const x = solveGF2(lights.slice());
if (!x) { press = new Array(M).fill(0); render(); setStatus('✗ This board has no solution (the equations contradict).', 'bad'); return; }
press = x;
const k = x.reduce((a, v) => a + v, 0);
render();
setStatus(fmt('✓ Solved by Gaussian elimination — press the %k% marked buttons.', { k: k }), 'ok');
console.log('Press set (1 = press):', x.join(''));
});
document.getElementById('rand').addEventListener('click', () => {
// A board built by random presses is always solvable; this guarantees a fun, solvable start.
lights = new Array(M).fill(0);
for (let i = 0; i < M; i++) if (Math.random() < 0.5) for (const j of toggleSet(i)) lights[j] ^= 1;
press = new Array(M).fill(0); render(); setStatus('Random solvable board. Press Solve.', '');
});
document.getElementById('clear').addEventListener('click', () => {
lights = new Array(M).fill(0); press = new Array(M).fill(0); render(); setStatus('All off. Toggle some lights, then Solve.', '');
});
// Start with a small solvable pattern.
for (const j of toggleSet(idx(2, 2))) lights[j] ^= 1;
for (const j of toggleSet(idx(0, 0))) lights[j] ^= 1;
render();
<p class="hint">Click a light to toggle it <b>and its four neighbors</b> — the real Lights Out rule. Then press <b>Solve</b>: a linear-algebra engine over GF(2) finds which buttons to press (highlighted) to turn the whole board off, or proves no solution exists.</p>
<div id="board" class="board"></div>
<div class="status" id="status">Toggle some lights, then Solve.</div>
<div class="btns">
<button id="solve" type="button">Solve</button>
<button id="rand" type="button">Random board</button>
<button id="clear" type="button" class="ghost">All off</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, 50px); gap: 5px; margin: .4rem 0; }
.cell { width: 50px; height: 50px; border-radius: 9px; cursor: pointer; position: relative;
border: 1px solid #adb1b8; background: #2b2f36; transition: all .12s; }
.cell.on { background: #ffd23f; border-color: #e0b400; box-shadow: 0 0 8px rgba(255,210,63,.6); }
.cell:hover { transform: scale(1.05); }
.cell.press::after { content: "↧"; position: absolute; inset: 0; display: flex;
align-items: center; justify-content: center; font: 800 22px system-ui, sans-serif;
color: #e63946; }
.cell.on.press::after { color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 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] ?? ''));
// Lights Out as linear algebra over GF(2). Pressing a button toggles it and its
// 4 orthogonal neighbors. Toggling = addition mod 2 (XOR); pressing twice cancels;
// order is irrelevant. So solving "turn every light off" is a linear system
// A x = b over GF(2),
// where x[i] = press button i (0/1), A[i] has 1s for button i's toggle set, and
// b = current light state. Gaussian elimination mod 2 solves it in polynomial time
// and decides solvability exactly (a 0 = 1 row means no solution).
const N = 5, M = N * N;
let lights = new Array(M).fill(0); // current on/off state
let press = new Array(M).fill(0); // highlighted solution
const idx = (r, c) => r * N + c;
function toggleSet(i) { // button i toggles itself + orthogonal neighbors
const r = (i / N) | 0, c = i % N, s = [i];
if (r > 0) s.push(idx(r - 1, c));
if (r < N - 1) s.push(idx(r + 1, c));
if (c > 0) s.push(idx(r, c - 1));
if (c < N - 1) s.push(idx(r, c + 1));
return s;
}
// Press matrix A: column i = which lights button i flips.
const A = Array.from({ length: M }, () => new Array(M).fill(0));
for (let i = 0; i < M; i++) for (const j of toggleSet(i)) A[j][i] = 1;
// Solve A x = b over GF(2). Returns x array, or null if inconsistent.
function solveGF2(b) {
// Augmented rows: each row = M coefficient bits + 1 rhs bit, packed in an array.
const rows = [];
for (let i = 0; i < M; i++) rows.push(A[i].slice().concat(b[i]));
const where = new Array(M).fill(-1);
let row = 0;
for (let col = 0; col < M && row < M; col++) {
let piv = -1;
for (let r = row; r < M; r++) if (rows[r][col]) { piv = r; break; }
if (piv === -1) continue;
[rows[row], rows[piv]] = [rows[piv], rows[row]];
for (let r = 0; r < M; r++)
if (r !== row && rows[r][col])
for (let k = col; k <= M; k++) rows[r][k] ^= rows[row][k];
where[col] = row; row++;
}
// Inconsistency: a row 0...0 | 1 means no solution.
for (let r = 0; r < M; r++) {
let allZero = true;
for (let k = 0; k < M; k++) if (rows[r][k]) { allZero = false; break; }
if (allZero && rows[r][M]) return null;
}
const x = new Array(M).fill(0);
for (let col = 0; col < M; col++) if (where[col] !== -1) x[col] = rows[where[col]][M];
return x;
}
function render() {
const b = document.getElementById('board'); b.innerHTML = '';
for (let i = 0; i < M; i++) {
const d = document.createElement('div');
d.className = 'cell' + (lights[i] ? ' on' : '') + (press[i] ? ' press' : '');
d.addEventListener('click', () => {
for (const j of toggleSet(i)) lights[j] ^= 1; // a click presses button i
press = new Array(M).fill(0);
render(); setStatus('Toggle some lights, then Solve.', '');
});
b.appendChild(d);
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
document.getElementById('solve').addEventListener('click', () => {
const x = solveGF2(lights.slice());
if (!x) { press = new Array(M).fill(0); render(); setStatus('✗ This board has no solution (the equations contradict).', 'bad'); return; }
press = x;
const k = x.reduce((a, v) => a + v, 0);
render();
setStatus(fmt('✓ Solved by Gaussian elimination — press the %k% marked buttons.', { k: k }), 'ok');
console.log('Press set (1 = press):', x.join(''));
});
document.getElementById('rand').addEventListener('click', () => {
// A board built by random presses is always solvable; this guarantees a fun, solvable start.
lights = new Array(M).fill(0);
for (let i = 0; i < M; i++) if (Math.random() < 0.5) for (const j of toggleSet(i)) lights[j] ^= 1;
press = new Array(M).fill(0); render(); setStatus('Random solvable board. Press Solve.', '');
});
document.getElementById('clear').addEventListener('click', () => {
lights = new Array(M).fill(0); press = new Array(M).fill(0); render(); setStatus('All off. Toggle some lights, then Solve.', '');
});
// Start with a small solvable pattern.
for (const j of toggleSet(idx(2, 2))) lights[j] ^= 1;
for (const j of toggleSet(idx(0, 0))) lights[j] ^= 1;
render();
The solver isn't searching or guessing. It writes one equation per cell — the on/off state must flip an odd number of times — and runs Gaussian elimination mod 2. The press cells it highlights are a guaranteed solution, found in a fixed number of steps. If you build a board it can't fix, it tells you so with certainty: some boards are genuinely unsolvable, and the same math proves it.
Comments
Loading comments...