357 lines
10 KiB
HTML
357 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Lights Out</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; touch-action: none; font-family: Arial, sans-serif; }
|
|
canvas { display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="game"></canvas>
|
|
<script>
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
let audioCtx = null;
|
|
let musicEnabled = true;
|
|
let sfxEnabled = true;
|
|
let musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (!audioCtx) audioCtx = new AudioContext();
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
}
|
|
|
|
// Relaxing puzzle melody
|
|
const melody = [
|
|
{n: 392, d: 0.5}, {n: 440, d: 0.5}, {n: 494, d: 0.5}, {n: 523, d: 1.0},
|
|
{n: 494, d: 0.5}, {n: 440, d: 0.5}, {n: 392, d: 1.0},
|
|
{n: 330, d: 0.5}, {n: 349, d: 0.5}, {n: 392, d: 0.5}, {n: 440, d: 1.0},
|
|
{n: 392, d: 0.5}, {n: 349, d: 0.5}, {n: 330, d: 1.0}, {n: 0, d: 0.5}
|
|
];
|
|
|
|
const bass = [
|
|
{n: 98, d: 1.0}, {n: 110, d: 1.0}, {n: 131, d: 1.0}, {n: 110, d: 1.0},
|
|
{n: 82, d: 1.0}, {n: 87, d: 1.0}, {n: 98, d: 1.0}, {n: 110, d: 1.0}
|
|
];
|
|
|
|
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
|
|
|
|
function playTone(freq, dur, type = 'triangle', vol = 0.06) {
|
|
if (!audioCtx || !musicEnabled || freq === 0) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = type;
|
|
osc.frequency.value = freq;
|
|
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + dur);
|
|
}
|
|
|
|
function playSfx(type) {
|
|
if (!audioCtx || !sfxEnabled) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
if (type === 'toggle') {
|
|
osc.frequency.value = 440;
|
|
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
|
} else if (type === 'win') {
|
|
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
|
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.15);
|
|
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.3);
|
|
osc.frequency.setValueAtTime(1047, audioCtx.currentTime + 0.45);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.6);
|
|
}
|
|
osc.type = 'square';
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.7);
|
|
}
|
|
|
|
function updateMusic() {
|
|
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
|
const now = audioCtx.currentTime;
|
|
if (now >= nextMelody) {
|
|
const note = melody[melodyIdx];
|
|
playTone(note.n, note.d, 'triangle', 0.05);
|
|
nextMelody = now + note.d;
|
|
melodyIdx = (melodyIdx + 1) % melody.length;
|
|
}
|
|
if (now >= nextBass) {
|
|
const note = bass[bassIdx];
|
|
playTone(note.n, note.d, 'triangle', 0.08);
|
|
nextBass = now + note.d;
|
|
bassIdx = (bassIdx + 1) % bass.length;
|
|
}
|
|
}
|
|
|
|
function startMusic() {
|
|
if (!musicPlaying && musicEnabled) {
|
|
initAudio();
|
|
musicPlaying = true;
|
|
nextMelody = audioCtx.currentTime;
|
|
nextBass = audioCtx.currentTime;
|
|
}
|
|
}
|
|
|
|
const canvas = document.getElementById('game');
|
|
const ctx = canvas.getContext('2d');
|
|
let W, H, safeTop, safeBottom;
|
|
|
|
function resize() {
|
|
W = window.innerWidth;
|
|
H = window.innerHeight;
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
safeTop = H * 0.1;
|
|
safeBottom = H * 0.9;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
const SIZE = 5;
|
|
let grid = [];
|
|
let moves = 0;
|
|
let level = 1;
|
|
let gameOver = false;
|
|
let gameRunning = false;
|
|
|
|
function createGrid() {
|
|
grid = [];
|
|
for (let r = 0; r < SIZE; r++) {
|
|
grid.push(new Array(SIZE).fill(false));
|
|
}
|
|
}
|
|
|
|
function shuffleGrid(numMoves) {
|
|
// Make a solvable puzzle by performing random toggles
|
|
for (let i = 0; i < numMoves; i++) {
|
|
const r = Math.floor(Math.random() * SIZE);
|
|
const c = Math.floor(Math.random() * SIZE);
|
|
toggle(r, c, false); // Don't count these as moves
|
|
}
|
|
}
|
|
|
|
function toggle(row, col, countMove = true) {
|
|
const directions = [[0, 0], [-1, 0], [1, 0], [0, -1], [0, 1]];
|
|
for (const [dr, dc] of directions) {
|
|
const nr = row + dr;
|
|
const nc = col + dc;
|
|
if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE) {
|
|
grid[nr][nc] = !grid[nr][nc];
|
|
}
|
|
}
|
|
if (countMove) {
|
|
moves++;
|
|
playSfx('toggle');
|
|
}
|
|
}
|
|
|
|
function isWin() {
|
|
return grid.every(row => row.every(cell => !cell));
|
|
}
|
|
|
|
function init() {
|
|
createGrid();
|
|
moves = 0;
|
|
gameOver = false;
|
|
gameRunning = true;
|
|
// Easy start - fewer toggles needed
|
|
shuffleGrid(3 + level);
|
|
startMusic();
|
|
}
|
|
|
|
function nextLevel() {
|
|
level++;
|
|
createGrid();
|
|
shuffleGrid(3 + level);
|
|
moves = 0;
|
|
gameOver = false;
|
|
}
|
|
|
|
canvas.addEventListener('click', e => {
|
|
initAudio();
|
|
handleClick(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener('touchstart', e => {
|
|
e.preventDefault();
|
|
initAudio();
|
|
handleClick(e.touches[0].clientX, e.touches[0].clientY);
|
|
});
|
|
|
|
function handleClick(x, y) {
|
|
if (gameOver) {
|
|
if (level < 10) {
|
|
nextLevel();
|
|
} else {
|
|
level = 1;
|
|
init();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Calculate grid position
|
|
const availH = safeBottom - safeTop - 80;
|
|
const availW = W - 40;
|
|
const cellSize = Math.min(availW / SIZE, availH / SIZE);
|
|
const gridSize = cellSize * SIZE;
|
|
const gridX = (W - gridSize) / 2;
|
|
const gridY = safeTop + 60 + (availH - gridSize) / 2;
|
|
|
|
const col = Math.floor((x - gridX) / cellSize);
|
|
const row = Math.floor((y - gridY) / cellSize);
|
|
|
|
if (row >= 0 && row < SIZE && col >= 0 && col < SIZE) {
|
|
toggle(row, col);
|
|
|
|
if (isWin()) {
|
|
gameOver = true;
|
|
playSfx('win');
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', e => {
|
|
initAudio();
|
|
if ((e.key === ' ' || e.key === 'Enter') && gameOver) {
|
|
if (level < 10) nextLevel();
|
|
else { level = 1; init(); }
|
|
}
|
|
if (e.key === 'r' || e.key === 'R') init();
|
|
});
|
|
|
|
function update() {
|
|
updateMusic();
|
|
}
|
|
|
|
function draw() {
|
|
// Background
|
|
ctx.fillStyle = '#1a1a2e';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Safe zones
|
|
ctx.fillStyle = '#16213e';
|
|
ctx.fillRect(0, 0, W, safeTop);
|
|
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
|
|
|
// Header
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 24px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('LIGHTS OUT', W / 2, 40);
|
|
|
|
ctx.font = '18px Arial';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(`Level: ${level}`, 20, 35);
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(`Moves: ${moves}`, W - 20, 35);
|
|
|
|
// Calculate grid
|
|
const availH = safeBottom - safeTop - 80;
|
|
const availW = W - 40;
|
|
const cellSize = Math.min(availW / SIZE, availH / SIZE);
|
|
const gridSize = cellSize * SIZE;
|
|
const gridX = (W - gridSize) / 2;
|
|
const gridY = safeTop + 60 + (availH - gridSize) / 2;
|
|
|
|
// Instructions
|
|
ctx.fillStyle = '#888';
|
|
ctx.font = '16px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('Turn off all the lights!', W / 2, gridY - 15);
|
|
|
|
// Draw grid
|
|
for (let r = 0; r < SIZE; r++) {
|
|
for (let c = 0; c < SIZE; c++) {
|
|
const x = gridX + c * cellSize;
|
|
const y = gridY + r * cellSize;
|
|
const padding = 4;
|
|
|
|
// Cell background
|
|
ctx.fillStyle = '#0f0f23';
|
|
ctx.beginPath();
|
|
ctx.roundRect(x + padding, y + padding, cellSize - padding * 2, cellSize - padding * 2, 8);
|
|
ctx.fill();
|
|
|
|
// Light
|
|
if (grid[r][c]) {
|
|
// Light ON - yellow glow
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.shadowColor = '#ffd700';
|
|
ctx.shadowBlur = 20;
|
|
ctx.beginPath();
|
|
ctx.roundRect(x + padding + 4, y + padding + 4, cellSize - padding * 2 - 8, cellSize - padding * 2 - 8, 6);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
} else {
|
|
// Light OFF - dark
|
|
ctx.fillStyle = '#2a2a4a';
|
|
ctx.beginPath();
|
|
ctx.roundRect(x + padding + 4, y + padding + 4, cellSize - padding * 2 - 8, cellSize - padding * 2 - 8, 6);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count lights on
|
|
const lightsOn = grid.flat().filter(c => c).length;
|
|
ctx.fillStyle = '#888';
|
|
ctx.font = '16px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(`Lights remaining: ${lightsOn}`, W / 2, gridY + gridSize + 30);
|
|
|
|
// Controls hint
|
|
ctx.fillStyle = '#666';
|
|
ctx.font = '14px Arial';
|
|
ctx.fillText('Tap a light to toggle it and its neighbors', W / 2, safeBottom + 25);
|
|
|
|
// Win screen
|
|
if (gameOver) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.font = 'bold 36px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('LEVEL COMPLETE!', W / 2, H / 2 - 50);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = '24px Arial';
|
|
ctx.fillText(`Solved in ${moves} moves`, W / 2, H / 2);
|
|
|
|
ctx.fillStyle = '#ffd700';
|
|
ctx.beginPath();
|
|
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#000';
|
|
ctx.font = 'bold 18px Arial';
|
|
ctx.fillText(level < 10 ? 'NEXT LEVEL' : 'PLAY AGAIN', W / 2, H / 2 + 62);
|
|
}
|
|
}
|
|
|
|
function gameLoop() {
|
|
update();
|
|
draw();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
window.gameScore = 0;
|
|
Object.defineProperty(window, 'gameScore', { get: () => level * 100 - moves });
|
|
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
|
|
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
|
|
init();
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html>
|