MMO как production: Grafana, PostgreSQL и WebSocket для игрового бота
Не в каждой игре можно подключить свой мониторинг. В Screeps бот пишется на JavaScript, а игровой мир отдаёт WebSocket и REST. Я собрал внешний коллектор и наблюдаю за экономикой энергии и глобальным маркетом как за биржей.
Screeps — единственная известная мне MMO, где AI-логика игрока пишется на обычном JavaScript и крутится 24/7 на серверах разработчика. Бот живёт в шарде, на тике 1Hz, потребляет реальное CPU из выделенного бюджета и торгует на общем глобальном маркете с другими игроками.
Из-за этого Screeps превращается в маленькую production-систему. И как любая production-система, она требует мониторинга. Не «открыл клиент, посмотрел». А дашборды, временные ряды, аннотации событий, алерты.

В большинстве игр это невозможно: внутреннее состояние закрыто, 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 │
│ │
└─────────┘
Три источника данных:
- WebSocket
wss://screeps.com/socket/websocket— подписка наuser:{id}/consoleиuser:{id}/cpu. Реальный поток: каждыйconsole.logбота, runtime errors, CPU и memory каждый тик. - REST
GET /api/user/memory-segment— раз в минуту опрашиваю 10 сегментов памяти. Бот пишет туда структурированные снапшоты: economy (роли, storage, кредиты), market (deals, fills), labs (реакции), spawns/deaths/attacks. - 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 за действие)upgrade—upgradeController(1e × WORK parts/tick)build,repair,fortify— стройка, ремонт, фортификация стен (точные счётчики изtask.build.jsи др.)renewal— продление крипов (по точной формулеceil(cost / 2.5 / parts))link_tax— 3% налог на каждую передачу через линкterminal_send+term_fee— энергия на исходящие отправки и комиссияpower—PowerSpawn.processPower(50e за op)
Эти счётчики живут в Memory.rooms[name]._eo = {b, r, f, u, ...}. Каждые 100 тиков снапшот пишется в сегмент 0 → попадает в PostgreSQL → отображается в Grafana как stacked bar за час.
CPU-нагрузка от инструментации — меньше 0.01 CPU за тик. Цена видимости — копейки.

Что показал дашборд первой ночью
Я думал, что главная статья расхода в развитой комнате — апгрейдер. Дашборд показал, что нет: 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. Это даёт вторую плоскость наблюдения: не «сколько энергии», а что бот вообще делает.

Когда бот ведёт себя странно — первое, что я делаю, это смотрю на эти панели. Пик repair без боя? Кто-то кидается в стену. Резкое падение unload_link? Линки переполнены, узкое место в хаулерах. fill_spawn в 0 при энергии в storage? Хаулеры зависли в другой задаче.
Графики заменяют интуицию. И именно это превращает игровую сессию в инженерную работу.
Claude Code как трейдер на этом дашборде
Финальный штрих архитектуры — MCP-интеграция Grafana с Claude Code на моём ноуте. Через mcp__grafana__* ассистент видит все 44 панели, делает SQL-запросы к PostgreSQL, ставит аннотации событий, генерит deep-link'и.
Когда я в чате спрашиваю «почему credits не растут?», флоу следующий:
mcp__grafana__query_prometheusили прямой SQL кscreeps_logs.segment_records— берёт текущий ledger- Сверяется с правилом из
docs/decision-frameworks/market-decisions.md - Отвечает с числами, ссылками на панели и объяснением
Дашборд — это контекст. Без него ассистент гадал бы. С ним — видит то же, что вижу я.
Что в этой архитектуре особенного
Главное — наблюдаемость как первый класс. Не «потом прикрутим логи», а с первого дня:
- Структурированные снапшоты в
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.
Комментарии работают через Giscus + GitHub. По клику ваши данные передаются в GitHub Inc. (США). Не будете кликать — ничего не отправляется.