← Все записи

Технологии5 мин чтения

Бой без единой картинки: сначала тесты

Первые бои Terminal Leave шли только в автотестах: симуляция, сервер боя, боты и реплеи заработали раньше, чем на экране появился первый боец.

Первый бой с настоящими сетевыми клиентами выглядел как два терминала. Два тестовых игрока подключились к серверу боя, отдали приказы, закрепили их, и сервер разыграл раунд на карте промзоны 24 на 24 клетки. Один из игроков получил в ответ пустой список событий: противник был дальше радиуса его обзора, а линию огня закрывал контейнер. Ни карты, ни бойцов на экране не было — только JSON. Но это пустое место и было главным результатом: туман войны работал.

Клиента у него ещё не было совсем.

Почему в таком порядке

Причин три. Первая — в устройстве игры. Бой в Terminal Leave считает только сервер. Клиент ничего не решает: он отправляет приказы и рисует то, что ему прислали. Значит, правила можно написать и проверить без клиента вообще, а клиент потом станет просто ещё одним потребителем уже работающего сервера.

Вторая причина практическая. Для клиента нужна графика — тайлы, спрайты бойцов, анимации, — а её на тот момент не было. Я решил не рисовать временные квадраты, а остановиться перед клиентом и делать то, что от графики не зависит. Не зависело от неё почти всё: симуляция, сервер боя, аккаунты, боты, реплеи.

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

К тому же правила — самая рискованная часть проекта. Одновременные события, обзор, туман войны, случайность, которая должна в точности повторяться. Если бы это не сложилось в работающую систему, никакая картинка бы не помогла.

Детерминизм с первой строки

Реплеи в Terminal Leave — это не запись видео, а повторный расчёт боя. Чтобы расчёт с теми же приказами давал тот же результат, симуляция с самого начала строилась по нескольким правилам. Время — целые миллисекунды от начала раунда. Проценты и углы — целые числа в тысячных долях, без плавающей точки там, где от неё зависит исход. Случайность — свой генератор с зерном боя, разбитый на именованные потоки, чтобы бросок одного бойца не сдвигал броски другого.

Эти правила проверяет не только тест, но и линтер. В коде симуляции запрещены Math.random, Date.now, new Date, таймеры, сетевые запросы и чтение переменных окружения. Нарушение не проходит проверку ещё до запуска тестов.

Сами тесты детерминизма простые. Один бой прогоняется 25 раз с одним зерном, и журналы событий должны совпасть байт в байт. Результат сравнивается с эталонным снимком, а сборка проверяет его на двух версиях Node.js — если расчёт где-то зависит от платформы, снимок разойдётся.

Одновременность

Самый показательный тест того времени — взаимное убийство. Два бойца стреляют друг в друга в одну и ту же миллисекунду, и оба должны погибнуть. В пошаговой игре так не бывает: кто ходит первым, тот и стреляет. В игре с одновременными ходами это обычный исход, и правила обязаны его допускать, а не решать по тому, чей приказ сервер прочитал раньше.

Рядом — тесты на очередь движения, на занятые клетки, на выстрел сквозь стену, на овервотч, прерывающий чужой забег.

Сервер боя

Сессию боя я написал так, чтобы она не знала о сокетах: исходящие сообщения уходят в переданный ей приёмник. Поэтому весь цикл — подключение, планирование, закрепление приказов, расчёт раунда, рассылка результатов — проверяется обычным тестом без сети. Отдельные тесты потом прогоняют то же самое через настоящие WebSocket-соединения.

Сервер не верит клиенту. Время действий, которое прислал клиент, отбрасывается и считается заново. Приказ чужому бойцу, путь сквозь стену, повторное закрепление получают отказ с кодом ошибки, и на каждый такой код есть тест.

Туман войны проверяется строже всего. Боец ходит за стеной; тот, кто ходил, видит в своём результате два шага, его противник — ни одного, а полный журнал на сервере содержит всё. Важно, что противнику не просто не рисуют эти шаги: их нет в данных, которые к нему пришли.

Боты

Боты появились до клиента, потому что первый бой новичка — против бота, а отключившегося игрока тоже кто-то должен вывести с поля. Бот отдаёт приказы через ту же проверку, что и игрок, и видит только то, что видят его бойцы. Если противника не видно, он идёт к его точке высадки — это знание есть у любого игрока.

Тесты ведут такой бой от начала до конца, на несколько раундов, и проверяют, что он заканчивается победой одной из сторон, а не зависает.

Реплеи и сквозной тест

В конце боя сервер сохраняет полный файл реплея: карту, правила, начальные отряды, зерно, приказы каждого раунда и результат. Тест загружает его, пересчитывает бой и сравнивает с оригиналом.

Последним появился сквозной тест, который заменил ручную проверку. Он поднимает основной API и сервер боя с настоящим HTTP и WebSocket. Два гостя встают в очередь, их сводят в бой, бойцы блокируются, один игрок сдаётся. Итог боя применяется к аккаунтам дважды, и тест проверяет, что во второй раз ничего не изменилось. Затем бойцы снова свободны, а счётчик боёв увеличился.

Что это дало

Графика пришла "на следующий день", и клиенту достался проверенный сервер. Ошибки первых часов были ошибками отображения: боец смотрит не туда, анимация идёт слишком быстро, объекты рисуются серыми квадратами, в браузере не загружается движок отрисовки. Правила при этом не менялись.

Но у такого порядка есть и цена. Тест проверяет, что правило выполнено, но не то, удобно ли по нему играть. Первый же взгляд на экран показал то, что тесты увидеть не могли: один приказ на раунд — это мало, боец добегает и стоит без дела. Так появились ходы из нескольких шагов, и правила пришлось менять уже под картинку.

Привычка осталась. Новая механика сначала появляется в симуляции вместе с тестами и только потом на экране. Сейчас в проекте больше двух тысяч автотестов, и большая их часть проверяет бой.