Во время разработки нашей Match 3 игры появилась необходимость в тестировании проходимости уровней, идея ручного тестирования очень быстро отпала из-за никзой скорости. После этого стали думать на счет автоматического тестирования. Сразу вспомнилась статья на Хабре, где ребята делилилсь подобным опытом. Их идея нас не очень удоавлетворила из-за того, что они тестировали через GUI, хоть и с минимальными анимациями. Этот способ быстрее ручного тестирования, но это не максимум. В своей игре мы решили пойти другим путем - тестировать уровни исключительно моделями игровых объектов и сущностей. В результате ухода из GUI к консоли скорость стала 3-5 минут на 198 уровней (в одном наборе 198 уровней) в зависимости от вида тестирования (вверх или вниз) и количества итераций. В этой статье, как раз, хотим рассказать о том, как проводилось тестирование уровней в нашей игре Travel Jewel (ссылка на Google Play будет указана в конце статьи). Сначала расскажем о том, что за игра, о чем она, о ее особенностях и отличиях от других Match 3 игр, затем о создании уровней и потом уже о самом тестировании, в конце выделим результаты тестирования. Итак, начнем.
Что за игра, о чем
Travel Jewel - классическая Match 3 игра, которая рассказывает истории о достопримечательностях крупных городов России. Дизайн выполнен из элементов корабля и вместо камней используются векторизированные изображения достопримечательностей. В качестве фоновой музыки выбран шум прибоя и крики чаек. Первый набор уровней содержит достопримечательности Петропавловска-Камчатского. Список городов составлен уже полностью, но об остальных городах пока не рассказываем. Механики используются классические: три камня - очки, четыре - очки + камень flash, пять - очки + супер-камень, который уничтожает все камни определенного типа, буква "L" - бомба. Кроме того, в игре реализовано автоматическое перемешивание камней в случае, когда больше нет камней для перемещения. В игре присутствует таблица рекордов трех типов: глобальная, по наборам уровней и по очкам за достижения. И да, в игре также есть ряд достижений, например, за каждый новый день захода в игру или за игру в 1, 5, 10, 50, 100, 150, 198 уровнях и за 3 звезды в них. Если реализовать поддержку Firebase и Unity Ads для iOS на pod'ах, то игру можно опубликовать и в App Store для iOS устройств. Исходя из чего, у всех устройств, даже на разных ОС будет одна большая таблица рекордов и кроссплатформенное сохранение результатов игры.
Игра написана на Java, на движке LibGDX с использованием библиотек Firebase (database, auth, storage, performance, analytics) и Crashlytics, который скоро будет доступен из Firebase. Помимо этого в Firebase используется Cloud Function, которая обновляет таблицу рекордов раз в час.
О создании уровней
О создании уровней много не скажешь, потому что все layout'ы создавались вручную, после этого был сгенерирован список целей для игрока на каждый уровень, как они будут меняться в зависимости от номера уровня и в завершение, все блоки (за камнями которые), решетки и льды были расставлены тоже вручную. В качестве средства для создания уровней использовался Excel. На листе располагалось 4 поля 8х8, первое отвечает за блоки, второй за наличие камня в ячейке, третий - за решетки и четвертый за льды. Проверка на валидность, что не стоит решетка на ячейке, в которой нет камня, делается на формулах. Для экспорта всех данных в файлы и дополнительных проверок были написаны макросы. После этого файл можно сделать более читаемым прогоном через https://jsonlint.com/, например, если это необходимо. Дальше файл отправляется через Firebase Function в Firebase Database, чтобы ключи были сгенерированы по стандартам Google (через push) и не менялись после изменений уровней при повторных заливках, короче говоря, для удобства личного и пользователей. Для пользователей - чтобы после каждого обновления уровней не надо было переигрывать заново. Для первого набора уровней такого способа создания уровней вполне достаточно. В будущем, для второго набора уровней можно уже что-то более дружественное придумать без такого количества этапов и ручной копи-пасты.
О тестировании
Первоначально количество ходов и значения для звезд подбиралось пальцем в небо исходя из логических соображений, что создавало сложности при ручном тестировании - уже на первых уровнях возникали огромные сложности при прохождении и получении хотя бы какого-то количества звезд. В итоге было решено написать бота для получения количества необходимых ходов для завершения целей и значений для звезд. Бот претерпел много изменений с самого начала и теперь выполняется в три этапа: считает сколько необходимо ходов для завершения цели, корректирует эти значения, чтобы шанс прохождения был 60% и затем на этих ходах получает значения для звезд, чтобы с тем же шансом можно было получить 3 звезды. Все шаги можно объединить в один, но это может усложить понимание кода и сделает невозможным или неудобным иметь, так сказать, snapshot промежуточного результата.
Код самой игры приводить не будем, потому что статья не об этом, будут приведены статическая диаграмма классов модели объектов игры и код только самого бота.
После этого необходим базовый класс «игры», который будут расширять классы определенного вида тестирования - вверх или вниз.
Базовый класс «игры»
Базовый класс содержит логику инициализации данных уровня (счета, игрового поля и целей), логику ведения счета и произведения хода, проверку на завершенность целей. К сожалению, на pikabu нет подсветки кода и возможности сворачивать блоки, поэтому выкладываю код на pastebin, чтобы размер поста был адекватным: https://pastebin.com/NqWc3SJt.
Вспомогательные классы базового класса: https://pastebin.com/6w2udRtZ.
Отдельно хотелось бы остановиться на логике совершения хода. Сначала из класса Field получаем список всех возможных ходов и дальше выбираем самый эффективный. Первым выполняется поиск супер-камней, которые уничтожают камни одинакового типа. Приоритет отдается камню, который имеет больше всего похожих камней, то есть с максимальной силой. Стоило бы, конечно, отдавать приоритет согласно целям, есть над чем поработать. Если первый поиск не дал результатов, то выполняется поиск для завершения целей по типам этих целей и случайным образом выбирается конкретный ход для выполнения цели. Дальше смотрим на ходы, где есть камни с любым эффектом, например, flash. Предпоследними проверками ищутся ходы на 5, 4 и 3 камня, если все предыдущие не дали результатов. Если же ни один из них не дал результатов, то выбирается случайный ход. После этого из хода считаются индексы для камня, которым будет совершен ход и индексы, куда будет совершен ход. В завершение – ход выполняется.
UpwardGame
Как не трудно догадаться, этот класс реализует только один метод - start, который в цикле проверяет завершенность целей, делает ход из базового класса и увеличивает счетчик ходов. По завершении работы делает запись в лог о количестве ходов и набранном счете, возвращает результат с теми же данными.
Класс UpwardGame: https://pastebin.com/viQyiAQb.
Тестирование вверх всегда выигрышное, поэтому и возврщается статус WON.
DownwardGame
DownwardGame делает почти то же самое, что и UpwardGame: в цикле проверяет завершенность целей, делает ход и базового класса, но уменьшает счетчик ходов. В случае, если ходы закончились, то цикл завершается. Если при нуле ходов цели все-таки выполнены, то игра считается выигранной. В зависимости от этого делается запись в лог и возвращается соответствующий результат.
Класс DownwardGame: https://pastebin.com/QdkU8rzB.
Метод upward-тестирования
Думаю не надо объяснять построчно, что делает метод. Основная суть в том, что "играется" 1000 игр на каждый уровень, по средним значениям ходов и набранных очков выбираются значения для необходимого количества ходов и звезд для завершения уровня. Из среднего счета звезды получаются следующим образом:
int star1 = (int) (scoreAv * .33f / 100) * 100;
int star2 = (int) (scoreAv * .66f / 100) * 100;
int star3 = (int) (scoreAv / 100f) * 100;
Метод upwardTest: https://pastebin.com/wwzqbKVA.
Данные по наборам уровней хранятся в обычном json-файле (до отправки в Firebase Database, конечно) и читаются с помощью Gson через класс обертку LevelPacks, который содержит только одну публичную property - ArrayList<LevelPack> levelPacks.
Метод downward-тестирования для коррекции количества ходов
Метод прогоняет 100 игр также на каждый уровень и проверяет какой процент пройденных. Если значение меньше, чем 60+-1, то количество ходов увеличивается на единицу, иначе уменьшается.
Метод downward-тестирования для коррекции количества ходов: https://pastebin.com/QsjDAKn3.
Метод downward-тестирования для коррекции значений для звезд
Метод прогоняет также 100 игр и проверяет какой процент игр набрал 3 звезды. Если меньше, чем 60+-1, то значение последней звезды уменьшается на 100, иначе увеличивается на то же значение, после этого первые две звезды рассчитываются также как и при upward-тестировании.
Метод downward-тестирования для коррекции значений для звезд: https://pastebin.com/BGme8p9c.
В конце тестирования все найденные значения проходили логическую проверку глазами. Например, на 25 уровне скорректированное значение для первой звезды - 0, что выглядит неприятно. Оно менялось на 100, а для второй звезды на 200, вместо 100 рассчитанных.
Результат тестирования
После автоматического тестирования проходимости уровней мы получили следующее:
- теперь стало возможно пройти уровни с высоким шансом на успех и звезд можно получить почти всегда 3, тогда как раньше, повторюсь, сложно было пройти уровень и получить хотя бы одну звезду; раньше на 5 уровней требовалась неделя для прохождения и корректировки количества ходов и значений для звезд, сейчас же за один день можно пройти ~20 уровней и не утомиться, к тому же даже не меняя количество ходов;
- тестирование помогло исправить огромное количество багов в логике игры;
- автоматическое тестирование по прежнему требует ручного из-за небольшого количества лишних ходов, однако, можем смело утверждать, что их количество не превышает 25%, что при чистом рандоме для создания камней может кому-то помочь выиграть.
Кроме того, после перехода на автоматическое тестирование выросла скорость тестирования по отношению к ручному - теперь меньше, чем за 5 минут на набор уровней можно получить проходимые уровни.
Послесловие
Да, бот не идеален, он берет только верхние действия и не по всем веткам проходит из-за неверной логики рандома в базовом классе "игры", в методе updateActions верхние два из нижних трех блоков никогда не выполняются (догадка, пока не проверяли). Кроме этого, некоторые ветки выбора также можно улучшить. Например, как отмечалось ранее, при нахождении супер-камня отдавать предпочтение для совмещения тому, который находится в целях.
Возможно у вас будет информация о генерации проходимых и симпатичных уровней определенного размера для Match 3, будем крайне признательны за подобного рода информацию. Также будем благодарны за комментарии и пожелания, особенно по коду!