Kivy — Snake
О практикуме
Snake (змейка) — классика аркад: змейка движется по сетке, съедает еду, растёт; столкновение со стеной или собой — конец игры. Поле 16×24 клетки; управление — свайп по полю и D-pad (кнопки-стрелки) внизу — как в реальных мобильных портах.
Чему учит практикум
- дискретный тик — змейка шагает по клеткам, не по пикселям (в отличие от Pong);
- два канала ввода — жест и виртуальные кнопки (Мобильные игры);
- ускорение — интервал
Clockуменьшается с ростом счёта; - отрисовка на canvas без отдельного спрайта на клетку — один
Widgetрисует всю сетку.
Краткий однофайловый вариант на Pygame — Lab — змейка. Здесь акцент на мобильный UX и Kivy API.
Оценка времени — 2–3 часа.
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Каркас | Экран с кнопкой Start |
| 1 | Константы | DIRECTIONS, цвета, запрет разворота |
| 2 | Отрисовка сетки | SnakeBoard._redraw |
| 3 | Движение | _tick, список snake |
| 4 | Еда | _spawn_food, рост |
| 5 | Game over | Стены и самопересечение |
| 6 | Свайп | set_direction |
| 7 | D-pad и UI | GameScreen, счёт, статус |
| 8 | Ревизия | Ускорение с ростом счёта |
Свайп по полю и D-pad внизу — типичный мобильный паттерн. Тот же свайп, что в 2048; дискретный тик по сетке — как в Lab — змейка на Pygame.
Как проходить практикум
- Папка
kivysnake/, venv,pip install "kivy>=2.3.0". - Этапы 0–8 по порядку; после каждого —
python main.py. - На этапе 7 собирается полный
GameScreen— до этого можно тестироватьSnakeBoardотдельно. - Финал — этап 8.
Архитектура
Змейка сочетает дискретную сетку (как 2048) и тик по таймеру (как Pong):
SnakeApp
└── GameScreen (BoxLayout)
├── score_label, status_label
├── SnakeBoard (Widget) ← canvas: сетка, snake[], food
└── D-pad GridLayout + Start / Restart
| Структура данных | Смысл |
|---|---|
snake | Список (x, y); голова — snake[0] |
food | Одна свободная клетка (x, y) |
direction / next_direction | Текущий ход и буфер ввода между тиками |
tick_event | Clock.schedule_interval; интервал уменьшается с очками |
Определение. Дискретный тик — за один вызов _tick змейка сдвигается ровно на одну клетку. Плавная анимация между клетками здесь не нужна — так проще логика и отладка.
Зависимости
mkdir kivysnake && cd kivysnake
python -m venv .venv
pip install "kivy>=2.3.0"
Один main.py. Поле 16×24 клетки, cell_size = dp(18) — подбирается под вертикальный экран телефона (мобильные игры).
Этап 0 — каркас
Цель. Вертикальный layout — задел под заголовок, поле и панель управления.
Теория
На этапе 0 только каркас GameScreen: позже сюда добавятся SnakeBoard (центр, size_hint=(1,1)) и D-pad (низ). Так проще отлаживать отрисовку и тик по отдельности — см. отладку.
mkdir kivysnake && cd kivysnake
pip install "kivy>=2.3.0"
from kivy.app import App
from kivy.metrics import dp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
class GameScreen(BoxLayout):
def __init__(self, **kwargs):
super().__init__(orientation="vertical", padding=dp(8), spacing=dp(8), **kwargs)
self.add_widget(Label(text="Snake", font_size=dp(24)))
self.add_widget(Button(text="Start"))
class SnakeApp(App):
def build(self):
return GameScreen()
if __name__ == "__main__":
SnakeApp().run()
Разбор.
BoxLayout(orientation="vertical")— типичный каркас мобильного экрана: сверху инфо, в центре игра, снизу кнопки.dp(8)— отступы, не зависящие от DPI (единицы в Kivy).
Самопроверка
- Вертикальный layout с кнопкой Start отображается.
Этап 1 — направления и цвета
Цель. словарь направлений и запрет мгновенного разворота на 180°.
DIRECTIONS = {
"up": (0, 1),
"down": (0, -1),
"left": (-1, 0),
"right": (1, 0),
}
OPPOSITE = {
"up": "down", "down": "up",
"left": "right", "right": "left",
}
Логические координаты — x вправо, y вверх (математическая сетка). Экран Kivy — ось Y снизу вверх, поэтому при отрисовке клетки (sx, sy):
screen_y = offset_y + (self.rows - 1 - sy) * self.cell_size
Разбор.
next_directionпринимает ввод; в_tickкопируется вdirection— за один тик змейка не разворачивается дважды.set_directionотклоняет направление, противоположное текущемуdirection, если игра уже идёт.
Самопроверка
- Ключи в
DIRECTIONSиOPPOSITEсогласованы.
Этап 2 — SnakeBoard, отрисовка
Цель. виджет поля рисует сетку, змейку и еду на одном canvas.
Класс SnakeBoard(Widget) с cols=16, rows=24, cell_size = dp(18).
Алгоритм _redraw:
self.canvas.clear()— стереть прошлый кадр.- Вычислить
board_w,board_hиoffset_x,offset_yдля центрирования сетки в виджете. - Фон — большой
Rectangle. - Линии сетки — тонкие
Rectangle1×N пикселей. - Если
self.snakeне пуст — еда (красный квадрат) и сегменты змейки (голова ярче).
Разбор.
- Один виджет вместо 16×24 дочерних
Tile— легче для змейки, чем для 2048 (практикум 2048 использует 16 виджетов — там нужны анимации плиток). bind(size=self._redraw, pos=self._redraw)— при ресайзе окна поле перерисовывается.
Самопроверка
- Пустое поле с сеткой видно по центру виджета.
Этап 3 — тик и движение
Цель. змейка из трёх клеток ползёт вправо по таймеру.
def start_game(self):
mid_y = self.rows // 2
self.snake = [(4, mid_y), (3, mid_y), (2, mid_y)]
self.direction = "right"
self.next_direction = "right"
self.running = True
self._schedule_tick()
def _tick(self, _dt):
self.direction = self.next_direction
dx, dy = DIRECTIONS[self.direction]
head_x, head_y = self.snake[0]
new_head = (head_x + dx, head_y + dy)
self.snake.insert(0, new_head)
self.snake.pop() # без еды — хвост укорачивается
self._redraw()
def _schedule_tick(self):
if self.tick_event:
self.tick_event.cancel()
self.tick_event = Clock.schedule_interval(self._tick, 0.15)
Разбор.
snake— список координат; голова —snake[0].insert(0, new_head)+pop()— сдвиг без роста.0.15с — ~6–7 шагов в секунду на старте.
Сравните с 2048 — там ход по событию (свайп), здесь — по таймеру (Clock).
Самопроверка
- После Start змейка из трёх клеток движется вправо.
Этап 4 — еда и рост
Цель. случайная еда; при поедании змейка не укорачивает хвост.
def _spawn_food(self):
occupied = set(self.snake)
free = [
(x, y)
for x in range(self.cols)
for y in range(self.rows)
if (x, y) not in occupied
]
if not free:
self.running = False # поле заполнено — победа
return
self.food = free[randint(0, len(free) - 1)]
В _tick, если new_head == self.food:
- не вызывать
pop()— хвост остаётся, длина +1; score += 10;_spawn_food();_schedule_tick()— пересчитать скорость (этап 8).
Самопроверка
- После поедания змейка длиннее на одну клетку.
- Счёт увеличивается (если уже подключён
update_scoreу родителя).
Этап 5 — game over
Цель. остановка при вылете за сетку или укусе хвоста.
if not (0 <= new_head[0] < self.cols and 0 <= new_head[1] < self.rows):
self._game_over()
return
if new_head in self.snake:
self._game_over()
return
def _game_over(self):
self.stop_game()
if hasattr(self.parent, "on_game_over"):
self.parent.on_game_over(self.score)
Разбор.
- Проверка
new_head in self.snake— доinsert, иначе голова совпадёт с бывшим хвостом на клетку. stop_game()отменяетtick_event— змейка замирает.
Самопроверка
- Врез в стену останавливает игру.
- Врез в хвост останавливает игру.
Этап 6 — свайп
Цель. смена направления жестом — как в 2048.
_touch_start в on_touch_down; в on_touch_up — dx, dy, порог dp(20):
def set_direction(self, direction):
if direction not in DIRECTIONS:
return
if self.running and direction == OPPOSITE.get(self.direction):
return
self.next_direction = direction
Разбор.
- Ввод меняет только
next_direction— безопасно между тиками. - Короткий тап без смещения не меняет направление (
return Trueпоглощает touch).
Самопроверка
- Свайп вверх задаёт движение вверх со следующего тика.
- Разворот на 180° за один тик невозможен.
Этап 7 — GameScreen и D-pad
Цель. полный экран — счёт, статус, поле, виртуальный крестовик.
Структура GameScreen(BoxLayout):
- верх —
score_label("Score: 0") +status_label("Tap Start"); - центр —
SnakeBoard(size_hint=(1, 1))растягивается; - низ —
GridLayout3×3 с кнопками ▲ ◀ ▶ ▼ и колонка Start / Restart.
def update_score(self, score):
self.score_label.text = f"Score: {score}"
def on_game_over(self, score):
self.status_label.text = f"Game Over! {score} pts"
self.start_btn.text = "Play Again"
Разбор.
- D-pad дублирует свайп — игрок выбирает удобный способ (виртуальные элементы управления).
Restartсбрасывает поле без автостарта;Start/Play Againвызываетboard.start_game().- Кнопки через
_make_btnиlambda— один обработчик на направление.
Самопроверка
- Кнопки ▲▼◀▶ меняют направление во время игры.
- Restart обнуляет счёт и останавливает тик.
Этап 8 — ускорение и ревизия
Цель. игра усложняется с ростом счёта; финальная проверка.
speed = max(0.08, 0.18 - self.score * 0.004)
self.tick_event = Clock.schedule_interval(self._tick, speed)
Разбор.
- Каждые 10 очков интервал уменьшается на 0.04 с; минимум 0.08 с (~12 шагов/с).
- Пересоздание
schedule_intervalв_schedule_tick— старый таймер отменяют черезcancel().
Сверка с листингами этапов 0–7 или повторный прогон всех шагов подряд.
Управление
| Ввод | Действие |
|---|---|
| Start / Play Again | Начать партию |
| Свайп по полю | Смена направления |
| D-pad | Смена направления |
| Restart | Сброс без немедленного старта |
Итоговая самопроверка
- +10 очков за каждую еду.
- Игра ускоряется с ростом счёта.
- Заполнение всего поля останавливает
running(опциональная "победа"). - Пройдены все три практикума Kivy — обзор раздела.
Дальше — сборка APK или сравнение с Flutter для нативного UI.