← Мои проекты

Собственный проект · закрытая бета

МежаРолевая книга, которую ведёт нейросеть

Текстовая ролевая игра для одного игрока: тёмная мистика, Петербург и Старая Ладога, лето 2012-го. Мастер собран из нескольких ИИ-ролей, кости бросает программа, а память кампании хранится в базе. Моя задача в том, чтобы длинная история не разваливалась: мир помнит, провал честный, герой не знает лишнего.

Роль
Автор: канон, движок, ИИ-система и интерфейс
Стек
Python · FastAPI · PostgreSQL + pgvector · React
Статус
Закрытая бета · вмежу.рф
Глава I · ПрактикаЭлектричка Ладожский — Волховстрой · 18 июня, 08:30

Пока Гольц идёт по вагону, присматриваюсь, что он отмечает в списке — не только же фамилии.

ЖребийРазглядеть список Гольца
Сдюжил
83101
3 зарубки1 тень
Чутьё 3 + Сыск 1= горсть 4Преграда 2
  • успехВ колонке рядом с фамилиями — пометки «+» и «−»; напротив Кольцовой — «+»
  • теньГольц поднял глаза и увидел, что ты смотришь в его список
коммит 9f3c1a7e · №1

Гольц держит планшет под углом, но вагон качает, и на повороте лист ложится к свету. Список отпечатан на кафедральном принтере: фамилии, группа, отметка о сборе. А правее — колонка, которой в таких списках не бывает.

Пометки карандашом: «+», «−», у большинства пусто. Напротив «Кольцова А. В.» стоит «+». Твоей строки не видно — её закрывает его большой палец.

Он доходит до вашего отсека, находит тебя глазами поверх очков — и ты понимаешь, что смотрел слишком долго.

Ход из демо-режима игры: заявка, бросок и проза мастера
Стол мастера в игре Межа: лента с броском «Разглядеть список Гольца», персонажи, улики и состояние героя
Та же лента на телефоне: карточка броска и проза

Стол мастера на большом экране и та же лента на телефоне. Игру читают вечером и в дороге, поэтому тёмная тема в ней единственная. Снимки сделаны в демо-режиме игры.

01Замысел

Книга, а не переписка с ботом

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

  1. I

    Провал честный

    Кости бросает программа. Рассказчик получает результат как неизменяемый факт и не может его смягчить.

  2. II

    Мир помнит

    Факты, люди, вещи, отношения и тайны лежат в базе и подбираются к каждому ходу.

  3. III

    Мир не ждёт

    У событий своё расписание. Гроза начнётся в шесть вечера, даже если герой сидит в палатке.

  4. IV

    Герой знает только своё

    Незнакомка остаётся «девушкой с гитарой», пока герой не услышал её имя.

6 глав
и 38 сцен в первой книге кампании «Осколок»
56
персонажей, у каждого мотивы и голос, у 47 есть тайны
18
вех, по которым засчитываются главы
776
автотестов движка
202
хода в сквозном прогоне всей книги

02Один ход

Что происходит, пока игрок ждёт первую строку

Игрок пишет одну реплику. Ответ приходит потоком, но до первого слова над ходом успевают поработать несколько ролей. Вот тот же ход из вагона, разобранный по шагам.

  1. 01Заявка

    Одна реплика, до 500 символов. Кнопок с вариантами нет: мастер сам разбирает, где речь, а где действие, и решает, есть ли у него цена.

    Пока Гольц идёт по вагону, присматриваюсь, что он отмечает в списке — не только же фамилии.

    POST /campaigns/{id}/turn → поток событий SSE

  2. 02Арбитр

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

    {  "needs_roll": true,  "attribute": "chutyo",  "skill": "sysk",  "barrier": 2,  "modifier": 0,  "label": "Разглядеть список Гольца",  "stakes_success": "увидеть, что он отмечает",  "stakes_fail": "ничего не разобрать в тряске",  "stakes_shadows": "Гольц замечает взгляд"}

    Упрощено: в схеме ещё перемещение, время, адресаты реплики и встречный бросок.

  3. 03Жребий

    Горсть считает и бросает программа: Чутьё 3 и Сыск 1 дают четыре кости. Восьмёрка и десятка приносят три зарубки, единица даёт тень. Преграда 2 пройдена, это «Сдюжил», но с осложнением. Нейросеть выбирает только навык и Преграду.

    ЖребийРазглядеть список Гольца
    Сдюжил
    83101
    3 зарубки1 тень
    Чутьё 3 + Сыск 1= горсть 4Преграда 2
    коммит 9f3c1a7e · №1
  4. 04Рассказчик

    Результат приходит к Рассказчику блоком «Жребий уже брошен (неизменяемый факт)» вместе с обязательными фактами успеха и тени. Он видит главу дословно, до 80 тысяч токенов, но не видит тайн, которые герою рано знать. Проза идёт потоком, 120–250 слов.

    Гольц держит планшет под углом, но вагон качает, и на повороте лист ложится к свету. Список отпечатан на кафедральном принтере: фамилии, группа, отметка о сборе. А правее — колонка, которой в таких списках не бывает.

    Пометки карандашом: «+», «−», у большинства пусто. Напротив «Кольцова А. В.» стоит «+». Твоей строки не видно — её закрывает его большой палец.

    Он доходит до вашего отсека, находит тебя глазами поверх очков — и ты понимаешь, что смотрел слишком долго.

  5. 05Проверки

    Готовый текст проверяют правила в коде и до шести моделей-судей параллельно. Если ход сломан, игрок видит, как текст заменяется: Рассказчик переписывает его с заметкой, что исправить. Переписываний не больше двух.

    • Честность броскапровал не смягчён, тень дала осложнениеRollHonestyCheck
    • Утечка тайннет секретов, которые герою рано знатьLeakCheck
    • Слышимостьреплики слышат только те, кто рядомEarshotCheck
    • Непрерывностьнет отката к прошлому ходу, вещи на своих местахContinuityCheck
    • Пространствопереход в другое место показанSpatialCheck
    • Именанезнакомцы названы ярлыками, род не перепутанunknown_named · name_leak
  6. 06Память

    После ответа фоновый Экстрактор записывает новые факты, отношения и время. Каждый факт получает эмбеддинг в pgvector, чтобы через сто ходов найтись по смыслу, а не по словам. Следующий ход ждёт Экстрактор не дольше 20 секунд.

    • факту Гольца в списке колонка пометок: «+», «−», у большинства пусто
    • фактнапротив Кольцовой стоит «+»
    • отношениеГольц заметил, что Савин смотрел в его список
    • время18 июня, 08:30 · электричка Ладожский — Волховстрой

    Пример того, что сохраняется после этого хода.

При смене сцены подключается Режиссёр: подводит итог прошлой сцены и ставит следующую. Ещё одна быстрая модель предлагает игроку 2–3 коротких действия, не называя незнакомцев по имени.

03Жребий

Кости, которые нельзя подкрутить

Броски идут по собственной системе «Жребий»: горсть десятигранных костей, свойство героя плюс навык. Набрал зарубок не меньше Преграды, значит «Сдюжил», на две больше означает «Верх». Ни одной зарубки, но есть тень, и это «Лихо»: мир делает ход против героя.

  • 2–6пусто50 %
  • 7–9зарубка30 %
  • 10две зарубки10 %
  • 1тень, осложнение10 %

Честность, которую можно проверить

Каждая кость выводится из секретного зерна кампании: HMAC-SHA-256 от номера броска и номера кости. Отпечаток зерна, его SHA-256, игрок видит с первого хода на билете главы и под каждым броском. Подменить зерно, чтобы переиграть неудобный бросок, нельзя: отпечаток перестанет совпадать.

В игре зерно хранится на сервере, игрок видит только отпечаток, а раскрытие зерна пока не включено. Здесь можно сделать то, что в игре ещё впереди: вскрыть зерно и пересчитать каждый бросок. Алгоритм перенесён из движка и сверен с ним на эталонных значениях.

Глава I · Практикане раскрыт

кампания6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b

коммитпоявится, когда загрузится скрипт

Зерно создано в вашем браузере, он здесь играет роль сервера. Пока видно только отпечаток.

Выберите проверку: кости выпадут из зерна, отпечаток которого уже на билете.

    04Знания героя

    Мастер знает всё. Герой знает только то, что видел

    В вагоне едет девушка с гитарой. Герой не знает, как её зовут, и игра не даст ему узнать это случайно: ни в прозе, ни в подсказках, ни на портрете.

    Персонажи

    Портрет ещё не открыт: куртка на крючкеЛена Чиж, портрет из игры

    девушка с гитаройимени не знаешь

    Лена ЧижБригадир второй бригады; гитара у костра — её.

    Что получает Рассказчик

    девушка с гитарой (имени герой не знает — в прозе только так: «девушка с гитарой»; имя у него уже есть и оно одно, СВОЁ не выдумывай (ни фамилии, ни прозвища); прозвучит настоящее — тогда можно): …

    Что видит Арбитр

    Лена Чиж · хочет: зачёта для своих и тишины · боится: Гольца · врёт: «Горин к рыбакам ходит — там баба, ясно же»

    canon/scenarios/oskolok/roster/lager.yaml

    - slug: chizh  name: "Лена Чиж"  aliases: ["Чиж", "Лена", "бригадир второй бригады"]  age: 21  yav: "Бригадир второй бригады; гитара у костра — её."  look: "Девушка-бригадир, быстрая, в бандане; голос садится к концу песни, и все подпевают."  wants: "Зачёта для своих и тишины."  fears: "Гольца."  lie: "«Горин к рыбакам ходит — там баба, ясно же»."  secrets:    - visibility: gm_only      owner_may_reveal: true      text: "Знает, что Горин «ходит к рыбакам», и искренне считает, что это баба; скажет между песнями."  # дальше в карточке: голос, горсти, здоровье, то, чего она не сделает ни при каких условиях
    • Ярлык вместо имени.Пока имя не прозвучало, в контексте Рассказчика его нет. Есть описание и запрет выдумывать своё.
    • Имя открывает проза.Экстрактор отмечает имя известным, только если в тексте видно, откуда оно взялось: человек представился, его окликнули при герое, оно написано на бирке.
    • Портрет вместе с именем.До этого в листе персонажей висит заглушка.
    • Утечка переписывается.Если имя прозвучало раньше времени, ход переписывается. Проверка понимает падежи: «брат Ани» не считается утечкой, «Петрович» не путается с «Петровной».
    • Тайны по вехам.72 тайны в каноне видит только мастер. Рассказчику достаются открытые, по номеру главы и взятым вехам: сон Ани не приснится раньше осколка.

    05Часы мира

    Мир не ждёт героя

    У каждой главы своё расписание. Событие случается в назначенный час, где бы ни был герой. Стоял рядом, значит увидит сам. Был в другом месте, значит узнает позже: от человека, по следу или звонком. В Старой Ладоге связь ловит только на холме у крепости, и это тоже записано в каноне.

    Часы игры18 июня, 17:30
    1. Гроза над ВолховомСтарая Ладога

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

      Как дойдёт новость: человек: Горин, не поднимая головы от помпы: «залило. Помпа промоину не берёт — стоит и стоит, хоть ведром вычерпывай» след: настил мокрый и тяжёлый, тент лежит в глине звонок: Аня с холма, когда ловит: «тут вода пришла, траншею залило, ты где?»

    2. Макс находит тему на форумеПетербург, комната 1312

      Незнакомые люди пишут, что им снится одно и то же: вода и свет вдалеке.

      Как дойдёт новость: звонок: смс, которое доходит, когда телефон ловит: «нашёл тему на форуме, люди пишут, что им снится одно и то же. бред? посмотри, как там вода»

    3. Гольц уезжает в ВолховДвор базы

      До ливня уезжает на «Ниве»: «к утру хлеб; дорогу размоет — заночую там».

      Как дойдёт новость: человек: Аня у тента: «Маркович в Волхов уехал, до дождя» след: у крыльца сухой прямоугольник там, где стояла «Нива»

    4. Палыч запирает воротаДвор базы

      Закрывает ворота на крюк и обходит двор с фонарём; Жучка за ворота с ним не идёт.

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

    5. Горин уходит «к рыбакам»Траншея

      Уходит от помпы за ворота без фонаря; возвращается в четыре мокрый по пояс, хотя дождь кончился в три.

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

    6. Промоина выносит слойРаскоп у северного вала

      К рассвету промоина открылась на локоть шире и вынесла ржавый фонарь, часы «Командирские» и серый с зеленцой обломок.

      Как дойдёт новость: человек: Аня у стенки за бровкой: «иди сюда. Тут часы. И фонарь. И вот это» след: на носилках у бровки фонарь «ФАР» и часы на прелом ремешке звонок: Аня, когда телефон ловит: «промоина вынесла, тут часы и фонарь, ты где?»

    7. Гольц звонит с холмаХолм у крепости

      Поднимается туда, где единственное место со связью, и говорит по телефону долго, стоя на одном месте.

      Как дойдёт новость: человек: Аня, глядя в кружку: «он там полчаса стоял. Я думала, с женой» след: трава на пятачке вытоптана кругом, в траве колпачок от ручки

    8. Носитель мёрзнет у костраДвор базы

      Тот, у кого в кармане осколок, мёрзнет первым: в июне, у огня.

      Как дойдёт новость: человек: Аня тихо: «у тебя он тоже не греется?» след: у костра чужой свитер поверх штормовки; Жучка от этого места отходит

    • Заявку времени разбирает код.«До утра» значит 07:00, «до вечера» значит 19:00, «до утра двадцатого» означает точную дату. Такой разбор главнее оценки, которую даёт Арбитр.
    • Каждый ход занимает время.Не меньше десяти минут. Большой скачок рисует в ленте карточку «Прошло…» с датами.
    • Мир делает ход сам.Если игрок шесть ходов подряд ничего не меняет или сцена затянулась, у мира появляется свой ход.
    • На тонких местах время рвётся.Внутри прошло меньше суток, снаружи несколько дней, и первая фраза после выхода должна дать это почувствовать.

    06Канон

    Один источник правды для текста, кода и базы

    Мир, люди, места, главы и правила лежат в YAML: 32 файла, около 7 800 строк. Из этих же файлов собираются промпты, константы движка и стартовое состояние базы, поэтому описание мира и его поведение не расходятся.

    canon/

    • rules.yamlконстанты «Жребия», сутки, бюджеты контекста
    • chapter1…6.yamlсцены, вехи, расписание мира
    • roster/*.yamlлюди, мотивы, тайны
    • places/*.yaml54 места и связи между ними
    • llm_tiers.yamlмодели по ролям и тарифам
    1. Бриф мастера8,5 тыс. символов, около 3,4 тыс. токенов. Неизменная часть промпта Рассказчика, поэтому она кэшируется.
    2. ДвижокКости, сутки, Преграды и вехи читаются из канона, а не дублируются в коде.
    3. БазаМеста, люди, существа и законы мира засеваются в PostgreSQL одной командой.
    4. ТарифыКакая модель играет какую роль и сколько стоит её токен, тоже записано в каноне, а не зашито в код.

    python tools/validate_canon.py

    сценарий oskolok: файлов 29  мест 54  героев 3  NPC 60  существ 9  вещей 7  сцен 38  вех 18
    
    0 ошибок, 12 предупреждений

    Валидатор импортирует функции самого движка и проверяет ссылки между файлами, формулы горстей, условия вех, ворота времени, длину целей глав и то, что каждая тайна открывается существующей вехой.

    07Проверка игрой

    Один удачный ход ничего не доказывает

    Ошибки ИИ-мастера видны не в отдельном ответе, а через двадцать ходов. Поэтому игру проверяет не только набор тестов, но и сквозные прогоны.

    1. Автоигрок и критик

      Модель с характером («осторожный сыщик», «наглый», «ломает мастера», «романтик») играет через API, другая модель оценивает каждый ход от 1 до 5 и пишет, что сломалось. На этих прогонах сравнивали модели: дешёвая получала в среднем 4,38 балла за $0,009 в ход, более сильная 4,69 за $0,027.

    2. Ни одной вехи за 206 ходов

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

    3. Перестройка главы

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

    4. 202 хода, 18 вех из 18

      Третий сквозной прогон прошёл всю книгу, шесть глав от Ладоги до Хибин. После него появились финал «Книга завершена» и эпилог: Рассказчик пишет его по всей прожитой истории.

    776автотестов движка

    12тестов-инвариантов, у каждого подпись, от какого бага он защищает

    0живых вызовов нейросети в тестах: защита в conftest бросает ошибку

    08Экономика

    Сколько стоит один ход

    Каждый вызов модели пишется в журнал с ролью и ценой, расход виден по ходам и по ролям. Тариф выбирается при создании книги: на обычном мастер стоит около $0,04–0,08 за ход, на экономичном DeepSeek от $0,01 до $0,04, прозу GigaChat можно попробовать как эксперимент.

    • Кэш префикса.Сообщения собираются так, чтобы начало не менялось: неизменный системный блок, затем история, которая только дописывается, и в конце изменчивая часть. Закэшированный вход у основной модели в десять раз дешевле: $0,25 против $2,50 за миллион токенов.
    • Ложная проверка стоит денег.Ход без переписывания обходился примерно в $0,032, с одним в $0,047, с двумя в $0,072. Когда выяснилось, что половина переписываний за двое суток была ложной, проверки исправили и прогнали заново на ходах за неделю: старая логика остановила бы шесть ходов, новая остановила один, и это была настоящая утечка имени.
    Выбор мастера для новой книги: тарифы и режим «По сценарию» или «Песочница»
    Выбор мастера при создании книги

    09Интерфейс

    Полночный документ

    Не меню фэнтези-игры, а вещь из нашего мира, в которую просочилась тьма: сажа, старая бумага, латунь. Проза набрана Literata, документы эпохи PT Mono, интерфейс системным шрифтом. Удача латунная, беда красная, а зелёного и жёлтого в игре нет.

    Пролог главы: буквица и проза на телефоне
    Пролог главы
    Лента: карточка броска остаётся в книге навсегда
    Бросок в ленте
    Лист героя: здоровье, дух, свойства
    Лист героя
    • Книга прежде интерфейса.Проза занимает экран, всё служебное открывается по тапу и уходит. Никаких полосок здоровья, ачивок и пузырей чата.
    • Одна рука.Поле ввода, бросок и лист снизу, под большим пальцем. На экране от 1280 пикселей лента превращается в стол мастера с панелями.
    • Доступность.Лента объявляется экранным дикторам новыми абзацами, контраст посчитан для каждого цвета, при ограничении анимаций кости появляются сразу.
    • Лица.55 портретов нарисованы нейросетью в одном стиле, по одному лицу на человека во всех книгах. Портрет стоил около двух с половиной центов.
    • Без лишнего.Клиент на React без других зависимостей: свой роутер, поток ответа читается из fetch.
    Лист героя на большом экране: свойства, навыки, состояние, вещи

    Что я вынес из Межи

    1. Нейросеть пишет, программа решает.Кости, время, вехи и знания героя держит код. Модели остаётся то, что она делает лучше всего: живая проза.
    2. Проверять долгой игрой.Самые дорогие ошибки нашлись в полных прогонах: события, которые не наступают, и вехи, которые нельзя взять.
    3. Считать каждую проверку.Ложное срабатывание стоит денег и ожидания игрока, поэтому у проверок есть журнал и прогон на настоящих ходах.
    Следующий проектhtmlpractice

    Давайте знакомиться

    Хорошие проекты
    начинаются
    с разговора.

    Нужен технический партнёр, помощь команде или решение для рабочего процесса? Расскажите, что хотите изменить — обсудим, чем я могу помочь.

    @work_ilyaЛично мне в Telegram
    Илья Болясов
    Илья БолясовРазработчик. Технический партнёр.