← § BLOG

MMO как production: Grafana, PostgreSQL и WebSocket для игрового бота

Не в каждой игре можно подключить свой мониторинг. В Screeps бот пишется на JavaScript, а игровой мир отдаёт WebSocket и REST. Я собрал внешний коллектор и наблюдаю за экономикой энергии и глобальным маркетом как за биржей.

#screeps#observability#grafana#postgres#architecture

Screeps — единственная известная мне MMO, где AI-логика игрока пишется на обычном JavaScript и крутится 24/7 на серверах разработчика. Бот живёт в шарде, на тике 1Hz, потребляет реальное CPU из выделенного бюджета и торгует на общем глобальном маркете с другими игроками.

Из-за этого Screeps превращается в маленькую production-систему. И как любая production-система, она требует мониторинга. Не «открыл клиент, посмотрел». А дашборды, временные ряды, аннотации событий, алерты.

Главное окно Screeps: комната E49S39 на shard3, видны источники, экстеншны, spawn, крипы с метками spwn/idle/renew/pick
Так выглядит «реальность» бота — комната E49S39, моя главная (RCL 8). Жёлтые точки — extensions для энергии, в центре spawn, крипы под метками pick/idle/renew/spwn выполняют задачи из приоритетной очереди. Игровой клиент показывает текущий тик, но никакой истории здесь нет: что было час назад с этой комнатой — клиент не помнит. Поэтому всё ниже.

В большинстве игр это невозможно: внутреннее состояние закрыто, API нет, в лучшем случае есть лог боя. В Screeps есть WebSocket на console.log и CPU per-tick, REST API на состояние комнат и сегменты Memory, глобальный маркет с историей сделок. Я подключил всё это к собственной Grafana и теперь наблюдаю за ботом так же, как за прод-сервисом.

Зачем игре production-grade observability

Screeps-бот — это распределённый агент в неуправляемой среде:

  • CPU-бюджет — 20 CPU/тик при GCL 6. Превысил → bucket падает → крипы стоят. Нужно знать avg/p99/тренды по ролям и методам.
  • Экономика энергии — внутренний ресурс. Источник регенерирует 10 e/tick, апгрейдер потребляет 22 e/tick. Если расход стабильно превышает доход — storage сольётся за часы.
  • Глобальный маркет — реальная биржа: bid/ask, депрессированные ордера, объём, фильтр по volume. Покупка 10K единиц минерала — реальные ~2M кредитов. Без мониторинга цен я тратил кредиты в пустоту.
  • Бой и атаки на колонии — события, которые надо видеть как точки на графиках, а не находить в логах за 24 часа.

Игровой клиент Screeps показывает «здесь и сейчас». Что было два часа назад на бирже минерала Z — недоступно. Историю надо собирать самому.

Архитектура: WebSocket + REST → PostgreSQL → Grafana

                                       ┌──────────────────────────────┐
[Screeps WS]  ──console+cpu──┐         │                              │
                              ├──→ [screeps-logger] ──→ [PostgreSQL]  │
[Screeps REST] ──segments────┘                              │         │
                                                            ▼         │
                                                       [Grafana]      │
                                                            ▲         │
                                                            │         │
                                                       [Claude Code]  │
                                                       MCP grafana    │
                                                            │         │
                                                            └─────────┘

Три источника данных:

  1. WebSocket wss://screeps.com/socket/websocket — подписка на user:{id}/console и user:{id}/cpu. Реальный поток: каждый console.log бота, runtime errors, CPU и memory каждый тик.
  2. REST GET /api/user/memory-segment — раз в минуту опрашиваю 10 сегментов памяти. Бот пишет туда структурированные снапшоты: economy (роли, storage, кредиты), market (deals, fills), labs (реакции), spawns/deaths/attacks.
  3. REST GET /api/user/code — реже, при необходимости знать версию задеплоенного кода.

Внешний коллектор — FastAPI приложение в Docker. Складывает всё в PostgreSQL, который читает Grafana. Никакой Kafka, никакой Loki — обычная реляционка, потому что данные структурированные и объёма мало (4-6 GB за 30 дней при retention 30 дней).

Почему не лог-файл

Соблазн «писать в файл и парсить» большой, но не работает по двум причинам:

  • Бот не пишет на диск. В Screeps нет файловой системы — есть только Memory (около 2 МБ на пользователя) и сегменты (10 штук × 100 KB). Всё, что хочешь сохранить, лежит в этом ограниченном бюджете и стирается при сбросе.
  • WS-канал хрупкий. Соединение живёт ~5 тиков (15 сек), потом дропается. console.log() больше 5 KB убивает WS. Поэтому крупные снапшоты идут не через console.log (WebSocket), а через сегменты (REST poll). А короткие события — наоборот, WebSocket, потому что критичен low latency.

PostgreSQL даёт SQL и jsonb — это и есть формат «бот написал что мог, мы потом разберёмся».

Известные ловушки

WebSocket на Screeps живёт по своим правилам. Стандартного ping/pong сервер не реализует — нужно дёргать recv() с таймаутом 15 сек и реконнектить. Большие console messages дропают соединение — поэтому полный Room.getEventLog() я в console не вывожу, только в сегменты.

Ещё нюанс: на хосте включён DPI-троттлинг к npm registry, и Docker-сборка падала с exit 146 на RUN npm ci. Не CPU и не OOM — сетевая дискриминация. Прокси через privoxy 8118 (в локальной инфре) лечит. Это та история, которая попала в CLAUDE.md сессии — чтобы следующая не открывала её заново.

Экономика энергии: дашборд как PnL-отчёт

Энергия в Screeps работает как нефть в реальной экономике:

  • Регенерируется в источниках с фиксированной скоростью (cap 10 e/tick на источник).
  • Тратится на спавн крипов, апгрейд контроллера, башни, ремонт стен, реакции в лабах.
  • Транспортируется через линки (с 3% налогом) и хаулерами.
  • Может конвертироваться в battery через factory и продаваться.

Если ты не видишь поток энергии в разрезе где+на что, то любая просадка экономики = детектив на пустом месте. «Storage упал, не понимаю почему» — это симптом отсутствия PnL.

В моём дашборде на каждой комнате есть панель «Энергобаланс» — две стек-колонки рядом за каждый час: слева доход, справа расход.

Доход (4 категории):

  • sources — реальный харвест из miner'ов, инкрементируется в коде в обёртке над creep.harvest()
  • sk_deliver — доставки от skHauler из Source Keeper комнат (бонусные источники)
  • market_buy — закупки энергии на маркете (когда уж совсем плохо)
  • terminal_recv — переводы из других моих комнат

Расход (11 категорий):

  • spawn — стоимость спавнящихся крипов (сумма body cost'ов)
  • tower — атаки и ремонт башен (10e за действие)
  • upgradeupgradeController (1e × WORK parts/tick)
  • build, repair, fortify — стройка, ремонт, фортификация стен (точные счётчики из task.build.js и др.)
  • renewal — продление крипов (по точной формуле ceil(cost / 2.5 / parts))
  • link_tax — 3% налог на каждую передачу через линк
  • terminal_send + term_fee — энергия на исходящие отправки и комиссия
  • powerPowerSpawn.processPower (50e за op)

Эти счётчики живут в Memory.rooms[name]._eo = {b, r, f, u, ...}. Каждые 100 тиков снапшот пишется в сегмент 0 → попадает в PostgreSQL → отображается в Grafana как stacked bar за час.

CPU-нагрузка от инструментации — меньше 0.01 CPU за тик. Цена видимости — копейки.

Grafana row про энергию: storage, terminal, прогресс и скорость апгрейда контроллера, spawn+ext, крипы по ролям — все панели в разрезе шести моих комнат
Один экран — все шесть комнат за сутки. Видно как E45S38 (зелёная) тянет storage в 150K, у остальных колоний стабильно 50K. Скорость апгрейда падает с 65 до 20 e/tick — это момент, когда я отключил буст апгрейдеров и расход компаундов на других задачах перестал съедать запас. Без графика этот переход был бы виден только постфактум — через жалобу «контроллер ползёт медленно».

Что показал дашборд первой ночью

Я думал, что главная статья расхода в развитой комнате — апгрейдер. Дашборд показал, что нет: fortify (укрепление стен) на RCL 7 в 4 раза дороже апгрейда. Башни едят больше, чем казалось, потому что я не учитывал ремонт стен. А link_tax стабильно отъедал 3% всей энергии.

Это не было видно из кода. Это видно только в стек-баре над временем.

Глобальный маркет: торговый терминал

В Screeps есть полноценная биржа. Каждый игрок может выставить ордер BUY или SELL на любой ресурс с ценой и объёмом. Сделки совершаются между игроками, маркет считает комиссию на основе расстояния между терминалами. Есть история сделок, есть глубина стакана.

И вот тут начинается интересное: цены не статичные. Они зависят от того, кто выкатывает большие объёмы, кто демпингует, какие T3-компаунды нужны для буста сейчас. На графике fair-цены минерала H за неделю — реальный паттерн биржи: ралли, коррекции, иногда пампы от одного игрока с большим стоком.

Anti-speculation: fair price вместо median

Первая наивная реализация: бот видит лучший SELL ордер, цена 50 cr/u, покупает. Через час оказывается, что это был ордер на 10 единиц от спекулянта, а реальная цена 200. Я переплатил.

Решение пришло из реальных бирж: cumulative volume. Цена считается не по «лучшему ордеру», а так:

fairSell = цена при которой накопленный объём ордеров ≥ FAIR_PRICE_VOLUME (500 единиц)

Идём от лучшего ордера вниз, суммируем amount, и берём цену того ордера, на котором сумма пересекает 500. Одиночные мелкие ордера на 10-20 единиц не влияют на fair price — они отсеиваются.

В Grafana есть три панели:

  • Fair SELL базовых минералов (H/O/Z/K/U/L/X) — тренды цен на сырьё
  • Fair BUY базовых минералов — на сколько готов рынок выкупить
  • Fair SELL T2/T3 компаундов (LH2O, ZH2O, XLH2O...) — для оценки прибыльности цепочки реакций

И две — про реальные потоки кредитов:

  • Закупка минералов на рынке/час (units) — stacked bars, какие ресурсы и сколько закупаются
  • Траты на закупку/час (cr) — рублёвый эквивалент

Это уже не игра. Это маленькая algo-trading система с реальной P&L.

Ledger: учёт прибыли по комнатам

Ещё ближе к бирже — внутренний ledger. Каждая покупка/продажа пишется в Memory.market.ledger[room][resource]:

{
  spent: 1_240_000,   // потрачено кредитов на закупки этого ресурса в эту комнату
  bought: 8400,       // куплено единиц
  revenue: 3_650_000, // выручено с продаж этого ресурса из этой комнаты
  sold: 14_200        // продано единиц
}

Prod3.ledger() в консоли — отчёт «доход/расход за период по комнатам и ресурсам». В Grafana есть панель «Доходы от продаж» (credits/час), куда логируется каждое событие market.order_fill (partial/full fill моих ордеров).

Когда я в чате говорю «бот поднял 1.4M cr/час чисто на T2 LHO2» — это не оценка, это число из дашборда.

Не только энергия: задачи, башни, угрозы

Кроме экономики энергии и маркета, на дашборд выведены все «события физического мира» бота. Каждый крип получает задачу из приоритетной очереди (fill_extensions, unload_link, surplus_to_terminal, repair, ...) — типы и частоты пишутся в сегмент taskLog. Каждое срабатывание башни (атака/heal/ремонт) — в attacks. Это даёт вторую плоскость наблюдения: не «сколько энергии», а что бот вообще делает.

Grafana панели: stacked bars типов задач за час, top-задач за 24 часа, срабатывания башен по комнатам, характер угрозы
Стек задач за 24 часа: `to_storage` 6.94K, `unload_link` 4.72K, `fill_spawn` 4.71K — это пульс хаулерной работы. Срабатывания башен — это уже security-телеметрия: видно ровный фон одиночных enemy-скаутов и пиковый момент 02:00 с двумя башнями в одной комнате (вторжение в E49S38). Bottom panel «характер угрозы» отделяет настоящие атаки от прохождения мимо.

Когда бот ведёт себя странно — первое, что я делаю, это смотрю на эти панели. Пик repair без боя? Кто-то кидается в стену. Резкое падение unload_link? Линки переполнены, узкое место в хаулерах. fill_spawn в 0 при энергии в storage? Хаулеры зависли в другой задаче.

Графики заменяют интуицию. И именно это превращает игровую сессию в инженерную работу.

Claude Code как трейдер на этом дашборде

Финальный штрих архитектуры — MCP-интеграция Grafana с Claude Code на моём ноуте. Через mcp__grafana__* ассистент видит все 44 панели, делает SQL-запросы к PostgreSQL, ставит аннотации событий, генерит deep-link'и.

Когда я в чате спрашиваю «почему credits не растут?», флоу следующий:

  1. mcp__grafana__query_prometheus или прямой SQL к screeps_logs.segment_records — берёт текущий ledger
  2. Сверяется с правилом из docs/decision-frameworks/market-decisions.md
  3. Отвечает с числами, ссылками на панели и объяснением

Дашборд — это контекст. Без него ассистент гадал бы. С ним — видит то же, что вижу я.

Что в этой архитектуре особенного

Главное — наблюдаемость как первый класс. Не «потом прикрутим логи», а с первого дня:

  • Структурированные снапшоты в Memory сегменты (json, не строки)
  • Чёткое разделение каналов: WS — short-latency events, REST poll — periodic snapshots
  • PostgreSQL как универсальная storage для time-series + jsonb-blob'ов событий
  • Grafana как визуализация, не как single source of truth (источник — Postgres + код бота)
  • 30-дневный retention — вышибает «вчерашний баг неуловим» как класс проблем
  • MCP-интеграция в LLM — ассистент работает с теми же данными, что и я

Это паттерн, который я переношу из B2B-телекома и SaaS в любой долгоживущий процесс. Бот для онлайн-игры в этом смысле ничем не отличается от мониторинга OLT или NMS-сервиса.

Не в каждой игре так можно

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

Screeps проектировался под программистов. WebSocket, REST, JS-runtime для AI, JSON-сегменты Memory — это часть продукта, а не утечка. Поэтому за ботом можно следить теми же инструментами, что и за прод-сервисом, и оптимизировать ту же экономику что в реальной торговле.

Если когда-нибудь будете делать игру для разработчиков — посмотрите, как это сделано в Screeps. Не GraphQL, не GRPC, не proprietary protocol. Старые добрые WS + REST + JSON. И полная свобода для того, кто хочет наблюдать.


В следующей статье — про CPU-кризис: как я упёрся в потолок 20 CPU/тик при 43 крипах в 6 комнатах, какую декомпозицию сделал (intents 8.25 hard ceiling vs 4.8 untracked = 90% потенциала оптимизации), и как module-level cache в эфемерном runtime'е дал bucket 2-22 → 13-52.

Поделиться
TelegramX (Twitter)
Discussion

Комментарии работают через Giscus + GitHub. По клику ваши данные передаются в GitHub Inc. (США). Не будете кликать — ничего не отправляется.

Открыть обсуждение на GitHub
MMO как production: Grafana, PostgreSQL и WebSocket для игрового бота · Григорий Масич