game-qaeconomybalanceautomationqa

Баланс игры ломается не багом, а цифрами: как я ловил эксплойты симулятором

Я делаю 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 максимальных.

Так нашлись четыре дыры. Каждая механика по отдельности работала правильно:

  1. Отбрасывание зажимало врагов. 78% шанс откинуть врага на 2 клетки при каждом попадании. Враг делал шаг вперёд и тут же летел назад. Башня стреляла быстрее, чем враги успевали приблизиться.
  2. Орбы убивали мгновенно. Орбы вокруг башни убивали любого обычного врага с одного касания, сколько бы у него ни было HP. Рост HP врагов переставал иметь значение.
  3. Шипы считались от HP врага. Враг, ударивший башню, терял процент от своего максимального HP. Чем сильнее враг, тем больнее он бил сам себя, и рост врагов снова ничего не давал.
  4. Лечение сверх максимума копилось. Аптечки лечили выше максимума, лимит был ×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

  1. Бой запускается без графики, можно прогнать тысячу раундов за минуты.
  2. Есть симулятор игрока с таблицей «ран → волна → время → заработок».
  3. Нет «стены»: бот не умирает на одной волне 10 ранов подряд.
  4. Нет «лифта»: бот не проходит сотню волн за первые ранов пять.
  5. Стресс-тест максимальной прокачки: каждый билд в итоге умирает.
  6. Любой эффект, который скейлится не от силы игрока (от HP врага, процент без потолка), проверен отдельно.
  7. Каждый источник силы (лаба, карты, перки, ульты) проверен отдельно и все вместе.
  8. Эффекты с шансом не перезапускают сами себя.
  9. Заблокированный апгрейд не даёт эффекта, даже если у него есть уровни.
  10. Числа баланса можно поменять без релиза.

Коротко

Баланс ломается не багом, а цифрами: каждая механика верна, а вместе они дают бессмертие. Руками это не найти, нужен бот, который сыграет тысячу раз, и стресс-тест, который спросит «а он вообще умирает?». Самые опасные механики — те, что скейлятся не от силы игрока. Их проверяйте первыми.