/** * Auto-Snake: AI-Powered Snake Game * Implements Advanced Pathfinding (BFS + Longest Path Survival) * to ensure the snake fills the grid and avoids infinite loops. */ const CONFIG = { GRID_SIZE: 20, TILE_SIZE: 0, COLORS: { BG: '#020617', SNAKE_HEAD: '#4ade80', SNAKE_BODY: '#22c55e', SNAKE_BODY_GRADIENT: '#166534', APPLE: '#ef4444', APPLE_SHINE: '#fca5a5', WALL: '#475569', GRID: 'rgba(56, 189, 248, 0.05)' } }; class Game { constructor() { this.canvas = document.getElementById('gameCanvas'); this.ctx = this.canvas.getContext('2d'); this.winCount = parseInt(localStorage.getItem('winCount') || '0'); this.bestScore = parseInt(localStorage.getItem('bestScore') || '0'); this.lastWinTime = parseInt(localStorage.getItem('lastWinTime') || Date.now()); this.speedRange = document.getElementById('speedRange'); this.winDisplay = document.getElementById('winCount'); this.timerDisplay = document.getElementById('timer'); this.scoreDisplay = document.getElementById('currentScore'); this.bestScoreDisplay = document.getElementById('bestScore'); this.resetBtn = document.getElementById('resetBtn'); this.toggleMazeBtn = document.getElementById('toggleMazeBtn'); this.overlay = document.getElementById('gameOverlay'); this.snake = []; this.apple = { x: 0, y: 0 }; this.mazeEnabled = false; this.walls = []; this.isGameOver = false; this.isWin = false; this.frameCount = 0; this.lastFrameTime = 0; this.init(); this.setupEventListeners(); this.reset(); this.gameLoop(0); } init() { const resize = () => { const container = this.canvas.parentElement; const size = Math.min(container.clientWidth, 600); this.canvas.width = size; this.canvas.height = size; CONFIG.TILE_SIZE = size / CONFIG.GRID_SIZE; }; window.addEventListener('resize', resize); resize(); this.winDisplay.textContent = this.winCount; this.bestScoreDisplay.textContent = this.bestScore; this.updateTimer(); setInterval(() => this.updateTimer(), 1000); } setupEventListeners() { this.resetBtn.addEventListener('click', () => { this.winCount = 0; this.bestScore = 0; localStorage.setItem('winCount', '0'); localStorage.setItem('bestScore', '0'); this.winDisplay.textContent = '0'; this.bestScoreDisplay.textContent = '0'; this.reset(); }); this.toggleMazeBtn.addEventListener('click', () => { this.mazeEnabled = !this.mazeEnabled; this.reset(); }); } reset() { const mid = Math.floor(CONFIG.GRID_SIZE / 2); this.snake = [ { x: mid, y: mid }, { x: mid, y: mid + 1 }, { x: mid, y: mid + 2 } ]; this.isGameOver = false; this.isWin = false; this.overlay.classList.add('hidden'); this.generateMaze(); this.spawnApple(); this.scoreDisplay.textContent = '0'; } generateMaze() { this.walls = []; if (!this.mazeEnabled) return; const padding = 4; const length = 8; for (let i = padding; i < padding + length; i++) { this.walls.push({ x: i, y: padding }); this.walls.push({ x: CONFIG.GRID_SIZE - 1 - i, y: CONFIG.GRID_SIZE - 1 - padding }); this.walls.push({ x: padding, y: CONFIG.GRID_SIZE - 1 - i }); this.walls.push({ x: CONFIG.GRID_SIZE - 1 - padding, y: i }); } } spawnApple() { let possible = []; for (let y = 0; y < CONFIG.GRID_SIZE; y++) { for (let x = 0; x < CONFIG.GRID_SIZE; x++) { if (!this.isOccupied(x, y)) possible.push({ x, y }); } } if (possible.length === 0) { this.handleWin(); return; } this.apple = possible[Math.floor(Math.random() * possible.length)]; } isOccupied(x, y, ignoreTail = false) { if (x < 0 || x >= CONFIG.GRID_SIZE || y < 0 || y >= CONFIG.GRID_SIZE) return true; const snakeToCheck = ignoreTail ? this.snake.slice(0, -1) : this.snake; if (snakeToCheck.some(s => s.x === x && s.y === y)) return true; if (this.walls.some(w => w.x === x && w.y === y)) return true; return false; } updateTimer() { const diff = Math.floor((Date.now() - this.lastWinTime) / 1000); const mins = Math.floor(diff / 60).toString().padStart(2, '0'); const secs = (diff % 60).toString().padStart(2, '0'); this.timerDisplay.textContent = `${mins}:${secs}`; } handleWin() { this.isWin = true; this.winCount++; this.lastWinTime = Date.now(); localStorage.setItem('winCount', this.winCount); localStorage.setItem('lastWinTime', this.lastWinTime); this.winDisplay.textContent = this.winCount; this.overlay.classList.remove('hidden'); document.getElementById('overlayTitle').textContent = 'VICTORY!'; document.getElementById('overlayMessage').textContent = 'Perfect cycle complete. Restarting...'; setTimeout(() => this.reset(), 5000); } handleGameOver() { this.isGameOver = true; this.overlay.classList.remove('hidden'); document.getElementById('overlayTitle').textContent = 'RECALCULATING'; document.getElementById('overlayMessage').textContent = 'Wait, adjusting strategy...'; setTimeout(() => this.reset(), 1000); } bfs(start, target, ignoreTail = false) { const queue = [[start]]; const visited = new Set(); visited.add(`${start.x},${start.y}`); while (queue.length > 0) { const path = queue.shift(); const curr = path[path.length - 1]; if (curr.x === target.x && curr.y === target.y) return path; const neighbors = [ { x: curr.x, y: curr.y - 1 }, { x: curr.x, y: curr.y + 1 }, { x: curr.x - 1, y: curr.y }, { x: curr.x + 1, y: curr.y } ]; for (const n of neighbors) { if (!this.isOccupied(n.x, n.y, ignoreTail) && !visited.has(`${n.x},${n.y}`)) { visited.add(`${n.x},${n.y}`); queue.push([...path, n]); } } } return null; } getLongestPath(start, target) { let path = this.bfs(start, target, true); if (!path) return null; let extended = true; while (extended) { extended = false; for (let i = 0; i < path.length - 1; i++) { const p1 = path[i]; const p2 = path[i+1]; const dirs = [ { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 } ]; for (const d of dirs) { const n1 = { x: p1.x + d.x, y: p1.y + d.y }; const n2 = { x: p2.x + d.x, y: p2.y + d.y }; if (!this.isOccupied(n1.x, n1.y, true) && !this.isOccupied(n2.x, n2.y, true) && !path.some(p => p.x === n1.x && p.y === n1.y) && !path.some(p => p.x === n2.x && p.y === n2.y)) { path.splice(i + 1, 0, n1, n2); extended = true; break; } } if (extended) break; } } return path; } canReachTail(virtualSnake) { if (virtualSnake.length < 2) return true; const head = virtualSnake[0]; const tail = virtualSnake[virtualSnake.length - 1]; const originalSnake = this.snake; this.snake = virtualSnake; const path = this.bfs(head, tail, true); this.snake = originalSnake; return !!path; } update() { if (this.isGameOver || this.isWin) return; const head = this.snake[0]; const tail = this.snake[this.snake.length - 1]; // 1. Path to apple let path = this.bfs(head, this.apple, true); let nextMove = null; if (path && path.length > 1) { const virtualSnake = [path[1], ...this.snake]; if (!(path[1].x === this.apple.x && path[1].y === this.apple.y)) { virtualSnake.pop(); } if (this.canReachTail(virtualSnake)) { nextMove = path[1]; } } // 2. Fallback: Longest path to tail if (!nextMove) { const pathToTail = this.getLongestPath(head, tail); if (pathToTail && pathToTail.length > 1) { nextMove = pathToTail[1]; } else { nextMove = this.getSurvivalMove(); } } if (!nextMove) { this.handleGameOver(); return; } this.snake.unshift(nextMove); if (nextMove.x === this.apple.x && nextMove.y === this.apple.y) { const score = this.snake.length - 3; this.scoreDisplay.textContent = score; if (score > this.bestScore) { this.bestScore = score; this.bestScoreDisplay.textContent = score; localStorage.setItem('bestScore', score); } this.spawnApple(); } else { this.snake.pop(); } this.frameCount++; } getSurvivalMove() { const head = this.snake[0]; const neighbors = [ { x: head.x, y: head.y - 1 }, { x: head.x, y: head.y + 1 }, { x: head.x - 1, y: head.y }, { x: head.x + 1, y: head.y } ].filter(n => !this.isOccupied(n.x, n.y)); if (neighbors.length === 0) return null; return neighbors.sort((a, b) => this.getReachableArea(b) - this.getReachableArea(a))[0]; } getReachableArea(pos) { const visited = new Set(); const stack = [pos]; visited.add(`${pos.x},${pos.y}`); let count = 0; while (stack.length > 0) { const curr = stack.pop(); count++; const neighbors = [ { x: curr.x, y: curr.y - 1 }, { x: curr.x, y: curr.y + 1 }, { x: curr.x - 1, y: curr.y }, { x: curr.x + 1, y: curr.y } ]; for (const n of neighbors) { if (!this.isOccupied(n.x, n.y) && !visited.has(`${n.x},${n.y}`)) { visited.add(`${n.x},${n.y}`); stack.push(n); } } } return count; } draw() { const { ctx, canvas } = this; const ts = CONFIG.TILE_SIZE; ctx.fillStyle = CONFIG.COLORS.BG; ctx.fillRect(0, 0, canvas.width, canvas.height); // Grid ctx.strokeStyle = CONFIG.COLORS.GRID; ctx.lineWidth = 0.5; for (let i = 0; i <= CONFIG.GRID_SIZE; i++) { ctx.beginPath(); ctx.moveTo(i * ts, 0); ctx.lineTo(i * ts, canvas.height); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, i * ts); ctx.lineTo(canvas.width, i * ts); ctx.stroke(); } // Walls this.walls.forEach(w => { const x = w.x * ts, y = w.y * ts; ctx.fillStyle = CONFIG.COLORS.WALL; this.drawTexturedRect(ctx, x, y, ts, ts, 'wall'); }); // Apple const ax = this.apple.x * ts + ts/2, ay = this.apple.y * ts + ts/2; this.drawApple(ctx, ax, ay, ts); // Snake for (let i = this.snake.length - 1; i >= 0; i--) { const s = this.snake[i]; const x = s.x * ts, y = s.y * ts; if (i === 0) { this.drawSnakeHead(ctx, x, y, ts); } else if (i === this.snake.length - 1) { this.drawSnakeTail(ctx, x, y, ts, i); } else { this.drawSnakeBody(ctx, x, y, ts, i); } } } drawApple(ctx, x, y, ts) { const radius = ts / 2 - 2; ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(x, y + ts/4, radius, radius/2, 0, 0, Math.PI*2); ctx.fill(); const grad = ctx.createRadialGradient(x - ts/6, y - ts/6, ts/10, x, y, ts/2); grad.addColorStop(0, '#ff4d4d'); grad.addColorStop(1, '#8b0000'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.beginPath(); ctx.arc(x - ts/5, y - ts/5, ts/8, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#22c55e'; ctx.beginPath(); ctx.ellipse(x + 2, y - ts/2.5, ts/4, ts/8, Math.PI/4, 0, Math.PI * 2); ctx.fill(); } drawSnakeHead(ctx, x, y, ts) { const next = this.snake[1]; let rotation = 0; if (next) { if (next.x < this.snake[0].x) rotation = Math.PI / 2; else if (next.x > this.snake[0].x) rotation = -Math.PI / 2; else if (next.y < this.snake[0].y) rotation = Math.PI; else if (next.y > this.snake[0].y) rotation = 0; } ctx.save(); ctx.translate(x + ts/2, y + ts/2); ctx.rotate(rotation); const grad = ctx.createRadialGradient(0, 0, ts/4, 0, 0, ts/2); grad.addColorStop(0, CONFIG.COLORS.SNAKE_HEAD); grad.addColorStop(1, CONFIG.COLORS.SNAKE_BODY); ctx.fillStyle = grad; this.drawRoundedRect(ctx, -ts/2 + 1, -ts/2 + 1, ts - 2, ts + 2, 12); // Apple tracking eyes const dx = this.apple.x - this.snake[0].x; const dy = this.apple.y - this.snake[0].y; const angle = Math.atan2(dy, dx) - rotation + Math.PI/2; const eyeOffset = ts * 0.05; const ex = Math.cos(angle) * eyeOffset; const ey = Math.sin(angle) * eyeOffset; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-ts*0.22, -ts*0.2, ts*0.14, 0, Math.PI*2); ctx.arc(ts*0.22, -ts*0.2, ts*0.14, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-ts*0.22 + ex, -ts*0.2 + ey, ts*0.07, 0, Math.PI*2); ctx.arc(ts*0.22 + ex, -ts*0.2 + ey, ts*0.07, 0, Math.PI*2); ctx.fill(); // Nostrils ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.arc(-ts*0.1, -ts*0.4, 2, 0, Math.PI*2); ctx.arc(ts*0.1, -ts*0.4, 2, 0, Math.PI*2); ctx.fill(); if (this.frameCount % 20 < 10) { ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -ts/2); ctx.lineTo(0, -ts/2 - 10); ctx.lineTo(-4, -ts/2 - 14); ctx.moveTo(0, -ts/2 - 10); ctx.lineTo(4, -ts/2 - 14); ctx.stroke(); } ctx.restore(); } drawSnakeBody(ctx, x, y, ts, index) { const curr = this.snake[index]; const next = this.snake[index - 1]; const prev = this.snake[index + 1]; if (!next || !prev) return; ctx.save(); ctx.translate(x + ts/2, y + ts/2); const grad = ctx.createLinearGradient(-ts/2, -ts/2, ts/2, ts/2); grad.addColorStop(0, CONFIG.COLORS.SNAKE_BODY); grad.addColorStop(1, CONFIG.COLORS.SNAKE_BODY_GRADIENT); ctx.fillStyle = grad; const dirNext = { x: next.x - curr.x, y: next.y - curr.y }; const dirPrev = { x: prev.x - curr.x, y: prev.y - curr.y }; const isHorizontal = next.y === prev.y; const isVertical = next.x === prev.x; if (isHorizontal) { this.drawRoundedRect(ctx, -ts/2, -ts/2 + 2, ts, ts - 4, 4); } else if (isVertical) { this.drawRoundedRect(ctx, -ts/2 + 2, -ts/2, ts - 4, ts, 4); } else { // Corner - smooth arc connection ctx.beginPath(); if ((dirNext.x === 1 && dirPrev.y === 1) || (dirNext.y === 1 && dirPrev.x === 1)) { // Bottom-right ctx.arc(ts/2, ts/2, ts - 2, Math.PI, Math.PI * 1.5); ctx.lineTo(ts/2, ts/2); } else if ((dirNext.x === -1 && dirPrev.y === 1) || (dirNext.y === 1 && dirPrev.x === -1)) { // Bottom-left ctx.arc(-ts/2, ts/2, ts - 2, Math.PI * 1.5, 0); ctx.lineTo(-ts/2, ts/2); } else if ((dirNext.x === 1 && dirPrev.y === -1) || (dirNext.y === -1 && dirPrev.x === 1)) { // Top-right ctx.arc(ts/2, -ts/2, ts - 2, Math.PI * 0.5, Math.PI); ctx.lineTo(ts/2, -ts/2); } else { // Top-left ctx.arc(-ts/2, -ts/2, ts - 2, 0, Math.PI * 0.5); ctx.lineTo(-ts/2, -ts/2); } ctx.closePath(); ctx.fill(); } // Animated scales if ((index + Math.floor(this.frameCount/2)) % 6 === 0) { ctx.fillStyle = 'rgba(255,255,255,0.12)'; ctx.beginPath(); ctx.arc(0, 0, ts/5, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } drawSnakeTail(ctx, x, y, ts, index) { const curr = this.snake[index]; const prev = this.snake[index - 1]; if (!prev) return; let rotation = 0; if (prev.x < curr.x) rotation = Math.PI / 2; else if (prev.x > curr.x) rotation = -Math.PI / 2; else if (prev.y < curr.y) rotation = Math.PI; else if (prev.y > curr.y) rotation = 0; ctx.save(); ctx.translate(x + ts/2, y + ts/2); ctx.rotate(rotation); const grad = ctx.createLinearGradient(-ts/2, -ts/2, ts/2, ts/2); grad.addColorStop(0, CONFIG.COLORS.SNAKE_BODY); grad.addColorStop(1, CONFIG.COLORS.SNAKE_BODY_GRADIENT); ctx.fillStyle = grad; // Tapered tail ctx.beginPath(); ctx.moveTo(-ts/2 + 4, -ts/2); ctx.lineTo(ts/2 - 4, -ts/2); ctx.quadraticCurveTo(0, ts/2, 0, ts/2 + 6); ctx.closePath(); ctx.fill(); ctx.restore(); } drawTexturedRect(ctx, x, y, w, h, type) { if (type === 'wall') { const grad = ctx.createLinearGradient(x, y, x + w, y + h); grad.addColorStop(0, '#475569'); grad.addColorStop(1, '#1e293b'); ctx.fillStyle = grad; this.drawRoundedRect(ctx, x + 2, y + 2, w - 4, h - 4, 6); ctx.strokeStyle = 'rgba(255,255,255,0.1)'; ctx.strokeRect(x + 6, y + 6, w - 12, h - 12); } } drawRoundedRect(ctx, x, y, w, h, r) { ctx.beginPath(); if (ctx.roundRect) { ctx.roundRect(x, y, w, h, r); } else { ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); } ctx.closePath(); ctx.fill(); } gameLoop(timestamp) { const speed = parseInt(this.speedRange.value); const interval = 1000 / speed; if (timestamp - this.lastFrameTime >= interval) { this.update(); this.draw(); this.lastFrameTime = timestamp; } requestAnimationFrame((t) => this.gameLoop(t)); } } document.addEventListener('DOMContentLoaded', () => new Game());