Игра "Fifteen Match"
Дата публикации: 29/03/2022Несколько месяцев назад я решил участвовать в хакатоне Flutter Puzzle Hack, где нужно было написать вариацию игры Пятнашки на Flutter. В данный момент соревнование завершилось и выложены работы всех претендетов. Моя версия доступна вот по этой ссылке. Ниже я описываю процесс создания игры и особенности реализации игры.
Итак, задача была создать вариацию знаменитой головоломки 15. Как известно, в своей основе она обманчива проста и ее достаточно легко реализовать. Сложность заключается только в проверка решаемости головоломки. Но эта задача математиками уже была решена и есть достаточно простой алгоритм, который позволяет проверить любую позицию на решаемость.
Идея
Но как сделать новую интересную головломку на базе пятнашек? Я начал думать и через некоторое время нашел интересное решение. Что если мы будем отображать на фишке не ее значение, а какой-то производное значение. Я начал с того, что решил рассчитать для каждой фишки расстояние между ее текущим и корректным положением. Для стандартного поля 4x4 это дает нам одно из чисел в диапазоне [0..3]. Теперь осталось как-то использовать эти числа. Я решил, что можно эти числа закодировать цветами, тогда исходное поле буде представлено в виде разноцветной мозаики. Таким образом основная идея выкристаллизовалась и я приступил к разработке.
Дизайн
Первым делом я сделал небольшой прототип на SwiftUI, чтобы убедиться в привлекательности и работоспособности идеи. Теперь дело было за дизайном, который я нарисовал в Sketch. Изначально один экран с игрой у меня превратился в полноценную головоломку. Я решил сделать несколько разных уровней, в основе которых лежала вышеизложенная идея.
С точки зрения дизайна, я решил, что у меня будет несколько разных видов фишек (квадратные, круглые, нестандартной формы, разных размеров и т.д.). Также я подумал о том, что уровни будут объединены в коллекции, каждая из которых будет использовать один из типов фишек. Для каждой коллекции я решил использовать свою цветовую палитру из пяти цветов. Эти цвета будут кодировать какую-нибудь из характеристик фишки (цвет, размер), а также использоваться для создания фонового градиента.
При создании дизайна я сразу рассчитывал, что игра будет поддерживать вертикальную и горизонтальную ориентацию. Поэтому сразу отрисовал основные экраны для всех нужных размеров. Это потом помогает грамотно верстать экраны во Flutter. Также пришлось отдельно отрисовать дизайн для iPad и web, т.к. они из-за своего размера требует несколько другого расположения элементов игры.
Логика игры
Приложение построено по классической схеме Model-View-Controller. Основное игровое поле представлено в виде обычного массива. Индекс элемента массива определяет корректное положение фишки, а значение элемента его текущее положение. Для стандартных пятнашек 4x4 это будет выглядеть следующим образом:
pieces = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
Индексация начинается с нуля, X и Y координаты рассчитываются для конкретной фишки динамически по формулам:
// Размерность игрового поля int gridSize = 4 // Текущее положение фишки int X = pieces[index] % gridSize int Y = pieces[index] ~/ gridSize
Вся логика игры инкапсулирована в объект Game(), который отвечает за начальную генерацию позиции, проверку ее решаемости, перемещение фишке, а также за отслеживание количества ходов, затраченного времени и проверок на окончание игры.
Проверка решаемости производится по классическому алгоритму с подсчетом количества инверсий. Инверсия это когда значение фишки меньше, чем у соседа справа, т.е. больший элемент находится левее меньшего. Пустая фишка (в нашем случае _15_), в подсчете не участвует. Для полностью решенной голововломки, количество инверсий равно нулю.
// Полностью решенная головоломка pieces = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, __] inversions(pieces) -> 0 // Фишка 11 передвинута на пустую позицию (головоломка решаема) pieces = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, __, 12, 13, 14, 11] inversions(pieces) -> 1 // Переставлены местами фишки 13 и 14 (головоломка нерешаема) pieces = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 13, __] inversions(pieces) -> 1
Зная количество инверсий мы можем проверить решаема ли данная позиция в принципе или нет. Для этого нам необходимо знать размерность игрового поля и номер ряда в которой находится пустая фишка. Номер ряда считается снизу начиная с единицы.
При четной размерности игрового поля, головоломка решаема, если количество инверсий четное, а номер ряда в которой находится пустая фишка нечетный и наоборот.
int gridSize = 4 int inversion = 2 // четное int rowEmptyFromBottom = 1 // нечетное // Проверяем решаемость позиции для четной размерности bool isSolvable if (gridSize % 2 == 0) { if (rowEmptyFromBottom % 2 == 1) { isSolvable = (inversions % 2 == 0) } else { isSolvable = (inversions % 2 == 1) } } // Головоломка решаемая isSolvable -> true
Для случая если размерность игрового поля нечетная, положение пустой фишки не имеет значение, головоломка всегда решаемая если количество инверсий четное.
int gridSize = 3 int inversion = 2 // четное // Проверяем решаемость позиции для нечетной размерности bool isSolvable if (gridSize % 2 == 1) { isSolvable = (inversions % 2 == 0) } // Головоломка решаемая isSolvable -> true
Используя данный алгоритм мы можем как проверить любую позицию на решаемость, так и сгенерировать любую заранее решаемую позицию.
Уровни
Для того, чтобы игра была более интересной, я добавил в нее концепцию коллекций, каждая из которых содержит несколько уровней. Как уже было упомянуто выше, я решил кодировать значение фишек цветами и размерами в зависимости от удаленности самой фишки от ее корректного положения.
Общие характеристики уровня, к которым относится цветовая палитра и тип фишки, хранятся в описании коллекции. А сами уровни содержат только исходную расстановку фишек. Для того, чтобы разделить уровни по сложности, я также ввел в дополнение к полю 4x4, поддержку размерностей 3x3 и 5x5. Кроме этого для более простых уровней я ввел возможность показа числового значения фишки. Вся эта дополнительная информация хранится в описании уровня.
Целью игры, таким образом, явлется решение исходной позиции. В процессе решения головоломки, чтобы облегчить задачу пользователю, на тех фишках, которые оказываются на своем месте показывается их числовое значение. Как только уровень решен, он помечается пройденным и игроку предлагается перейти к решению следующего уровня.
Типографика и цветовые палитры
Все шрифты и их оформление вынесены в отдельный стилевой файл. Я определил нужные семантические стили для всех типов текста, использумых в приложении. И таким образом значительно упростился как сам код текстовых виджетов, так и общее управление стилями теперь сосредоточено в одном месте.
С цветами ситуация немного другая. Так как я решил поддерживать коллекции, которые сами определяют цветовые палитры для уровней, то выносить их в отдельный общий стилевой файл, как я сделал с текстами, не было смысла. Управление цветами было полностью отдано коллекции. Единственное исключение это два универсальных цвета - черный и белый, которые используются для текста и иконок, в зависимости от того светлая или темная тема у конкретной коллекции. Эти цвета были прописаны в коде программы напрямую.
Поддерживаемые устройства
Приложение в первую очередь писалось под смартфоны, причем как в портретной, так и в ландшафтной ориентаций. При изменении ориентации, происходит автоматическая перестройка пользовательского интерфейса. Кроме этого, учитывая разнообразие размеров экранов у смартфонов, приложение старается подстраиваться под разные размеры, оставаясь при этом приятным для глаз и удобным для игры.