183 lines
4.5 KiB
JavaScript
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();
|
|
})();
|