Как я писал разбор демок CS2 на вахте: Go, строгий ИИ и грабли в Laravel
Обо мне и предыстория проекта
Приветствую! Сразу расставлю точки над i: я не тимлид из крупной продуктовой корпорации и не гуру SaaS. Моя основная деятельность связана с авиацией, график подразумевает постоянные вахты, а за плечами висят ипотека и семья. Бросить стабильный источник заработка ради стартапа я не могу, поэтому развиваю проект в свободное время — по вечерам после смен и в период межвахтового отдыха.
За спиной — шесть лет освоения языков программирования, паттернов проектирования и архитектуры, а также фриланс и участие в коммерческих командах. Фундаментальное понимание того, как всё должно функционировать под капотом, у меня есть. Полноценного фултайма в коммерческой разработке до этого не было, так что данный проект — моя личная попытка довести собственный продукт от концепта до релиза в продакшн.
Counter-Strike увлекает меня ещё со времен версии 1.6: 10-й уровень на FACEIT, участие в локальных турнирах и часы за детальным разбором чужих демок ради повышения скилла. Отсюда и родилась боль, подтолкнувшая к созданию платформы. Качественная аналитика для игроков представлена преимущественно зарубежными сервисами: их проблематично оплачивать из РФ, они полностью англоязычные и далеки от реалий нашего гейминга. Видеогайды непозволительно длинные, а специализированные сайты с раскидками требуют совершать по три клика ради демонстрации одного смока.
Именно так родился ELOUP.GG — тренировочный хаб для игроков в CS2, включающий интерактивные карты с гранатами, интерактивную тактическую доску для командной работы в реальном времени, подробные гайды и разбор матчей с помощью искусственного интеллекта в тандеме с профессиональными аналитиками.
Возможности платформы лучше изучить самостоятельно на самом сайте. Здесь же я поделюсь тем, за что так ценится SE7EN: технической кухней, подводными камнями и архитектурными решениями.
Технологический стек
Когда ведешь разработку в одиночку и урывками, приоритет один — на реализацию каждой фичи должен уходить вечер, а не затягиваться на недели.
-
Laravel 13+Inertia+Vue 3. Для соло-разработчика это палочка-выручалочка: на выходе получается полноценное SPA, но без головной боли по проектированию отдельного API для фронтенда. Логика роутинга, авторизации и валидации инкапсулирована в Laravel, а страница просто принимает необходимые пропсы. -
Inertia SSR. Проект критически зависит от поискового трафика: гайды и раскидки обязаны индексироваться, поэтому разметка параллельно рендерится в Node. -
PostgreSQL— тип данныхjsonbпришелся как нельзя кстати для хранения результатов разбора. -
Redis+Horizon— надежный инструмент для управления очередями задач: рендеринг видео раскидок, обработка демок, запросы к ИИ. -
Laravel Reverb— легковесный WebSocket-сервер, обеспечивающий работу тактической доски. -
Go— специализированный парсер матчей, о котором пойдет речь далее.
Почему выбор пал на Go при создании парсера
Демо-файл в CS2 представляет собой увесистый бинарник размером в сотни мегабайт. Его парсинг требует значительных объемов оперативной памяти и процессорного времени. Готовых библиотек для работы с форматом Source 2 на PHP не существует, а написание собственного аналога — утопия. Зато в экосистеме Go есть проверенная библиотека demoinfocs-golang с полноценной поддержкой CS2.
В результате парсер оформлен как независимый микросервис со строгими регламентами безопасности:
-
принимает запросы исключительно на
127.0.0.1:8081и не светится наружу черезnginx; -
не сохраняет состояние между сессиями. Вся очередь, приоритеты задач и статусы хранятся в
Laravel, а микросервис лишь принимает путь к файлу и возвращает готовыйJSON; -
при высокой нагрузке отвечает кодом
503с заголовкомRetry-After, после чегоLaravelвозвращает задачу обратно в очередь. Благодаря этому приоритеты платных запросов регулируетHorizon, а не сам парсер; -
все запросы защищены подписью
HMACс использованием общего секретного ключа и таймстемпа.
Тело самого запроса при этом не хэшируется, и это осознанный компромисс. Просчитывать хэш для потока данных весом 350 МБ до начала разбора означало бы читать файл дважды. Это допустимо ровно до тех пор, пока сервис изолирован на loopback-интерфейсе; при масштабировании на отдельную машину проверку хэша тела придется внедрить.
Для безопасной обработки пользовательских загрузок предусмотрено несколько защитных барьеров:
-
кастомная проверка файла по сигнатуре (
PBDEMS2для CS2) еще до передачи в парсер; -
определение метода сжатия по сигнатуре, а не по расширению файла: поддерживаются
zstd(в этом формате отдает демки FACEIT),gzipили распакованные файлы; -
жесткое ограничение на максимальный размер распакованного архива во избежание zip-бомб, способных забить дисковое пространство.
Реальные показатели на стандартном матче de_mirage (350 МБ, 23 раунда): парсинг занимает всего 4,3 секунды. Координаты игроков фиксируются с частотой 8 Гц, что дает около 534 кадров на 10 участников за раунд — это примерно 110 КБ в JSON или 17 КБ в сжатом gzip виде. Полученного объема данных с избытком хватает как для глубокой аналитики, так и для отрисовки плавного 2D-реплея в браузере.
Обработка происходит синхронно в рамках воркера очереди. Изначально я закладывался на асинхронную модель с вебхуками, но поскольку матч обрабатывается считанные секунды, лишняя сложность оказалась ни к чему. Страница анализа опрашивает сервер через Inertia посредством метода router.reload({ only: ['analysis'] }) до тех пор, пока статус не изменится на «готово».
Грабли № 1: Загрузка 350-мегабайтнеых файлов
Залить такой массив данных за один присест нереально: на продакшн-сервере в nginx жестко задан client_max_body_size на уровне 100 МБ, да и мобильный интернет на вахте постоянно обрывается. Поэтому передача разбита на чанки по 8 МиБ.
Создавать отдельную таблицу в базе под трекинг загрузки я не стал — текущим статусом выступает фактический размер временного файла на диске. Клиент отправляет очередной фрагмент вместе с указанием смещения (offset), а сервер сверяет его с реальным объемом. При несовпадении сервер возвращает код 409 с актуальным размером, и клиент продолжает отправку с нужной точки. Без этой валидации повторно отправленный фрагмент задвоил бы файл, а перепутанные местами чанки превратили бы демку в нечитаемый мусор, ошибка в котором всплыла бы только на этапе парсинга.
Далее сработала классическая ловушка типизации. Код проверки выглядел примерно так:
$data = $request->validate([
'offset' => ['required', 'integer', 'min:0'],
]);
if ($data['offset'] !== $current) { // Сравнение '0' !== 0 всегда возвращает true
return response()->json(['size' => $current], 409);
}
Валидатор успешно проверяет входные данные на соответствие типу integer, но не конвертирует их: из multipart-запроса числовые параметры всегда прилетают в виде строк. Строгое сравнение '0' !== 0 порождало вечный цикл с кодом 409, стопоря загрузку на нулевой отметке. Локальные юнит-тесты при этом передавали настоящий тип int и успешно проходили. Проблема решается добавлением одной строчки приведения типов — $offset = (int) $data['offset']; — и написанием интеграционного теста, отправляющего смещение в строковом формате, как это делает настоящий браузер.
Грабли № 2: Фантомный хост от FACEIT
Первоначальный план выглядел безупречно: пользователь кидает ссылку на матч FACEIT, а система сама подтягивает демку. В их Data API у каждого матча присутствует поле demo_url, ведущее на домен вроде demos-europe-central.backblaze.faceit-cdn.net.
Подвох заключался в том, что этот домен не резолвится ни из одной подсети. Я тестировал доступность с локальной машины и с боевого сервера, через системные резолверы и публичные DNS вроде 1.1.1.1, 8.8.8.8, 9.9.9.9. Сначала грешил на сетевые блокировки, затем на временный сбой. Выяснилось, что demo_url указывает на закрытый ресурс, и запрашиваемую ссылку необходимо предварительно запрашивать через специализированный Downloads API. Доступ к нему предоставляется строго по индивидуальным заявкам, а рассмотрение запроса занимает около месяца.
Поэтому сейчас модуль интеграции с FACEIT честно предупреждает пользователя о временных ограничениях и не пытается скачать заведомо недоступный файл. Основным сценарием по-прежнему остается прямая загрузка архива. Архитектура парсера позволяет интегрировать новый источник силами одной ветки условий, не затрагивая базовую логику, так что подключение матчей с FACEIT и режима Premier по sharecode пройдет бесшовно.
ИИ-аналитика: код определяет ЧТО произошло, модель — КАК это преподнести
Это наиболее захватывающий компонент системы. Первая итерация была наивной: скормить языковой модели сырую статистику матча с формулировкой «проанализируй как профессиональный тренер». Обратная связь от первых бета-тестеров указала на фатальные промахи в фактологии:
-
модель заявляла, что игрок «погиб первым в раунде», хотя фактически он уходил из жизни четвёртым;
-
утверждала, что «у оппонентов эко-раунд», когда на деле у противников был полный закуп;
-
упрекала игрока за то, что он «не донес бомбу», хотя раунд отыгрывался за сторону защиты, где бомба в принципе отсутствует.
Прямые запреты в системном промпте не помогали — модель продолжала галлюцинировать. Это привело к смене базовой парадигмы: всю фактологию рассчитывает детерминированный код, а задача ИИ сводится исключительно к текстовой интерпретации.
Текущая схема функционирования выглядит следующим образом:
-
Алгодетекторы в Go-парсере. Ошибки вычисляются точным кодом, а не нейросетью: качество контрстрейфа (частота стрельбы в движении и ее исходы), экономические просчеты (неоправданный форс-бай, отсутствие форс-сбережения оружия), повторяющиеся смерти на одних и тех же позициях, сухие соло-пики без предварительного использования гранат и размена, преждевременные ротации или реагирование на фейки. Каждое зафиксированное событие жестко привязывается к номеру раунда и тику.
-
Генерация выжимки матча. Сырые метрики и покадровые дампы невозможно скормить модели из-за ограничений по стоимости и контексту. Вместо этого формируется текстовая выжимка «матч сквозь призму одного игрока»: экономика закупок, стартовые дуэли, размены, гибели в контексте окружения, расход гранат. Упоминание никнеймов исключено полностью — ни самого подопечного, оперируемого обезличенно, ни остальных девяти участников. Для модели существуют только роли «ты», «тиммейт» и «оппонент», и этого контекста ей вполне достаточно.
-
Каскад точечных запросов. Программный код отбирает топ-3 ключевые ошибки, после чего модель параллельно генерирует текстовый блок под каждую из них. На финальном этапе отдельным запросом формируется общий вердикт, перечень сильных сторон и персональные рекомендации на основе уже утвержденных блоков.
-
Верификация каждого блока. Сгенерированные тезисы сверяются с исходной выжимкой раунда. При малейшем несоответствии запускается единственная попытка регенерации, после чего не прошедший проверку блок отбраковывается. Такой грубый текстовый фильтр внедрен сознательно: лучше упустить валидное замечание, чем выдать пользователю выдуманную дезинформацию.
-
Валидация позиций на карте. Типичная болезнь языковых моделей — путать топонимику карт: переносить
A-лонгс одной локации на другую или приплетатьбананиз Inferno на Dust II. Для каждой карты заведен строгий словарь легитимных позиций, и упоминание чужеродных зон безжалостно отсекается.
Приятным бонусом привязки «раунд + тик» стала интерактивность: каждый пункт разбора кликабелен и мгновенно перематывает 2D-реплей точно к моменту совершения ошибки. Игроку больше не нужно верить на слово — он видит всё своими глазами.
В качестве языковой модели используется DeepSeek через отечественного провайдера AITunnel. Качество генерируемого контента достигается не переходом на дорогие флагманские модели, а выверенным контекстом и каскадом компактных запросов. Месячные затраты на ИИ составляют символические пару сотен рублей. В реалиях соло-проекта финансово обременительным оказался не сам ИИ, а серверная инфраструктура для парсинга демок, поэтому лимиты на разборы продиктованы исключительно пропускной способностью парсера, а не токенными бюджетами.
Чтобы правка промптов не ломала уже отлаженный функционал, был внедрен эталонный регрессионный пайплайн: набор эталонных разборов и тестовых вопросов прогоняется через новую версию системных инструкций с автоматической верификацией и сравнением результатов с предыдущими итерациями.
Тактическая доска в режиме реального времени
Доска представляет собой интерактивную сессионную комнату, где участники команды могут совместно чертить тактики поверх внутриигровых карт: указывать направления атак, траектории гранат и оборонительные позиции. Ее устройство:
-
эталонное состояние доски персистентно хранится в базе данных и обновляется пользователями с правами редакторов;
-
интерактивные изменения транслируются не через сервер приложений, а посредством
whisper-событий в presence-каналеReverbпо адресуroom.{id}. Это клиентские эвенты, в которыхLaravelне участвует, что обеспечивает нулевую задержку при рисовании; -
авторство каждого действия валидируется самим демоном
Reverb(через директивуaccept_client_events_from = members), исключая возможность подделки чужих графических пометок; -
весь входящий контент от удаленных клиентов проходит санитайзинг на фронтенде, а права доступа (статусы редакторов и заблокированных пользователей) проверяются при подключении к каналу.
Реплеи матча на тактической доске и в модуле разбора рендерятся единым движком. Готовые сторонние графические библиотеки безнадежно лагали на мобильных устройствах, поэтому модуль визуализации карт я переписал с нуля, оставив лишь легковесный panzoom для жестов масштабирования и панорамирования. Отчетливо помню ночную вахту, когда под гул авиационных двигателей, засыпая над клавиатурой ноутбука, я переписывал эту логику. Зато теперь интерфейс на смартфонах работает идеально плавного.
Еще три архитектурные ловушки, отнявшие массу времени
SSR и белый экран смерти. Страница первично рендерится в среде Node, а браузер в дальнейшем производит гидратацию разметки. Использование стандартного тега для модальных окон приводило к полному удалению корневого контейнера div#app во время гидратации: пользователи наблюдали пустой белый экран, хотя в dev-режиме без SSR всё функционировало безупречно. Проблема решена заворачиванием телепорта в кастомную обертку, активирующую монтирование строго после завершения гидратации. Отсюда сформировалось правило: обращения к объекту window, вычисление ширины экрана и инициализация new Date() допускаются исключительно внутри хука onMounted, в противном случае неизбежны расхождения между серверным и клиентским рендерингом.
PostgreSQL и нюансы кириллицы. Реализуя поисковый функционал по раскидкам, я приводил текст к нижнему регистру с помощью SQL-функции LOWER(). При дефолтной локали базы данных C эта функция игнорирует символы кириллицы: русскоязычный поиск молча возвращал пустые результаты без единой ошибки в логах. На продакшене база развернута с локалью C.UTF-8, тогда как локальное окружение работало на C, из-за чего тесты стабильно падали «на ровном месте». Теперь конфигурация базы проверяется отдельным ассертом в тестах во избежание рецидивов.
Тихий деплой с последствиями. Версия PHP на моей локальной машине оказалась новее серверной. Менеджер зависимостей Composer честно разрешил пакеты под локальное окружение, подтянув библиотеку symfony/cache, несовместимую с установленным на сервере модулем расширения Redis. Процесс деплоя завершался ошибкой, однако веб-сервер продолжал обслуживать запросы на старой кодовой базе. Проблема решилась жесткой фиксацией версий в секции config.platform файла composer.json с учетом особенностей продакшн-окружения и введением негласного правила «никогда не удалять пины версий».
Текущие результаты и планы на будущее
Что уже реализовано и функционирует: интерактивные карты с раскидками, тактическая доска с поддержкой реплеев, база гайдов, загрузка демок с автоматическим расчетом метрик, ИИ-анализ с привязкой к таймлайну ошибок, а также система заявок для профессиональных аналитиков. Платформа официально запущена: монетизация строится на подписной модели и разовых отчетах — детальные условия представлены на самом сайте.
Находятся в разработке или ожидают реализации:
-
автоматизированный импорт демок с платформы
FACEIT— ожидаем одобрения доступа кDownloads API; -
загрузка матчей соревновательного режима
Premierпо sharecode — требуется развертываниеSteam-ботадля взаимодействия сGame Coordinator; -
часть алгоритмических детекторов проходит стадию полевых испытаний: я вручную валидирую их срабатывания на реальных матчах, и только после подтвержденной точности они интегрируются в продакшн-анализ.
Вместо заключения
Разрабатывать столь масштабный продукт в одиночку, совмещая его с основными рабочими сменами и бытовой рутиной — задача изнурительная. Но когда подсистема загрузки, парсер, ИИ-модуль и графический реплей впервые синхронизировались в единый конвейер, а разбор матча открылся на экране смартфона с возможностью перемотки к таймкоду ошибки, это дало мощнейший прилив мотивации, ради которого всё и затевалось.
Я не питаю иллюзий проснуться миллионером к завтрашнему утру. Но искренне верю, что созданный инструмент принесет пользу тысячам таких же преданных фанатов CS, как и я сам.
Буду рад любой конструктивной обратной связи: критике архитектурных решений, баг-репортам, обсуждению связки Laravel + Go и методам борьбы с галлюцинациями нейросетей. Если вы разделяете страсть к CS2 и пишете на Laravel, Vue или Go — добро пожаловать в единомышленники. Пишите свои мысли в комментариях или напрямую в личные сообщения.
Из школьной скамьи в миллионеры: как дети делают карьеру в видеоиграх
Фанаты разрабатывают масштабный мод для Red Dead Redemption 2 с нашествием зомби
Компания Inart представила распаковку коллекционной фигурки Женщины-кошки из фильма Тёмный рыцарь Возрождение легенды
Проблемы с обзорами Call of Duty Modern Warfare 4 на Xbox указали на уязвимость в системе Microsoft
В Escape from Tarkov началось событие с заражёнными, а в Arena — бои без огнестрельного оружия
У Sony произошла утечка данных 2,58 млн пользователей включая пароли
Издательство АСТ открыло предзаказ на коллекционное издание книги Фрэнка Герберта Капитул Дюны
Краудфандинговая кампания настольной игры Too Little Time собрала свыше 88 миллионов рублей менее чем за сутки