Files
snake-game/script.js
T
2026-02-03 14:57:13 +00:00

183 lines
4.5 KiB
JavaScript

(() => {
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();
})();