---
title: "Как сделать Змейку на JS: таймер шага, хвост, переход через край"
description: "Пошаговая логика игры «Змейка» на JavaScript: таймер шага с накопителем, хвост как массив клеток, переход через край по остатку от деления, очередь поворотов без разворота на 180°, еда только в свободных клетках. Код с тестами, проверенный в Node.js 24."
slug: kak-sobrat-igru-zmejka-na-sobytiyah
date: 2026-10-31
updated: 2026-11-02
scheduled_at: "2026-10-31T19:00:00+03:00"
rubric: praktika-i-uroki
tags: ["zmejka", "javascript", "shkolnikam", "novichkam", "praktika"]
reading_time: 10
draft: false
summary: "Змейка — это клеточная игра, где голова сдвигается на одну клетку по таймеру шага, а хвост повторяет её путь. Логику удобно описать как набор событий: «натикал шаг» двигает змейку, «нажата стрелка» кладёт поворот в очередь, «голова на еде» запускает рост, «голова на теле» заканчивает игру. Переход через край считается остатком от деления, который работает и для отрицательных координат, а еда выбирается только из свободных клеток, поэтому не появляется внутри змейки."
---

## Коротко

Змейка собирается из четырёх правил: таймер шага двигает голову на одну клетку, хвост повторяет путь головы, край поля переносит голову на противоположную сторону, еда появляется только в свободной клетке. Две частые ошибки — смерть от быстрого двойного поворота и еда внутри тела — решаются очередью поворотов и выбором из списка свободных клеток. Ниже — полный код логики без отрисовки с шестью проверками, запущенными в Node.js 24.

## Содержание

1. Из чего состоит Змейка: четыре события
2. Таймер шага: накопитель времени вместо счёта кадров
3. Хвост: массив клеток и рост без пересчёта
4. Переход через край: остаток от деления для отрицательных чисел
5. Повороты: очередь и запрет разворота на 180°
6. Еда только в свободных клетках
7. Таблица: скорость и размер поля
8. Код на JavaScript с проверками
9. Практический сценарий: от логики к игре за вечер
10. Когда вам это не подойдёт

## Из чего состоит Змейка: четыре события

Змейку удобно описывать как реакции на четыре события, и почти вся игра помещается в эти реакции:

| Событие | Условие | Действие |
|---|---|---|
| Натикал шаг таймера | змейка жива | взять поворот из очереди, сдвинуть голову |
| Нажата стрелка | поворот не противоположен последнему | положить поворот в очередь |
| Голова в клетке еды | — | +1 к росту, новая еда |
| Голова в клетке тела | это не освобождаемый хвост | конец игры |

Состояние игры — это массив клеток тела, направление, очередь поворотов, счётчик роста и клетка еды. Отрисовка только читает это состояние.

## Таймер шага: накопитель времени вместо счёта кадров

Шаг змейки нужно отсчитывать по времени, а не по числу кадров, иначе на мониторе 144 Гц игра станет в 2,4 раза быстрее, чем на 60 Гц. Каждый кадр прибавляйте его длительность к накопителю; пока накопитель не меньше длины шага, вычитайте шаг и двигайте змейку:

```text
накопитель += dt
пока накопитель ≥ шаг:  накопитель −= шаг; сделать шаг
шагов в секунду = 1 / шаг   (0,14 с → около 7,1)
```

Ограничьте число шагов за кадр, например пятью: после того как вкладка была свёрнута, накопитель может вырасти до секунд, и змейка «телепортируется» через полполя.

## Хвост: массив клеток и рост без пересчёта

Тело змейки — массив клеток, где нулевой элемент — голова. На каждом шаге новая клетка головы добавляется в начало массива, а последняя удаляется с конца; хвост сам повторяет путь головы. Рост — это пропуск удаления: съеденная еда увеличивает счётчик роста, и пока он больше нуля, хвост не укорачивается.

Важная тонкость: в клетку, которую хвост покидает на этом шаге, голова войти может. Поэтому при проверке столкновения последний сегмент исключают — но только если змейка в этот шаг не растёт.

## Переход через край: остаток от деления для отрицательных чисел

Переход через край считается остатком от деления, но в JavaScript оператор `%` для отрицательных чисел даёт отрицательный результат: `-1 % 20` равно `-1`. Нужна форма, которая всегда возвращает число от 0 до n − 1:

```text
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, числа в комментариях совпадают с выводом.

```js
// Змейка без отрисовки: только правила. Поле 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 шага:

1. **Отрисовка.** На холсте размером `cols × размер_клетки` закрашивайте каждую клетку тела и еды квадратом; например, 20 × 15 клеток по 32 пикселя дают холст 640 × 480.
2. **Цикл.** В `requestAnimationFrame` вычисляйте `dt` в секундах, вызывайте таймер шага и затем рисуйте состояние.
3. **Ввод.** На `keydown` переводите `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight` в вызовы `turn`; для телефона добавьте свайпы с порогом 30 пикселей.
4. **Счёт и рестарт.** Очки — это длина тела минус 3; при `alive === false` показывайте итог и пересоздавайте состояние по нажатию пробела.

Отдельно проверьте вкладку в фоне: свернув её на 10 секунд, змейка не должна пролететь поле — за это отвечает предел пяти шагов за кадр.

## Когда вам это не подойдёт

Подход «логика по таймеру на сетке» хорош для Змейки и похожих игр, но не для всего:

- **Плавное движение между клетками:** если нужно, чтобы змейка скользила, а не прыгала по клеткам, к логике добавляется интерполяция отрисовки между шагами — это отдельная техника, которой здесь нет.
- **Сетевая игра на несколько змеек:** при игре по сети шаги нужно синхронизировать между игроками и разрешать одновременные столкновения голов, а это задача сетевого кода.
- **Очень большие поля:** при поле 1000 × 1000 клеток перебор свободных клеток на каждый выбор еды станет заметен; понадобится хранить свободные клетки в отдельной структуре.

---

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок, где логика собирается из событий, условий и действий, а при необходимости углубляется на TypeScript. Движок в разработке и публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=kak-sobrat-igru-zmejka-na-sobytiyah) (подача заявки не гарантирует приглашение).

**Читайте также:**
- [От первых аркад и «Змейки» до инди-хитов: история ключевых механик 2D-игр](/blog/istoriya-klyuchevyh-mehanik-2d-igr)
- [Тайловые сетки и устройство 2D-миров](/blog/tajlovye-setki-i-ustrojstvo-2d-mirov)
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
