32
Lights Out
tap any tile
idle
120 lines ยท vanilla
view source
// Lights Out โ classic 5x5. Tap a tile to toggle it + its 4 orthogonal neighbors.
// Start from a solvable scramble (~10 random clicks from solved). Win = all off.
const N = 5;
let grid; // Uint8Array N*N, 1=on
let moves; // move counter
let won;
let cellSize; // px per tile (recomputed each frame)
let originX, originY; // top-left of grid
let pulse; // last-tapped indicator (i, t)
function scramble() {
grid = new Uint8Array(N * N); // start solved (all off)
// Apply ~10 random valid presses โ guarantees solvable (involutive)
const presses = 10;
for (let k = 0; k < presses; k++) {
const r = (Math.random() * N) | 0;
const c = (Math.random() * N) | 0;
press(r, c);
}
// Extremely unlikely all-off after scramble; if so, redo
let any = false;
for (let i = 0; i < grid.length; i++) if (grid[i]) { any = true; break; }
if (!any) press(2, 2);
moves = 0;
won = false;
}
function press(r, c) {
toggle(r, c);
toggle(r - 1, c);
toggle(r + 1, c);
toggle(r, c - 1);
toggle(r, c + 1);
}
function toggle(r, c) {
if (r < 0 || r >= N || c < 0 || c >= N) return;
grid[r * N + c] ^= 1;
}
function checkWin() {
for (let i = 0; i < grid.length; i++) if (grid[i]) return false;
return true;
}
function init() {
scramble();
pulse = { i: -1, t: 0 };
}
function layout(width, height) {
// Reserve top strip for HUD (24px) and bottom strip for hint (20px)
const padTop = 32, padBot = 24, padSide = 12;
const availW = Math.max(40, width - padSide * 2);
const availH = Math.max(40, height - padTop - padBot);
const side = Math.min(availW, availH);
cellSize = Math.floor(side / N);
const gridSize = cellSize * N;
originX = ((width - gridSize) / 2) | 0;
originY = padTop + (((availH - gridSize) / 2) | 0);
}
function hitTest(x, y) {
const c = Math.floor((x - originX) / cellSize);
const r = Math.floor((y - originY) / cellSize);
if (r < 0 || r >= N || c < 0 || c >= N) return null;
return { r, c };
}
function tick({ ctx, dt, width, height, input }) {
layout(width, height);
const clicks = input.consumeClicks();
if (input.justPressed && input.justPressed("r")) {
scramble();
pulse = { i: -1, t: 0 };
}
for (const click of clicks) {
const hit = hitTest(click.x, click.y);
if (!hit) continue;
if (won) { scramble(); pulse = { i: -1, t: 0 }; continue; }
press(hit.r, hit.c);
moves++;
pulse = { i: hit.r * N + hit.c, t: 0.35 };
if (checkWin()) won = true;
}
if (pulse.t > 0) pulse.t = Math.max(0, pulse.t - dt);
draw(ctx, width, height);
}
function draw(ctx, width, height) {
// Background
ctx.fillStyle = "#0a0a14";
ctx.fillRect(0, 0, width, height);
// HUD
ctx.fillStyle = "#9aa0b4";
ctx.font = "13px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.fillText(`Moves ${moves}`, 10, 18);
ctx.textAlign = "right";
let lit = 0;
for (let i = 0; i < grid.length; i++) if (grid[i]) lit++;
ctx.fillText(`Lit ${lit}/${N * N}`, width - 10, 18);
// Grid
const g = cellSize * N;
ctx.fillStyle = "#15182a";
ctx.fillRect(originX - 2, originY - 2, g + 4, g + 4);
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const x = originX + c * cellSize;
const y = originY + r * cellSize;
const on = grid[r * N + c] === 1;
const inset = 2;
if (on) {
// Glowy yellow
ctx.fillStyle = "#ffcc00";
ctx.fillRect(x + inset, y + inset, cellSize - inset * 2, cellSize - inset * 2);
// Inner highlight
ctx.fillStyle = "rgba(255,255,255,0.18)";
ctx.fillRect(x + inset, y + inset, cellSize - inset * 2, Math.max(2, (cellSize - inset * 2) * 0.35));
} else {
ctx.fillStyle = "#1f2238";
ctx.fillRect(x + inset, y + inset, cellSize - inset * 2, cellSize - inset * 2);
}
// Pulse ring on last-pressed tile
if (pulse.t > 0 && pulse.i === r * N + c) {
const a = pulse.t / 0.35;
ctx.strokeStyle = `rgba(255,255,255,${a.toFixed(3)})`;
ctx.lineWidth = 2;
ctx.strokeRect(x + inset, y + inset, cellSize - inset * 2, cellSize - inset * 2);
}
}
}
// Hint / win line
ctx.fillStyle = won ? "#34c759" : "#6a6f88";
ctx.font = won ? "bold 14px system-ui, sans-serif" : "12px system-ui, sans-serif";
ctx.textAlign = "center";
const msg = won
? `Solved in ${moves} โ tap to reshuffle`
: "tap a tile ยท R reshuffles";
ctx.fillText(msg, width / 2, height - 8);
}
Comments (2)
Log in to comment.