Коротко
Змейка собирается из четырёх правил: таймер шага двигает голову на одну клетку, хвост повторяет путь головы, край поля переносит голову на противоположную сторону, еда появляется только в свободной клетке. Две частые ошибки — смерть от быстрого двойного поворота и еда внутри тела — решаются очередью поворотов и выбором из списка свободных клеток. Ниже — полный код логики без отрисовки с шестью проверками, запущенными в Node.js 24.
Содержание
- Из чего состоит Змейка: четыре события
- Таймер шага: накопитель времени вместо счёта кадров
- Хвост: массив клеток и рост без пересчёта
- Переход через край: остаток от деления для отрицательных чисел
- Повороты: очередь и запрет разворота на 180°
- Еда только в свободных клетках
- Таблица: скорость и размер поля
- Код на JavaScript с проверками
- Практический сценарий: от логики к игре за вечер
- Когда вам это не подойдёт
Из чего состоит Змейка: четыре события
Змейку удобно описывать как реакции на четыре события, и почти вся игра помещается в эти реакции:
| Событие | Условие | Действие |
|---|---|---|
| Натикал шаг таймера | змейка жива | взять поворот из очереди, сдвинуть голову |
| Нажата стрелка | поворот не противоположен последнему | положить поворот в очередь |
| Голова в клетке еды | — | +1 к росту, новая еда |
| Голова в клетке тела | это не освобождаемый хвост | конец игры |
Состояние игры — это массив клеток тела, направление, очередь поворотов, счётчик роста и клетка еды. Отрисовка только читает это состояние.
Таймер шага: накопитель времени вместо счёта кадров
Шаг змейки нужно отсчитывать по времени, а не по числу кадров, иначе на мониторе 144 Гц игра станет в 2,4 раза быстрее, чем на 60 Гц. Каждый кадр прибавляйте его длительность к накопителю; пока накопитель не меньше длины шага, вычитайте шаг и двигайте змейку:
накопитель += dt
пока накопитель ≥ шаг: накопитель −= шаг; сделать шаг
шагов в секунду = 1 / шаг (0,14 с → около 7,1)
Ограничьте число шагов за кадр, например пятью: после того как вкладка была свёрнута, накопитель может вырасти до секунд, и змейка «телепортируется» через полполя.
Хвост: массив клеток и рост без пересчёта
Тело змейки — массив клеток, где нулевой элемент — голова. На каждом шаге новая клетка головы добавляется в начало массива, а последняя удаляется с конца; хвост сам повторяет путь головы. Рост — это пропуск удаления: съеденная еда увеличивает счётчик роста, и пока он больше нуля, хвост не укорачивается.
Важная тонкость: в клетку, которую хвост покидает на этом шаге, голова войти может. Поэтому при проверке столкновения последний сегмент исключают — но только если змейка в этот шаг не растёт.
Переход через край: остаток от деления для отрицательных чисел
Переход через край считается остатком от деления, но в JavaScript оператор % для отрицательных чисел даёт отрицательный результат: -1 % 20 равно -1. Нужна форма, которая всегда возвращает число от 0 до n − 1:
wrap(v, n) = ((v mod n) + n) mod n
wrap(-1, 20) = 19 wrap(20, 20) = 0
Если переход через край не нужен, вместо wrap проверяйте выход за границы и заканчивайте игру — это классический вариант с «глухими» стенами.
Повороты: очередь и запрет разворота на 180°
Повороты складывают в очередь на 2 элемента и проверяют против последнего направления в очереди, а не текущего. Без очереди игрок, который за один шаг быстро нажал «вверх» и «влево», теряет первое нажатие: второе перезаписывает его и оказывается разворотом назад, а змейка врезается в шею. С очередью «вверх» выполнится на этом шаге, «влево» — на следующем.
Противоположное направление определяется просто: сумма векторов направлений равна нулю.
Еда только в свободных клетках
Еду выбирают из списка свободных клеток, а не случайной клеткой с повтором при попадании в тело. Повтор работает на пустом поле, но когда змейка занимает 95% клеток, он может крутиться десятки раз. Список свободных клеток строится за один проход по полю; если он пуст — поле заполнено, и это победа.
Таблица: скорость и размер поля
Скорость задаётся длиной шага, а сложность — сочетанием шага и размера поля.
| Режим | Поле, клеток | Шаг, с | Шагов в секунду | Время пересечь поле по ширине |
|---|---|---|---|---|
| Учебный | 20 × 15 | 0,20 | 5 | 4,0 с |
| Обычный | 20 × 15 | 0,14 | ≈ 7,1 | 2,8 с |
| Быстрый | 30 × 20 | 0,08 | 12,5 | 2,4 с |
| Ускорение | любое | минус 0,005 за еду, не меньше 0,06 | до ≈ 16,7 | — |
Время пересечения — это ширина поля, умноженная на длину шага: 20 × 0,14 = 2,8 с.
Код на JavaScript с проверками
Код содержит всю логику Змейки без отрисовки и шесть проверок. Он запущен в Node.js 24, числа в комментариях совпадают с выводом.
// Змейка без отрисовки: только правила. Поле cols × rows клеток, шаг по таймеру.
const DIRS = { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } };
function createSnake(cols, rows, random) {
const s = {
cols, rows, random,
body: [{ x: 5, y: 5 }, { x: 4, y: 5 }, { x: 3, y: 5 }], // [0] — голова
dir: "right",
queue: [], // до 2 поворотов, нажатых между шагами
grow: 0,
alive: true,
food: null,
};
s.food = spawnFood(s);
return s;
}
// Поворот кладём в очередь; разворот на 180° относительно последнего направления отбрасываем
function turn(s, name) {
const last = s.queue.length ? s.queue[s.queue.length - 1] : s.dir;
const a = DIRS[last], b = DIRS[name];
if (a.x + b.x === 0 && a.y + b.y === 0) return false; // противоположное направление
if (name === last || s.queue.length >= 2) return false;
s.queue.push(name);
return true;
}
// Переход через край: остаток от деления, который работает и для -1
const wrap = function (v, n) { return ((v % n) + n) % n; };
function step(s) {
if (!s.alive) return;
if (s.queue.length) s.dir = s.queue.shift();
const d = DIRS[s.dir];
const head = { x: wrap(s.body[0].x + d.x, s.cols), y: wrap(s.body[0].y + d.y, s.rows) };
const ate = head.x === s.food.x && head.y === s.food.y;
if (ate) s.grow += 1;
// Хвост освобождает клетку в этом же шаге, поэтому в неё можно войти — если змейка не растёт
const tailLeaves = s.grow === 0;
const checkLen = tailLeaves ? s.body.length - 1 : s.body.length;
for (let i = 0; i < checkLen; i++) {
if (s.body[i].x === head.x && s.body[i].y === head.y) { s.alive = false; return; }
}
s.body.unshift(head);
if (s.grow > 0) s.grow -= 1; else s.body.pop();
if (ate) s.food = spawnFood(s);
}
// Еда — только в свободной клетке: перечисляем свободные и выбираем одну
function spawnFood(s) {
const busy = new Set(s.body.map(function (c) { return c.y * s.cols + c.x; }));
const free = [];
for (let i = 0; i < s.cols * s.rows; i++) if (!busy.has(i)) free.push(i);
if (!free.length) return null; // поле заполнено — победа
const i = free[Math.floor(s.random() * free.length)];
return { x: i % s.cols, y: Math.floor(i / s.cols) };
}
// Таймер шага: накапливаем время кадра и делаем столько шагов, сколько «натикало»
function createTicker(stepSec) {
let acc = 0;
return function (dt, onStep) {
acc += dt;
let n = 0;
while (acc >= stepSec && n < 5) { acc -= stepSec; onStep(); n++; }
return n;
};
}
// Детерминированный генератор для проверки
function lcg(seed) { let x = seed; return function () { x = (x * 1664525 + 1013904223) % 4294967296; return x / 4294967296; }; }
// 1. Переход через край: голова в x = 19 на поле 20 клеток уходит в x = 0
const s = createSnake(20, 15, lcg(7));
s.body = [{ x: 19, y: 5 }, { x: 18, y: 5 }, { x: 17, y: 5 }];
s.food = { x: 10, y: 10 };
step(s);
console.log("после края:", s.body[0]); // { x: 0, y: 5 }
// 2. Быстрое «вверх, затем влево» между шагами срабатывает за два шага, «влево» сразу — нет
console.log(turn(s, "left")); // false — разворот на 180°
console.log(turn(s, "up"), turn(s, "left")); // true true
step(s); step(s);
console.log("голова:", s.body[0], "направление:", s.dir); // { x: 19, y: 4 } left
// 3. Рост: еда прямо перед головой
s.food = { x: 18, y: 4 };
const before = s.body.length;
step(s); step(s);
console.log("длина:", before, "→", s.body.length); // 3 → 4
// 4. Вход в клетку, которую освобождает хвост, — не столкновение
const t = createSnake(20, 15, lcg(1));
t.body = [{ x: 5, y: 5 }, { x: 6, y: 5 }, { x: 6, y: 6 }, { x: 5, y: 6 }]; // квадрат 2×2
t.dir = "down"; t.food = { x: 0, y: 0 };
step(t);
console.log("жива после входа в клетку хвоста:", t.alive); // true
// 5. Таймер: 0,14 с на шаг, 60 кадров по 1/60 с дают 7 шагов
const tick = createTicker(0.14);
let steps = 0;
for (let i = 0; i < 60; i++) steps += tick(1 / 60, function () {});
console.log("шагов за 1 с:", steps); // 7
// 6. Еда никогда не появляется на теле: 1000 проверок
const u = createSnake(6, 4, lcg(42));
let bad = 0;
for (let i = 0; i < 1000; i++) {
const f = spawnFood(u);
if (u.body.some(function (c) { return c.x === f.x && c.y === f.y; })) bad++;
}
console.log("еда на теле:", bad); // 0
Проверки покрывают переход через край, отказ от разворота, быструю пару поворотов, рост, вход в клетку уходящего хвоста, число шагов таймера и то, что из 1000 выборов еды ни один не попал на тело.
Практический сценарий: от логики к игре за вечер
Логика выше превращается в играбельную Змейку за 4 шага:
- Отрисовка. На холсте размером
cols × размер_клеткизакрашивайте каждую клетку тела и еды квадратом; например, 20 × 15 клеток по 32 пикселя дают холст 640 × 480. - Цикл. В
requestAnimationFrameвычисляйтеdtв секундах, вызывайте таймер шага и затем рисуйте состояние. - Ввод. На
keydownпереводитеArrowUp,ArrowDown,ArrowLeft,ArrowRightв вызовыturn; для телефона добавьте свайпы с порогом 30 пикселей. - Счёт и рестарт. Очки — это длина тела минус 3; при
alive === falseпоказывайте итог и пересоздавайте состояние по нажатию пробела.
Отдельно проверьте вкладку в фоне: свернув её на 10 секунд, змейка не должна пролететь поле — за это отвечает предел пяти шагов за кадр.
Когда вам это не подойдёт
Подход «логика по таймеру на сетке» хорош для Змейки и похожих игр, но не для всего:
- Плавное движение между клетками: если нужно, чтобы змейка скользила, а не прыгала по клеткам, к логике добавляется интерполяция отрисовки между шагами — это отдельная техника, которой здесь нет.
- Сетевая игра на несколько змеек: при игре по сети шаги нужно синхронизировать между игроками и разрешать одновременные столкновения голов, а это задача сетевого кода.
- Очень большие поля: при поле 1000 × 1000 клеток перебор свободных клеток на каждый выбор еды станет заметен; понадобится хранить свободные клетки в отдельной структуре.
Мы разрабатываем J2DRU — браузерный 2D игровой движок, где логика собирается из событий, условий и действий, а при необходимости углубляется на TypeScript. Движок в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Читайте также: