Морвейн

Помните игры, где герой с армией бродит по карте, вербует отряды в городах, осаждает замки и собирает по кусочкам карту следующей земли? Мы сделали такую игру заново и запустили её прямо внутри Telegram. Её не нужно ставить и не нужно регистрироваться: нажал кнопку в боте — и через пару секунд ты уже на карте.

Игра называется «Морвейн». Под капотом — один HTML‑файл без фреймворков, отрисовка на canvas, процедурные миры, тактический бой на 80 клетках и сервер на Cloudflare, который пока не стоил нам ни копейки. А всю графику — от травы до драконов — мы сгенерировали нейросетью и научили код собирать её в цельную карту.

В этой статье расскажу, что умеет игра, как она устроена и какие задачи пришлось решать по дороге. Будет про генерацию континентов, сохранения в облаке Telegram с лимитом 4 КБ на значение, честный рейтинг без античита на сервере, вырезание розового фона «по оттенку» и про то, почему преследующий вас слон должен быть опасен.

Что это за игра

Это пошаговая стратегия с RPG‑элементами: нужно покорить пять земель — от зелёного Морвейна через пустыни Сарнага, заснеженный Хрустальный Бор и Пепелище до мрачного Тёмного Удела. Вдохновение — классика 90-х: кто играл в King's Bounty, узнает дух сразу, но механики мы переделали под короткие сессии на телефоне.

Карта мира. Каждая земля — 80×60 клеток, сгенерированных заново в каждой игре: леса, горы, болота, пустыни, дороги и мосты. На ней пять городов, семь замков лордов и престол Хозяина Земли, пещеры, лагеря наёмников, пирамиды, сундуки и свитки. Всё скрыто туманом войны. Путь загораживают леса и скалы — их расчищают нанятые лесорубы, камнетёсы и плотники. А по карте бродят отряды монстров, которые замечают героя и бросаются в погоню.

Бой. Поле 10×8 клеток с деревьями, камнями и стенами при осаде. У каждого отряда своя скорость и очередь хода. Стрелки бьют издалека, но теряют точность за укрытием и не могут стрелять, когда враг вплотную. Копейщики особенно опасны для всадников и крупных зверей. Магия растёт вместе с героем: огненный шар, исцеление, воскрешение, ускорение, теневые копии отряда, смерч. Из боя можно отступить, но чем быстрее враг, тем больше потерь при бегстве.

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

Прогресс. Захваченные замки открывают части карты пути в следующую землю, а последнюю часть хранит Хозяин Земли. Каждая следующая земля богаче, но и сильнее: на Тёмном Уделе врагов в 3,4 раза больше, чем на Морвейне. В городах — лавки заклинаний и снаряжения, гильдии работников и поручения.

Режимы. Обычный — с сохранениями каждое утро. Жёсткий — игра сохраняется после каждого действия, и поражение необратимо. И «Испытание недели»: раз в неделю все игроки получают одинаковую карту и героя и соревнуются, кто быстрее покорит первую землю. Достижения, звания, карточки «Поделиться» и приглашения друзей с бонусом обоим — тоже есть.

Стек: один файл и бесплатный сервер

Главное архитектурное решение — вся игра в одном index.html: HTML, CSS, JavaScript и все картинки в WebP (base64) внутри, около 5,5 МБ. Ни фреймворков, ни сборщика, ни npm. Карта, бой, генерация мира и ИИ противников работают на телефоне игрока, без сервера. Один файл — это ещё и атомарный релиз: не бывает ситуации, когда новый код загрузился, а старые картинки взялись из кеша.

Почему так:

  • Хостинг — GitHub Pages. Релиз — это git push. Файл кешируется, повторный запуск мгновенный.

  • Telegram Mini App. Игра открывается из бота в один тап. Telegram даёт имя игрока, облачное хранилище для сохранений, кнопку «Назад», «Поделиться» и платежи в звёздах. Вне Telegram тот же файл работает в обычном браузере — просто с локальными сохранениями.

  • Отрисовка на canvas. Карта собирается слоями: земля и плавные переходы, обрывы гор, деревья и объекты с сортировкой по глубине, туман войны, погода. Готовый кадр мира хранится во внеэкранном холсте: пока герой идёт, каждый кадр — это сдвиг готовой картинки и фигурка героя, а блики на воде дорисовываются поверх примерно 7 раз в секунду. Полная перерисовка — только когда мир изменился. Плотность пикселей ограничена ×2, а частицы можно выключить для слабых телефонов.

  • Сервер — Cloudflare Workers + D1 (SQLite). Он нужен только для общего: статистики, таблиц рекордов, «посылок» с наградами и ответов бота (вебхук с командами /play, /top, /invite и другими). Каждый запрос подписан Telegram (initData, HMAC‑SHA256 от токена бота), поддельные сервер отклоняет, а идентификатор игрока вычисляет сам. Токен бота живёт только в секретах Cloudflare. Бесплатного тарифа — 100 000 запросов в сутки — хватает примерно на 4000 активных игроков в день.

  • Аналитика пачками. Именно она делает лимит достаточным: события копятся в очереди и уходят одним запросом раз в 2 минуты или при накоплении 40 штук. Когда игру сворачивают, остаток улетает через sendBeacon, который браузер досылает даже после закрытия страницы.

  • «Посылки» вместо прямых начислений. Всё, что игрок получает от сервера (бонус за приглашение друга, подарок, в будущем покупка), лежит в одной таблице, и игра забирает это сама. Выдачу решает сервер, забрать дважды или чужое нельзя, а новые виды наград не требуют обновлять код сервера.

Самое интересное под капотом

Мир, который не запирает игрока

Рельеф строится из фрактального шума с порогами: выше одного — горы, ниже другого — вода, между ними леса, болота и пустыни. Характер земли задаётся пятью числами в описании земли, без отдельного кода: у Сарнага порог пустыни высокий, и половина карты уходит в песок, а Хрустальный Бор с низким порогом леса зарастает тайгой.

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

Земли генерируются лениво, при первом визите. Для «Испытания недели» генерация идёт от зерна недели, поэтому у всех игроков карта совпадает до клетки.

Погоня, которая думает

Бродячие отряды не ищут путь каждый по отдельности. Один раз за такт строится поле расстояний от героя — поиск в ширину (BFS) по восьми направлениям, — и все преследователи просто «скатываются» по нему к меньшим значениям, обходя леса и скалы. Десять монстров стоят столько же, сколько один: 4800 клеток обходятся один раз. Срезать угол скалы по диагонали нельзя, а скорость отряда равна скорости его самого медленного существа.

Решение гнаться зависит от соотношения сил, и считается оно только по активным отрядам, без резерва: равные и сильные гонятся, слабые бьют только вплотную, а совсем слабые просятся к вам в армию за плату. Враги ходят, пока идёт герой, и могут перехватить его на полпути; ночью они подтягиваются ещё на несколько шагов.

Отдельная история — бегство. В ранних версиях догнавший слон показывал окно «Атаковать / Уйти», игрок нажимал «Уйти» — и слон просто останавливался. Вся погоня теряла смысл. Теперь от догнавшего врага можно уйти только с потерями: каждый отряд теряет 20% бойцов (40% в жёстком режиме), умноженные на отношение скорости самого быстрого преследователя к скорости отряда (от 0,5 до 2). Пехота, уходящая от волков, платит вдвое больше кавалерии. После бегства преследователь сутки не гонится, чтобы не зажать игрока в петлю. Добровольно отойти от логова, к которому сам подошёл, по‑прежнему можно бесплатно.

Бой: укрытия, бунт и простой, но честный ИИ

Укрытие. Когда стрелок стреляет, игра берёт 24 точки на отрезке от стрелка до цели и проверяет, не попадает ли какая‑нибудь из них в клетку с деревом, камнем или стеной. Если попадает, урон падает на 30%. Семплинг проще алгоритма Брезенхэма и на поле 10×8 с запасом точен. В журнале боя игрок видит приписку «укрытие -30%» и понимает, почему выстрел слабый.

Бунт. Лидерство — мягкий лимит: перебрать можно, но тогда перед боем игра честно показывает шанс бунта и то, кто именно уйдёт. Шанс равен двойной доле превышения (от 10 до 95%), рыцарь срезает его на 15 пунктов, а переходят к врагу в первую очередь самые «дорогие» по лидерству существа. Взять с собой лишнего дракона — риск драться с ним же.

ИИ противника намеренно прост и предсказуем: вплотную — бьёт, стрелок — стреляет в ближайшего, иначе подходит и бьёт любую достижимую цель, а если не дотягивается — идёт по полю расстояний в обход препятствий. Израненный отряд (меньше 30% здоровья) может сдаться панике и убежать за край поля — игрок видит, что он сбежал, а не погиб. Драконы не бегут никогда. Хитрый ИИ на маленьком поле быстро начинает раздражать, а простой позволяет игроку планировать.

Честный рейтинг без античита на сервере

В «Испытании недели» все играют на одной карте, и сравнивать результаты честно можно, только если удача тоже одинакова. Поэтому вся случайность, влияющая на игру, идёт через одну функцию R(). В обычной игре это Math.random, а в испытании — генератор mulberry32, засеянный FNV‑хешем строки «зерно недели | ключ потока». Потоков много: свой на каждый день и свой на каждый бой (клетка боя + номер попытки). Это важно: если бы поток был один, лишний шаг по карте сдвигал бы все броски в будущих боях, и два игрока с одинаковой тактикой в бою получали бы разные исходы. Номер недели считается по ISO в UTC одной и той же функцией на клиенте и на сервере.

Та же идея закрыла классическую лазейку жёсткого режима «закрыть игру, пока бой не проигран». В жёстком режиме каждый бой получает случайное зерно, которое сохраняется в момент начала боя. Перезапуск вернёт вас в тот же бой с той же удачей — и человека с разрядившимся телефоном это не накажет, и читеру не поможет. Сервер при этом игру не симулирует: он проверяет подпись Telegram и правдоподобие результата. Для инди‑игры без денег на кону это разумный компромисс.

Сохранения в облаке Telegram по 4 КБ

Telegram даёт каждому Mini App облачное хранилище, привязанное к аккаунту: сохранение переживает смену телефона. Но одно значение там — не больше 4096 символов, а в игре пять карт по 4800 клеток с туманом войны. Решение в три шага:

  1. Сжатие. Рельеф и туман записываются RLE‑строкой: карта в 4800 клеток превращается в 1–2 тысячи символов. Всё остальное — обычный JSON.

  2. Нарезка. Строка режется на куски по 4000 символов, которые пишутся параллельно.

  3. Атомарность. Куски пишутся в чередующиеся слоты A и B, а мета‑запись «актуален слот B, в нём N кусков» — строго последней. Если игру закрыли на полпути, мета ещё указывает на старый целый слот, и сохранение не портится.

Параллельно та же строка ложится в localStorage как быстрый кеш, а при загрузке берётся более свежая из двух копий. Снимок состояния делается один раз, в момент сохранения, и уже готовой строкой уходит в оба места. Иначе медленная запись в облако могла бы захватить более позднее состояние, чем локальная, и две копии разошлись бы.

Графика из нейросети, собранная кодом

Сначала вся карта рисовалась процедурно — кружками и многоугольниками. Потом мы начали переводить её на сгенерированные картинки: текстуры земли, деревья, города, замки, шапки окон, фигуры всех 32 существ. Одна земля — это около 30 картинок. Чтобы они сложились в одну картину, у всех промтов общий блок стиля с ракурсом «сверху под 60°, свет слева сверху» и строка палитры земли. Картинки проходят через Python‑конвейер (Pillow + NumPy) и вшиваются в файл игры как WebP. Дальше начинается инженерия.

Прозрачность. Генераторы не умеют настоящий альфа‑канал и рисуют «шахматку» прямо на картинке. Поэтому все объекты генерируются на ярко‑розовом фоне #FF00FF, а вырезаем мы его не по точному цвету, а по оттенку — углу в плоскости (R‑G, B‑G). Это даёт три вещи сразу:

  • фон любой яркости уходит целиком, включая градиенты, которые генератор любит добавлять «для красоты»;

  • тёмно‑фиолетовые тени под объектами (фон, умноженный на тень) превращаются в полупрозрачные чёрные, и дерево ложится со своей тенью на любую траву;

  • фиолетовая мантия чародейки, у которой синего заметно больше, чем красного, остаётся нетронутой.

На кромке розовая примесь «размешивается»: из цвета пикселя вычитается доля фона, и она же становится прозрачностью. Поэтому дымные хвосты призраков и сияние магических кристаллов остаются полупрозрачными и не получают розового ореола.

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

Переходы между типами земли. Чтобы трава, песок и болото не стыковались квадратами, для каждого типа строится маска прозрачности: 8 точек на клетку, тройное скользящее среднее (дешёвое приближение гауссова размытия) и фрактальный шум только в полосе перехода. Кромка получается неровной и мягкой, а сетка клеток перестаёт читаться. Горы рисуются плато с угловатой кромкой, отвесной стеной из текстуры утёса и осыпью под ней. Кривая контраста кромки берётся из заранее построенной таблицы на 256 значений, внутри циклов нет Math.min/Math.max, а шум кешируется на землю. Это ускорило пересборку масок примерно вдвое — важно, когда лесоруб вырубает рощу, и карта меняется.

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

Экономика, в которой видно всё

Раз в неделю армия получает жалованье, резерв — за полцены. Не хватило денег — каждый отряд теряет ровно ту долю бойцов, которая не оплачена. Чтобы это не было неприятным сюрпризом, рядом с золотом в панели виден итог недели (+ или ‑), за два дня до выплаты приходит предупреждение, а окно «Казна» раскладывает доход и содержание по статьям, вплоть до каждого отряда. Уход неоплаченных бойцов стоит за флагом в коде: если аналитика покажет, что игроки страдают на первой земле, механику можно выключить одной строкой.

Тесты без тестового фреймворка

Раз игра — один файл, её удобно тестировать целиком: Playwright открывает его в Chromium, дёргает функции игры напрямую и снимает скриншоты. Сценарии проверяют бои, найм, лидерство, перехват на карте, бегство и продолжение боя после перезапуска, а скриншоты каждой земли в телефонном разрешении ловят обрезанные подписи и съехавшие окна. Производительность меряем с замедлением процессора в 4 раза через Chrome DevTools Protocol — именно так выяснилось, что полноэкранные градиенты «теней облаков» слишком дороги для слабых телефонов, и от них отказались везде, кроме небольшого тумана Тёмного Удела.

На каком этапе игра

Сейчас «Морвейн» — это версия 0.36, открытая альфа. Игра проходится от начала до конца: пять земель, три героя, 32 существа, три режима, достижения, еженедельный рейтинг и приглашения друзей. Игровая часть в целом собрана, и сейчас идёт настройка баланса по аналитике и отзывам игроков.

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

Как это делается

Игра разрабатывается короткими итерациями. Каждая правка сначала попадает в прототип: отдельную копию игры, которую можно открыть по ссылке и потрогать с телефона. Только после проверки она переезжает в основную ветку. Релиз — это git push: GitHub Pages обновляет страницу, и игроки получают новую версию при следующем открытии бота. Старые сохранения при этом мигрируют на лету, чтобы никто не потерял прогресс.

Для отладки есть скрытый админ‑режим: с ним можно прыгать между землями, телепортироваться к любому типу объектов и запускать тестовые бои на любом грунте, не проходя полкарты. Он доступен только аккаунту разработчика, а пока он включён, достижения и аналитика отключены, чтобы тесты не портили статистику.

Что впереди

  • Остальные земли в новой графике. На очереди Хрустальный Бор, Пепелище и Тёмный Удел — по 30 картинок на каждую, со своими стенами, замками и деревьями.

  • Города, которые видно по карте. Уже есть подписи и значки специализации, дальше — разный вид самих городов, чтобы сразу было видно, где продаются свитки, а где броня.

  • Монетизация через Telegram Stars — только косметика и удобства, без покупки силы. Инфраструктура «посылок» для этого уже готова.

  • Английская версия и отдельное приложение для Android. Тот же файл в WebView, с локальными сохранениями вместо облака Telegram — код к этому уже готов.

Попробуйте

Игра бесплатная и запускается прямо в Telegram, без установки: @morveyn_game_bot. Дневник разработки, новости и обсуждение баланса — в канале @morveynRPG.

Буду рад вопросам в комментариях: про генерацию карт, про бэкенд за $0, про то, как заставить нейросеть рисовать в одном стиле тридцать разных объектов. И, конечно, про то, где игра сломалась у вас.

Комментарии (5)


  1. solus_factor
    28.10.2026 11:31

    Статья про игру и ни одной картинки?!


    1. Velzevul1918 Автор
      28.10.2026 11:31

      Согласен) мой косяк. добавил.
      приглашаю оценить в живую в телеге)


  1. Dismal
    28.10.2026 11:31

    Здраствуйте. Я, Кирилл.  Я джва года хочу такую игру!


    1. Velzevul1918 Автор
      28.10.2026 11:31

      Спасибо) надеюсь итоговый вариант не разочарует.
      Пробуй и рассказывай своё впечатление


    1. Velzevul1918 Автор
      28.10.2026 11:31

      и охрана дворца и разбойники и лесные эльфы)