Баланс игры ломается не багом, а цифрами: как я ловил эксплойты симулятором
Я делаю idle tower defense: башня в центре, враги лезут волнами, игрок покупает апгрейды. Все тесты зелёные, каждая кнопка работает, каждый апгрейд делает ровно то, что написано. А потом тестер присылает скрин: волна 199, 70 тысяч монет, «игра пройдена, скучно». До этого я сам еле доходил до 60-й.
Багов в привычном смысле не было. Не было краша, неверной цифры на экране, забытой проверки. Сломался баланс: несколько честно работающих механик в сумме давали бессмертие. Такое не ловится ни юнит-тестами, ни ручным прогоном, и об этом эта статья.
Про читы и атаки на экономику у меня уже был отдельный разбор. Здесь другое: игрок ничего не взламывает, он просто играет лучше, чем ты рассчитывал.
Почему баланс не проверить руками
Ручной прогон проверяет то, до чего QA доиграл. А ломается баланс там, куда он не доиграл: на 150-й волне, после двадцати часов прокачки, при конкретной комбинации апгрейдов. В моей игре 46 апгрейдов, у каждого десятки уровней, плюс лаборатория, карты и перки. Комбинаций больше, чем можно перебрать за всю жизнь.
Вторая причина: баланс — это кривые, а не точки. Правильный вопрос не «сколько стоит третий уровень урона», а «с какой скоростью растёт сила игрока и с какой — сила врагов». Если первая кривая хоть где-то обгоняет вторую навсегда, игра бесконечна. Это видно только на графике за сотни волн.
Шаг 1: отделить логику боя от движка
Чтобы гонять тысячи боёв, логика должна запускаться без графики. У меня весь бой — чистый C# без Unity: враги, пули, урон, экономика. Unity только рисует то, что насчитала симуляция.
Если в вашем проекте бой живёт в MonoBehaviour и физике, это первое, что стоит поменять ради тестируемости. Без этого баланс проверяется только глазами.
Шаг 2: симулятор игрока
Симулятор — это бот, который играет за человека:
- стартует ран с текущей прокачкой;
- во время боя тратит кэш на апгрейды (жадно: самый дешёвый полезный);
- после смерти тратит монеты в мастерской;
- повторяет 60 ранов и пишет таблицу: ран, волна, минуты, заработок, уровни.
Бот играет хуже человека, и это нормально. Он показывает нижнюю границу: если даже тупой бот за 10 ранов проходит 100 волн, игра слишком лёгкая. Если он 15 ранов подряд умирает на одной волне, там невидимая стена.
Такая стена у меня нашлась в первый же прогон: снайперы останавливались на дистанции 3.2, а радиус башни был 2.8. Игрок физически не мог их достать, и 15 ранов, около двух часов, все умирали на волнах 11–14. Руками я бы это списал на «надо больше качаться».
Шаг 3: стресс-тест «максимального» игрока
Обычный симулятор проверяет путь новичка. Эксплойты живут в конце игры, поэтому нужен второй тест: сразу дать боту много прокачки и смотреть, умрёт ли он вообще.
Я прогонял набор билдов, каждый до смерти башни, но не дольше 10 часов игрового времени:
| Билд | Что включено |
|---|---|
| ws0 | ничего, чистый старт |
| ws10 | все апгрейды по 10 уровней |
| ws10 + labs / cards / perks / ult | по одному источнику силы сверху |
| ws10 + all, ws30 + all, ws60 + all | всё сразу |
Главная метрика — не «до какой волны дошёл», а почему ещё жив. Для этого в отчёт я выводил урон башни рядом с HP обычного врага на той же волне.
Первый же прогон дал строку, которая сразу всё объяснила: башня с уроном 101 жива на волне 241, где у обычного врага 54 000 HP. Чтобы убить одного врага, ей нужно больше 500 попаданий. Значит, враги умирают не от выстрелов, и надо искать, от чего.
Шаг 4: зонд — что именно держит башню
Когда симулятор говорит «бессмертен», нужно залезть внутрь. Я написал зонд: каждые 20 волн он печатает состояние боя. Сколько врагов живо, сколько дошло до башни, минимальная дистанция, HP башни и все защитные статы.
Вывод был неожиданным: до башни не доходил никто. Ноль врагов у стены на 160-й волне. И ещё одна странность: HP башни 1162 из 202 максимальных.
Так нашлись четыре дыры. Каждая механика по отдельности работала правильно:
- Отбрасывание зажимало врагов. 78% шанс откинуть врага на 2 клетки при каждом попадании. Враг делал шаг вперёд и тут же летел назад. Башня стреляла быстрее, чем враги успевали приблизиться.
- Орбы убивали мгновенно. Орбы вокруг башни убивали любого обычного врага с одного касания, сколько бы у него ни было HP. Рост HP врагов переставал иметь значение.
- Шипы считались от HP врага. Враг, ударивший башню, терял процент от своего максимального HP. Чем сильнее враг, тем больнее он бил сам себя, и рост врагов снова ничего не давал.
- Лечение сверх максимума копилось. Аптечки лечили выше максимума, лимит был ×5–6 HP. Башня жила с шестикратным запасом.
Отдельно тестер нашёл ещё две дыры: рапид-файр запускал сам себя почти навсегда, а лаборатория давала ×7 к урону и HP за копейки.
Как чинил: привязать всё к силе игрока
Общий корень у всех дыр один: механика скейлилась от чего-то, кроме силы игрока, например от HP врага или от процента без потолка. Такие механики неизбежно ломают кривые на длинной дистанции.
- Орбы теперь бьют на 8× урона башни, а не убивают мгновенно.
- Шипы: процент от HP врага, но не больше 25× урона башни.
- Толчок отбрасывания меньше в 5 раз, максимальный шанс 50% вместо 80%.
- Лечение сверх максимума ограничено ×3.
- Рапид-файр: шанс считается раз в секунду, а после очереди идёт перезарядка.
- Лаборатория: 15 уровней вместо 40, цена растёт круто.
Повторный стресс-тест: каждый билд теперь умирает. Со всем открытым по 10 уровней башня доходит примерно до 175-й волны за 100 минут, с полной прокачкой — до 600-й. Путь новичка почти не изменился.
Что встроить в процесс
Главное, что я вынес: баланс — такой же объект тестирования, как API. У него должны быть автотесты, которые гоняются после каждой правки цифр.
- Симулятор новичка в CI или хотя бы перед релизом. Если кривая «волна за ран» сдвинулась больше чем на X%, смотреть глазами.
- Стресс-тест максимума. Правило простое: ни один билд не должен жить вечно. Строка «ALIVE» в отчёте — это баг.
- Юнит-тесты на механики, например «орбы не убивают боссов», «заблокированный апгрейд не даёт эффекта». Они ловят регрессии дешевле симулятора.
- Все цифры вне кода. У меня баланс лежит в JSON и дублируется в Remote Config. Нашли дыру после релиза — правим в консоли, без обновления игры.
Чек-лист баланса для QA
- Бой запускается без графики, можно прогнать тысячу раундов за минуты.
- Есть симулятор игрока с таблицей «ран → волна → время → заработок».
- Нет «стены»: бот не умирает на одной волне 10 ранов подряд.
- Нет «лифта»: бот не проходит сотню волн за первые ранов пять.
- Стресс-тест максимальной прокачки: каждый билд в итоге умирает.
- Любой эффект, который скейлится не от силы игрока (от HP врага, процент без потолка), проверен отдельно.
- Каждый источник силы (лаба, карты, перки, ульты) проверен отдельно и все вместе.
- Эффекты с шансом не перезапускают сами себя.
- Заблокированный апгрейд не даёт эффекта, даже если у него есть уровни.
- Числа баланса можно поменять без релиза.
Коротко
Баланс ломается не багом, а цифрами: каждая механика верна, а вместе они дают бессмертие. Руками это не найти, нужен бот, который сыграет тысячу раз, и стресс-тест, который спросит «а он вообще умирает?». Самые опасные механики — те, что скейлятся не от силы игрока. Их проверяйте первыми.