(() => { const board = document.getElementById("board"); const ctx = board.getContext("2d"); const scoreEl = document.getElementById("score"); const statusEl = document.getElementById("status"); const restartBtn = document.getElementById("restart"); const gridSize = 20; const cellSize = board.width / gridSize; const tickMs = 120; const directions = { ArrowUp: { x: 0, y: -1 }, ArrowDown: { x: 0, y: 1 }, ArrowLeft: { x: -1, y: 0 }, ArrowRight: { x: 1, y: 0 }, w: { x: 0, y: -1 }, s: { x: 0, y: 1 }, a: { x: -1, y: 0 }, d: { x: 1, y: 0 }, }; function isOpposite(a, b) { return a.x + b.x === 0 && a.y + b.y === 0; } function createInitialState() { const start = { x: 9, y: 10 }; const snake = [start, { x: 8, y: 10 }, { x: 7, y: 10 }]; const dir = { x: 1, y: 0 }; return { snake, dir, nextDir: dir, food: placeFood(snake), score: 0, over: false, }; } function placeFood(snake) { const occupied = new Set(snake.map((seg) => `${seg.x},${seg.y}`)); const empty = []; for (let y = 0; y < gridSize; y += 1) { for (let x = 0; x < gridSize; x += 1) { const key = `${x},${y}`; if (!occupied.has(key)) { empty.push({ x, y }); } } } if (empty.length === 0) return null; const pick = empty[Math.floor(Math.random() * empty.length)]; return pick; } function nextHead(head, dir) { return { x: head.x + dir.x, y: head.y + dir.y }; } function outOfBounds(pos) { return pos.x < 0 || pos.x >= gridSize || pos.y < 0 || pos.y >= gridSize; } function hitSelf(pos, snake) { return snake.some((seg) => seg.x === pos.x && seg.y === pos.y); } function step(state) { if (state.over) return state; const dir = state.nextDir; const head = state.snake[0]; const next = nextHead(head, dir); const ateFood = state.food && next.x === state.food.x && next.y === state.food.y; const bodyToCheck = ateFood ? state.snake : state.snake.slice(0, -1); if (outOfBounds(next) || hitSelf(next, bodyToCheck)) { return { ...state, over: true }; } const nextSnake = [next, ...state.snake]; if (!ateFood) { nextSnake.pop(); } const nextFood = ateFood ? placeFood(nextSnake) : state.food; return { ...state, snake: nextSnake, dir, nextDir: dir, food: nextFood, score: state.score + (ateFood ? 1 : 0), }; } function drawGrid() { ctx.clearRect(0, 0, board.width, board.height); ctx.fillStyle = "#fdfdfb"; ctx.fillRect(0, 0, board.width, board.height); ctx.strokeStyle = "#e5e2dc"; ctx.lineWidth = 1; for (let i = 0; i <= gridSize; i += 1) { const p = i * cellSize; ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, board.height); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(board.width, p); ctx.stroke(); } } function drawSnake(snake) { snake.forEach((seg, index) => { ctx.fillStyle = index === 0 ? "#2f5d50" : "#4a7a6f"; ctx.fillRect(seg.x * cellSize + 1, seg.y * cellSize + 1, cellSize - 2, cellSize - 2); }); } function drawFood(food) { if (!food) return; ctx.fillStyle = "#b93b3b"; ctx.beginPath(); const cx = food.x * cellSize + cellSize / 2; const cy = food.y * cellSize + cellSize / 2; ctx.arc(cx, cy, cellSize / 2.6, 0, Math.PI * 2); ctx.fill(); } function render(state) { drawGrid(); drawFood(state.food); drawSnake(state.snake); scoreEl.textContent = String(state.score); statusEl.textContent = state.over ? "Game over. Press Restart or Enter." : ""; } let state = createInitialState(); let intervalId = null; function startLoop() { if (intervalId) clearInterval(intervalId); intervalId = setInterval(() => { state = step(state); render(state); }, tickMs); } function restart() { state = createInitialState(); render(state); startLoop(); } function handleKey(event) { const key = event.key; if (key === "Enter" && state.over) { restart(); return; } if (!Object.prototype.hasOwnProperty.call(directions, key)) return; const candidate = directions[key]; if (isOpposite(candidate, state.dir)) return; state = { ...state, nextDir: candidate }; } document.addEventListener("keydown", handleKey); restartBtn.addEventListener("click", restart); render(state); startLoop(); })();