Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

ИИ‑мастер для настольной RPG: один запрос на ход, кубик до ответа и баг, который удваивал здоровье

Дата публикации: 18-10-2026 10:32:11

Я делаю браузерную текстовую ролевую игру, где ведущим работает языковая модель. Игрок пишет любое действие своими словами, мастер отвечает рассказом, а игра ведёт механику: здоровье, деньги, характеристики, инвентарь, броски d20. Кажется, что всё просто: отправь модели историю и лист персонажа, получи ответ. На практике самые интересные задачи оказались не в промптах вида «напиши смешно», а на стыке вероятностной модели и детерминированной игровой механики. Ниже — четыре таких места и то, как я их решил.Стек: Node.js 24, Express, встроенный node:sqlite, модели через OpenRouter и общий для сервера и браузера модуль движка. Читать далее

Основное содержимое страницы с новостью.

Уровень сложностиСредний

Время на прочтение5 мин

Охват и читатели6.4K

Кейс

Я делаю браузерную текстовую ролевую игру, где ведущим работает языковая модель. Игрок пишет любое действие своими словами, мастер отвечает рассказом, а игра ведёт механику: здоровье, деньги, характеристики, инвентарь, броски d20. Кажется, что всё просто: отправь модели историю и лист персонажа, получи ответ. На практике самые интересные задачи оказались не в промптах вида «напиши смешно», а на стыке вероятностной модели и детерминированной игровой механики. Ниже — четыре таких места и то, как я их решил.

Стек: Node.js 24, Express, встроенный node:sqlite, модели через OpenRouter и общий для сервера и браузера модуль движка.

1. Механику ведёт код, а не модель

Первая версия была наивной: модели дают весь лист персонажа и просят «ответь и обнови состояние». Это быстро ломается. Модель забывает предметы, раздаёт мечи +100, путает, сколько у героя денег, и через десяток ходов лист персонажа живёт своей жизнью.

Поэтому модель отвечает строгим JSON, в котором кроме рассказа есть только изменения:

{
  "check": { "stat": "ХАР", "dc": 12, "why": "уговорить дракона", "kind": "other", "ok": true },
  "narration": "Дракон открывает один глаз…",
  "hp": -3,
  "add": [{ "name": "Чужой сапог", "icon": "👢", "slot": "feet", "def": 1 }],
  "fact": "Гоша назвал дракона папой"
}

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

let dhp = num(r.hp);
if (dhp < 0 && !r.pierce) { // броня снижает урон, но не больше чем вдвое
  const def = Math.max(0, gear(ch).def);
  const dmg = -dhp, after = Math.max(Math.ceil(dmg / 2), dmg - def);
  dhp = -after;
}

Поле fact уходит в «хронику» — короткий список событий, который подмешивается в промпт следующих ходов. Так мастер помнит, что двадцать ходов назад герой назвал дракона папой, а вся история целиком в контекст не тащится.

2. Один запрос на ход вместо двух

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

Решение — бросать кубик заранее. Сервер кидает d20 до запроса и кладёт результат в промпт:

КУБИК: выпало 14, с бонусом Харизмы итог 16.

Системный промпт объясняет, как с этим работать: если проверка нужна, первым полем ответа идёт check (характеристика, сложность по шкале 8 / 12 / 15 / 18+, успех или провал), а рассказ пишется уже с учётом исхода. Если проверка не нужна, check нет и кубик не упоминается.

Сервер хранит такой ответ «запечатанным». Игроку показывается только «нужна проверка Харизмы, сложность 12» и анимация кубика, а поле ok вырезается:

const publicCheck = (c) => { const { ok, ...rest } = c; return rest; };

Игрок нажимает «Бросить», и рассказ открывается без нового запроса к модели.

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

function sealedRoll(ch, check, die, used) {
  let roll = resolveCheck(ch, check, die, used);
  if (typeof check.ok === 'boolean' && !roll.crit && roll.ok !== check.ok && Number.isFinite(roll.total)) {
    roll = resolveCheck(ch, { ...check, dc: check.ok ? roll.total : roll.total + 1 }, die, used);
  }
  return roll;
}

Критические 1 и 20 не трогаем — это святое. И ещё одна мелочь: потоковую выдачу пришлось научить придерживать текст, пока не ясно, будет ли проверка, иначе игрок успевает прочитать исход до броска.

3. Баг, который удваивал здоровье

Тестировщики пожаловались: стоит мастеру прибавить герою максимум здоровья, как дальше он начинает удваиваться. Причина нашлась в одной фразе промпта: «hp, money, stats — изменения, а не итоговые значения». Про max_hp в ней не было ни слова. Модель видела «Здоровье 10/20», решала, что герой стал крепче, и писала max_hp: 20 — итоговое число. А движок честно прибавлял: 20, 40, 80…

Лечение получилось двухслойным. В промпте max_hp добавлен в список изменений с правилом «меняй редко и не больше чем на четверть; лечение, отдых и еда — это hp». А в движке стоит страховка на случай, если модель всё равно пришлёт итог:

function maxHpDelta(cur, v) {
  if (!v) return 0;
  const lim = Math.max(2, Math.round(cur / 4));
  return clamp(v > lim ? v - cur : v, -lim, lim);
}

Число больше допустимого шага считается итоговым значением и превращается в разницу, а сам шаг ограничен ±25%. Из той же истории выросло ещё одно правило: когда максимум растёт, текущее здоровье растёт на ту же величину. Иначе «благословение: +5 к здоровью» выглядит для игрока так, будто ничего не изменилось.

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

4. Стили мастера: где в промпте живёт характер

Последнее нововведение — «характер» ведущего: Классика, Угар, Хам, Эпос и Документалка. Первая версия разочаровала: стили звучали почти одинаково. Блок стиля стоял в середине длинного системного промпта, после него шли правила формата, механики и примеры, и модель про стиль просто забывала.

Помогли три вещи:

  1. Перенести блок стиля в самый конец системного промпта и прямо написать, что стиль важнее привычной манеры и тона мира, а правила игры, проверки и формат JSON не меняются.

  2. Продублировать его одной строкой прямо перед действием игрока: «(Стиль ведущего: Хам.)».

  3. Дать каждому стилю свой объём ответа. Хаму нужно место, чтобы и отыграть сцену, и поиздеваться над игроком, а Документалке — чтобы закадровый голос успел выдержать паузу.

Строка‑напоминание оказалась самой дешёвой и самой действенной правкой: десяток токенов, а разница — как между двумя разными играми.

Итого
  • Модель придумывает, код считает. Всё, что влияет на баланс, проходит через движок с жёсткими границами.

  • Случайность — до запроса, а не после. Это экономит запрос и делает ответ цельным.

  • У каждого числа в ответе модели есть явный тип и страховка в коде.

  • Важные инструкции — в конец промпта и короткой строкой рядом с вводом пользователя.

Проект называется «Абсурдия», посмотреть, как это работает вживую, можно здесь: https://absurdia.online/?src=habr. Буду рад вопросам про промпты и архитектуру в комментариях.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Полгода в одиночку: как глупая идея превратилась в платформу для ролевых игр с ИИ или сага о том, как я делал Katomu07.0617-10-2026
2Как ИИ не дал загнуться ещё одной ноунейм игре. Даже текстовой08.8824-07-2026
3Карточный бой, который сервер может переиграть: детерминированный движок браузерной MMO на TypeScript08.6419-10-2026
4ZCode, GLM-5.2 и ИИ-роутер: делаем браузерные игры по 7 рублей010.406-08-2026
5Как превратить свободный текст игрока в исполняемый граф08.4627-07-2026
6ИИ-картограф: как карты сшиваются в бесшовный мир — тулсеты (toolsets) для MMORPG игр на PHP, ч. 21010.0812-08-2026
7Нейросеть-автопилот вместо 400 Playwright-тестов0707-07-2026
8Своя модель решений на русском за вечер аренды GPU: что она умеет, сколько стоит и где проигрывает08.809-10-2026
9Практика программирования в эпоху ИИ (разбор реальной задачи на С++ с помощью ИИ агента)08.7927-07-2026
10Эксперименты с ролеплейным ИИ, ч.2. Как устроены мозги NPC в SkyrimNet06.4607-10-2026

Классификация: Наука. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 9. Источник: habr.com.