Kivy — 2048
О практикуме
2048 — головоломка на поле 4×4: при сдвиге плитки с одинаковыми числами сливаются (2+2→4), на пустую клетку падает новая 2 или 4. Цель — плитка 2048. Управление — свайпы на телефоне или стрелки / WASD на ПК.
Почему 2048 первым в практикуме Kivy
- ходы дискретные — не нужен постоянный
Clock(в отличие от Pong); - хорошо тренирует свайп — базовый жест мобильных игр (Мобильные игры);
- логику легко вынести в отдельный файл без Kivy — чистая модель.
Проект разделён на game_logic.py (правила) и main.py (интерфейс) — тот же приём MVC, что в SmallPong и разделении логики в обзоре Kivy.
Оценка времени — 2–3 часа.
Карта этапов
| Этап | Фокус | Результат |
|---|---|---|
| 0 | Окружение | Пустое окно Kivy |
| 1 | Game2048 | Сдвиг и слияние без GUI |
| 2 | Tile | Цветная плитка с числом |
| 3 | BoardGrid | Сетка 4×4 |
| 4 | GameScreen | Заголовок, счёт, кнопка "Новая игра" |
| 5 | Свайп | Ходы по жесту |
| 6 | Клавиатура | Стрелки и R на десктопе |
| 7 | JsonStore | Сохранение рекорда |
| 8 | Ревизия | Итоговая самопроверка |
Нужны Python 3.10+, классы, списки и базовый Kivy (App, Widget, GridLayout). Теория фреймворка — обзор Kivy; Pygame-аналоги — практикум игр и мини-игры в Lab.
Как проходить практикум
- Создайте папку
kivy2048/, venv и установите Kivy — зависимости. - Идите этапы 0–8 по порядку — после каждого запускайте
python main.py. - Копируйте код целиком из блока этапа, без пропусков
# .... - Отмечайте Самопроверку; читайте Разбор — там связь строк с правилами 2048.
- Финал — этап 8 и сверка с суммой всех листингов.
Маршрут чтения
- Архитектура и глоссарий — до первой строки кода.
- Зависимости — окружение и структура файлов.
- Этапы 0–8.
- Переход к Kivy — Pong или сборке APK.
Архитектура
Проект сознательно разделён на модель и интерфейс — тот же приём MVC, что в SmallPong и Match3:
main.py
Game2048App.build()
└── GameScreen ← свайп, клавиатура, JsonStore
├── ScoreBox ← счёт и рекорд
└── BoardGrid ← 16 × Tile
└── game: Game2048 ← game_logic.py, без import kivy
Определение. Дискретный ход — состояние меняется только после события (свайп, клавиша), а не каждый кадр. Поэтому здесь не нужен Clock.schedule_interval — в отличие от Pong и Snake.
| Слой | Файл / класс | Ответственность |
|---|---|---|
| Модель | game_logic.py, Game2048 | Сетка 4×4, слияние, счёт, game over |
| Представление | Tile, BoardGrid | Цвета плиток, сетка на экране |
| Контроллер | GameScreen | Жесты, refresh_ui(), рекорд |
| Если смешать слои | Типичный баг |
|---|---|
Kivy в game_logic.py | Модель нельзя тестировать из REPL / pytest |
move() внутри draw() | Двойные ходы за один жест |
Обновление счёта в Tile | Рассинхрон UI и game.score |
Глоссарий
| Термин | В этом практикуме |
|---|---|
| Слияние (merge) | Две соседние равные плитки в строке/столбце → одна с суммой; за ход каждая пара сливается один раз |
| Сжатие (compress) | Сдвиг всех ненулевых значений к краю без слияния |
| Свайп | Жест: touch_down → touch_up, порог dp(30) отсекает случайные тапы |
refresh_ui() | Единая точка синхронизации модели и всех виджетов после move() |
JsonStore | Простое key-value хранилище Kivy на JSON-файле для рекорда |
sp / dp | Масштабируемые единицы Kivy — см. обзор |
Зависимости
Требования
- Python 3.10+;
- pip;
- Kivy 2.3+ (OpenGL; на Android — через Buildozer).
Окружение
mkdir kivy2048 && cd kivy2048
python -m venv .venv
Windows (PowerShell):
.\.venv\Scripts\Activate.ps1
pip install "kivy>=2.3.0"
Linux / macOS:
source .venv/bin/activate
pip install "kivy>=2.3.0"
Структура проекта
kivy2048/
game_logic.py # с этапа 1 — правила без Kivy
main.py # App, экран, виджеты
scores.json # с этапа 7 — рекорд JsonStore
requirements.txt # опционально: kivy>=2.3.0
Подробнее про venv — Зависимости Python.
Этап 0 — окружение
Цель. Рабочее venv, установленный Kivy, пустое окно.
Теория
App.build() — единственная точка, где Kivy ожидает корневой виджет. Пока это Label; на этапе 4 замените на GameScreen. Подробнее про жизненный цикл — обзор Kivy.
mkdir kivy2048 && cd kivy2048
python -m venv .venv && .venv\Scripts\activate
pip install "kivy>=2.3.0"
main.py:
from kivy.app import App
from kivy.uix.label import Label
class Game2048App(App):
def build(self):
return Label(text="2048 — старт", font_size="32sp")
if __name__ == "__main__":
Game2048App().run()
Разбор.
font_size="32sp"— масштабируемый шрифт (аналог sp в Android, см. обзор Kivy).Game2048App— имя класса по игре; позжеbuild()вернёт полноценный экран.
Самопроверка
-
python main.pyоткрывает окно с текстом. - Повторный запуск не выдаёт ошибок импорта.
Этап 1 — класс Game2048
Цель. Правила 2048 в чистом Python, без Kivy.
Теория
Алгоритм одного хода (например, влево) — три фазы для каждой строки:
- Сжатие — убрать нули, сдвинуть числа к краю.
- Слияние — пройти слева направо; если
line[i] == line[i+1], сложить, записать сумму вi, обнулитьi+1(каждая пара — один раз за ход). - Повторное сжатие — закрыть дыры после merge.
Для _move_right строку разворачивают, применяют left-merge, разворачивают обратно. Для up/down — обход по столбцам (транспонирование логики).
Определение. move(direction) возвращает True, только если сетка изменилась — тогда добавляется новая плитка 2/4 и проверяется can_move(). Иначе ход "пустой" и счёт не меняется.
Создайте game_logic.py — в нём нет import kivy. Такой модуль можно тестировать из REPL и pytest.
Состояние игры
class Game2048:
SIZE = 4
def reset(self):
self.grid = [[0] * self.SIZE for _ in range(self.SIZE)]
self.score = 0
self.won = False
self.game_over = False
self._add_random_tile()
self._add_random_tile()
Разбор.
grid— двумерный список 4×4;0— пустая клетка.won— флаг "достигли 2048";game_over— нет ходов.- После сброса сразу две случайные плитки — как в оригинальной игре.
Слияние строки
Метод _compress_and_merge(line):
- убирает нули;
- объединяет соседние равные числа (один раз за ход на пару);
- дополняет нулями до длины 4;
- начисляет очки в
self.score.
Направления:
_move_left— построчно;_move_right— строка в обратном порядке, merge, снова reverse;_move_up/_move_down— обход по столбцам.
move(direction) возвращает True, если поле изменилось. Только тогда добавляется новая плитка и проверяется can_move().
Проверка без GUI
from game_logic import Game2048
g = Game2048()
print(g.grid)
g.move("left")
print(g.score, g.grid)
Самопроверка
- После
resetна поле ровно две ненулевые клетки. - Повторный
moveв ту же сторону без эффекта возвращаетFalse. - При слиянии 2+2 счёт увеличивается на 4.
-
can_move()возвращаетTrue, пока есть пустые клетки или соседние равные.
Этап 2 — виджет Tile
Цель. одна ячейка поля — число на цветном фоне с закруглёнными углами.
Плитка — наследник Widget с дочерним Label и фоном RoundedRectangle в canvas.before:
from kivy.graphics import Color, RoundedRectangle
from kivy.metrics import dp
from kivy.uix.label import Label
from kivy.uix.widget import Widget
TILE_COLORS = {
0: ("#cdc1b4", "#cdc1b4"),
2: ("#eee4da", "#776e65"),
4: ("#ede0c8", "#776e65"),
# ... 8, 16, 32, ... 2048
}
class Tile(Widget):
def set_value(self, value):
self.value = value
self._label.text = "" if value == 0 else str(value)
bg, fg = TILE_COLORS.get(value, ("#3c3a32", "#f9f6f2"))
# перерисовать RoundedRectangle и цвет текста
Разбор.
canvas.before— фон под текстом Label.- Пустая клетка (
value == 0) — без цифры, нейтральный фон. tile_font_size(value)— для 1024+ уменьшайте шрифт (dp(42)→dp(22)), иначе число не влезет.bind(size=..., pos=...)→_redraw— при ресайзе окна плитка остаётся на месте (частая ошибка).
Палитра повторяет классическую схему веб-версии 2048.
Самопроверка
- Плитки с 2 и 128 визуально различаются цветом и размером цифры.
- При изменении размера окна плитка масштабируется.
Этап 3 — BoardGrid
Цель. 16 плиток в GridLayout, синхронизация с game.grid.
class BoardGrid(GridLayout):
def refresh(self):
flat = [cell for row in self.game.grid for cell in row]
for tile, value in zip(self.tiles, flat):
tile.set_value(value)
Разбор.
cols=4— фиксированная сетка;spacingиpaddingвdp— отступы между плитками.refresh()вызывается после каждого успешногоgame.move()— UI всегда отражает модель.- Порядок
flat— построчно слева направо, сверху вниз; должен совпадать с индексациейgrid[row][col].
Самопроверка
- Сетка 4×4 заполняет центральную область с отступами
dp(8). - После тестового
moveв REPL иrefresh()цифры на экране меняются.
Этап 4 — экран GameScreen
Цель. полный экран — заголовок, счёт, поле, кнопка, статус.
Соберите вертикальный BoxLayout (или FloatLayout с вложенным столбцом):
- заголовок "2048";
- блоки СЧЁТ и РЕКОРД (
ScoreBox—BoxLayoutсRoundedRectangleфоном); - подсказка про свайпы;
- контейнер поля с фоном
BOARD_COLOR; - кнопка "Новая игра" →
game.reset()+refresh_ui(); status_label— текст "Игра окончена!" / "Вы достигли 2048!".
def refresh_ui(self):
self.board.refresh()
self.score_box.set_score(self.game.score)
if self.game.game_over:
self.status_label.text = "Игра окончена!"
elif self.game.won:
self.status_label.text = "Вы достигли 2048!"
else:
self.status_label.text = ""
Разбор.
refresh_ui()— единая точка обновления UI после логики; не размазывайте обновление счёта по разным методам.- Кнопка через
bind(on_press=self.new_game)— стандартный паттерн Kivy (обзор).
Самопроверка
- Кнопка "Новая игра" сбрасывает поле и счёт текущей партии.
- Счёт обновляется после ходов (временно — вызов
do_moveиз кода или клавиатуры).
Этап 5 — свайп
Цель. ход по жесту — основной способ игры на телефоне.
В GameScreen сохраняйте _touch_start в on_touch_down, в on_touch_up вычисляйте смещение:
SWIPE_THRESHOLD = dp(30)
if abs(dx) < self.SWIPE_THRESHOLD and abs(dy) < self.SWIPE_THRESHOLD:
return # короткий тап — не ход
if abs(dx) > abs(dy):
direction = "right" if dx > 0 else "left"
else:
direction = "up" if dy > 0 else "down"
self.do_move(direction)
do_move вызывает game.move(direction) и при True — refresh_ui().
Разбор.
- Порог отсекает случайные тапы по кнопке "Новая игра".
- Сравнение
|dx|и|dy|выбирает доминирующее направление — диагональный свайп станет горизонтальным или вертикальным. collide_pointвon_touch_down— touch только внутри игрового экрана.
Тот же приём — в Kivy — Snake, этап 6, и в теории сенсорного ввода.
Самопроверка
- Свайп влево сдвигает плитки влево (мышью на ПК и пальцем на телефоне).
- Короткий тап не считается ходом.
Этап 6 — клавиатура
Цель. удобная отладка на десктопе без тачскрина.
from kivy.core.window import Window
# в Game2048App.build():
screen = GameScreen()
Window.bind(on_key_down=screen.on_key_down)
return screen
Коды стрелок Kivy: 273–276. WASD: w=119, a=97, s=115, d=100. R — новая игра.
Разбор.
- Обработчик должен вернуть
True, если клавиша обработана — иначе Kivy может передать её другим виджетам. - На Android клавиатура в игре обычно не нужна; блок можно оставить только для десктопа.
Самопроверка
- Стрелки и WASD двигают плитки.
-
Rзапускает новую партию.
Этап 7 — рекорд в JsonStore
Цель. сохранить лучший счёт между запусками приложения.
from kivy.storage.jsonstore import JsonStore
self.store = JsonStore("scores.json")
self.best_score = (
self.store.get("best")["score"] if self.store.exists("best") else 0
)
# в refresh_ui, при обновлении счёта:
if self.game.score > self.best_score:
self.best_score = self.game.score
self.store.put("best", score=self.best_score)
Разбор.
JsonStore— простой key-value на JSON-файле; для одного рекорда достаточно.- Для сложных сохранений смотрите SQLite (работа с БД в Python) — здесь избыточно.
Самопроверка
- После перезапуска приложения рекорд отображается в блоке "РЕКОРД".
- Файл
scores.jsonпоявляется в рабочей папке.
Этап 8 — полная ревизия
Цель. убедиться, что игра цельная и готова к следующему практикуму или сборке APK.
Сверьте проект с суммой листингов этапов 0–7 — вместе они дают полную реализацию.
Управление в финальной версии
| Ввод | Действие |
|---|---|
| Свайп | Сдвиг плиток |
| Стрелки / WASD | То же на ПК |
R | Новая игра |
| Кнопка "Новая игра" | Сброс партии |
Итоговая самопроверка
- Можно набрать 2048 (или проиграть при заполнении поля).
- Рекорд пишется в
scores.json. -
game_logic.pyне импортирует Kivy — модель тестируется отдельно. - UI обновляется только через
refresh_ui()/board.refresh().
Дальше — Kivy — Pong (непрерывная физика и Clock) или сборка APK.