2048 для PocketBook

Портирование браузерной игры на электронную книгу

2048 на устройстве

Игра 2048 появилась в 2014 году — Gabriele Cirulli написал её за выходные на JavaScript и выложил в открытый доступ. Механика простая: плитки с числами скользят по полю 4×4, одинаковые складываются, цель — получить плитку 2048. Игра мгновенно стала популярной и обросла портами на все мыслимые платформы.

У меня есть электронная книга PocketBook, и я решил добавить её в этот список.

PocketBook SDK и InkView

Приложения для PocketBook пишутся на C с использованием проприетарного API — InkView. SDK поставляется с кросс-компилятором для ARM Linux и эмулятором для Windows. Сама разработка выглядит довольно старомодно: никаких фреймворков, никакого UI toolkit — только функции вроде DrawTextRect, FillArea, SetFont. Зато всё под контролем, что для e-ink вполне разумно.

Для того чтобы разрабатывать и тестировать приложения не перекидывая каждый раз файл на устройство, я сделал симулятор на базе Emscripten и WebAssembly — C-код компилируется в WASM, а InkView API реализован поверх Canvas 2D в браузере.

Реализация

Вся игра написана в одном файле game2048.c на чистом C. Никаких внешних зависимостей, никаких изображений — только вызовы InkView API.

Плитки отрисовываются прямоугольниками с заливкой и узором из линий. Поскольку экран e-ink — оттенки серого, цвета задаются через макрос:

1
#define GS(v) (((v)<<16)|((v)<<8)|(v))  /* 0xVV → 0xVVVVVV */

Двенадцать уровней плиток — от светло-серых до чёрных, с горизонтальной штриховкой, вертикальной или сеткой. Плитка 2048 — чёрная сплошная.

Анимация работает через SetHardTimer — это однократный таймер, который нужно перезапускать каждый кадр. Два этапа: скольжение плиток (110 мс) и «хлопок» слияния (180 мс). Время измеряется через emscripten_get_now() в симуляторе, и через gettimeofday() на устройстве — всё это скрыто за #ifdef __EMSCRIPTEN__.

Проблемы

C89

SDK поставляется со старым компилятором ARM GCC, который по умолчанию работает в режиме C89. Это означает: никаких объявлений переменных внутри for, никаких инициализаторов составных типов, никакого bool из стандартной библиотеки. Пришлось переписать весь код:

1
2
3
4
5
6
/* C99 — не работает */
for (int i = 0; i < n; i++) { ... }

/* C89 — окей */
int i;
for (i = 0; i < n; i++) { ... }

Вместо (MoveRec){r, c, dr, dc} — явное присваивание полей. Вместо #include <stdbool.h> — переносимый вариант:

1
2
3
4
5
#ifndef __bool_true_false_are_defined
typedef int bool;
#define true 1
#define false 0
#endif

Определение ориентации при запуске

Приложение получает событие EVT_ORIENTATION только при смене ориентации, но не при старте. Из-за этого переменная is_landscape оставалась false независимо от реального положения устройства, и в режиме ландшафта всё рисовалось неправильно.

Решение простое — в EVT_INIT явно спросить у системы:

1
is_landscape = (GetOrientation() == 1 || GetOrientation() == 2);

Мигание экрана

После каждого хода вся игра перерисовывалась через FullUpdate() — стандартный полный рефреш e-ink. На экране это выглядит как заметное мигание: экран сначала уходит в чёрный, потом полностью отрисовывается заново.

Для первого показа FullUpdate() нужен — он даёт максимальное качество. Но после каждого хода достаточно SoftUpdate(), который обновляет экран без полного цикла перезаписи:

1
2
static void draw_game_full(void) { draw_game_buf(); FullUpdate(); }  /* при запуске */
static void draw_game(void) { draw_game_buf(); SoftUpdate(); } /* после хода */

Со временем на e-ink появляются артефакты от мягких обновлений — «призраки» старых плиток. Но для игры это некритично, и пользователь всегда может сделать полное обновление вручную.

Размер на экране

На устройстве игра занимала четверть экрана — оказалось, что у PocketBook 631 экран 1264×1680, а весь layout был захардкожен под 600×800.

Исправил масштабированием: вычисляю коэффициент при старте приложения и применяю его ко всем координатам через макрос S():

1
2
3
4
5
6
7
8
static float g_scale = 1.0f;
#define S(v) ((int)((v) * g_scale))

static void update_scale(void) {
int sw = ScreenWidth(), sh = ScreenHeight();
float sx = sw / 600.0f, sy = sh / 800.0f;
g_scale = sx < sy ? sx : sy;
}

После этого все вызовы вроде FillArea(S(375), S(15), S(100), S(58), ...) масштабируются автоматически. Размеры шрифтов тоже умножаются на g_scale. При смене ориентации шрифты закрываются и открываются заново с новым размером.

Итог

Игра работает на устройстве: свайп двигает плитки, анимация плавная насколько позволяет e-ink, счёт сохраняется между сессиями. Код — на GitHub, в репозитории есть готовые бинарники для PB631 и для Windows-эмулятора.