В прошлых статьях про инженерную архитектуру ГИГАХРУЩА мы разбирали, как в одном браузерном файле на чистом TypeScript держится тороидальный клеточный мир 1024x1024, кастомный WebGL-рэйкастер, симуляция A-Life и объемный генератор MESH PASS.

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

В этой статье мы покажем, как с помощью векторного анализа корпусов, классов эквивалентности и PCA-осей нам удалось заставить Марковское ядро генерировать живую, контекстную речь для тысяч НПС — без сторонних фреймворков ("dependencies": {}), с задержкой 0 миллисекунд и 100% детерминизмом по SeedRng.

Приступим-с!
Приступим-с!

1. Откровение дистрибутивной алгебры: Как математика видит слова в тексте

Когда мы только подступили к задаче озвучки НПС (диалоги, баблы над головами, лог речи, посты в локальную соцсеть «Инфосеть Демос»), перед нами встала дилемма.

  • Жесткий хардкод (If-Else / шаблоны): Написать 5000 заготовок "{npc} пришел в {place}". Скучно, видно швы, игроку надоедает через полчаса.

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

  • LLM в облаке: Недопустимо. Контракт ГИГАХРУЩА — автономный браузерный билд, который должен работать 60 FPS даже на кассовом терминале без доступа к сети.

Мы задались вопросом: можно ли обойтись без захардкоженных словарей и регексов, заставив математику саму извлечь структуру человеческого языка из литературы?

Мы взяли 11 обучающих корпусов (16,900+ предложений из «Пикника на обочине», «Самосбор-тредов», «Метро 2033», «S.T.A.L.K.E.R.», «Града обреченного», «Соляриса», советских инструкций Гражданской Обороны и ГОСТов) и написали офлайн-исследователь scripts/discover_categories.ts.

Факторизация словаря ()

Мы не кормили алгоритму готовые словари. Вместо этого алгоритм скользил окном контекста по тысячам страниц и строил вектор окружения для каждого слова w \in W.

Затем мы ввели на словаре отношение эквивалентности \sim:

w_1 \sim w_2 \iff \cos(\vec{v}_{context}(w_1), \vec{v}_{context}(w_2)) > \theta

И произошло то, что поразило нас самих: математика без знания русского языка сама сгруппировала слова в идеальные смысловые кластеры (фактор-множество W / \sim)!

Вот реальный фрагмент вывода нашего скрипта scripts/discover_categories.ts:

[Категория #20] (Векторный узел: ОБОРУДОВАНИЕ / ПАТРОНЫ)
  патронов/патроны (+0.43), оборудования/оборудование (-0.20), 
  огонь (-0.11), заблокировать (-0.21), сальников (-0.25)

[Категория #28] (Векторный узел: ГЕРМЕТИЗАЦИЯ / ПРОХОД)
  герметизация/герметизации (+1.00), кабели/кабелей (+0.57), 
  проход/прохода (+0.50), автомат (+0.10), расход (-0.22)

[Категория #29] (Векторный узел: ЖИЛОЙ БЛОК)
  жилом/жилой (+0.54), свой (+0.56), жертв/жертву (+0.66), ванны/ванной (-0.33)

Алгоритм сам понял, что [чай, кофе, тушенка, концентрат] выполняют одинаковую ролевую функцию в предложении, а [гермозатвор, шлюз, вентиляция, трансформатор] являются объектами пространства!

Живой пример: От Сидоровича и Артема к Ольге Дмитриевне и Майору Громову

Давайте наглядно разберем, как чужие литературные персонажи превращаются в канонических жителей ГИГАХРУЩА.

Представьте исходную строку из обучающего корпуса S.T.A.L.K.E.R. или «Метро 2033»:

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

Алгоритм не хардкодит имена. При факторизации текста по W / \sim предложение превращается в абстрактную матричную цепочку:

\begin{aligned}\text{«Сидорович»} &\to \langle \text{СУБЪЕКТ\_АДМИНИСТРАТОР} \rangle \\\text{«Меченого»} &\to \langle \text{СУБЪЕКТ\_ИСПОЛНИТЕЛЬ} \rangle \\\text{«бункера»} &\to \langle \text{МЕСТО\_УКРЫТИЕ} \rangle \\\text{«вентиляционной шахты»} &\to \langle \text{МЕСТО\_ОПАСНОСТЬ} \rangle \\\text{«артефакт»} &\to \langle \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle\end{aligned}

Формируется чистый грамматический скелет:

\mathcal{S} = \langle \text{СУБЪЕКТ\_АДМИНИСТРАТОР} \to \text{МЕСТО\_УКРЫТИЕ} \to \text{СУБЪЕКТ\_ИСПОЛНИТЕЛЬ} \to \text{ЦЕННЫЙ\_ПРЕДМЕТ} \to \text{МЕСТО\_ОПАСНОСТЬ} \rangle

Когда движок ГИГАХРУЩА выполняет Категориальный Изоморфизм \mathcal{A} в рантайме, он подставляет персонажей и предметы нашей мегаструктуры:

  • \langle \text{СУБЪЕКТ\_АДМИНИСТРАТОР} \rangle \to Ольга Дмитриевна (врач санпропускника) или Сержант Баринов (начальник оружейки)

  • \langle \text{СУБЪЕКТ\_ИСПОЛНИТЕЛЬ} \rangle \to проходчик Яков или ликвидатор

  • \langle \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle \to дозиметр ДП-5В, чистый фильтр АФУ-300, малахитовый артефакт

  • \langle \text{МЕСТО\_УКРЫТИЕ} \rangle \to защитный шлюз, приёмный покой

И на выходе речевой роутер выдает аутентичную речь ГИГАХРУЩА:

“Ольга Дмитриевна сидит у защитного шлюза санпропускника и просит ликвидатора принести чистый фильтр АФУ-300 из вентиляционной шахты.”

Никакого regex-хардкода: мы взяли живую драму и синтаксис человеческой литературы, но в слоты встали персонажи нашей игры — Ольга Дмитриевна, Сержант Баринов, Яков и Майор Громов!

Ульянка тоже не любит математику!
Ульянка тоже не любит математику!

2. Извлечение синтаксических скелетов (Patterns Discovery)

Чтобы Марковская цепь движка не перескакивала хаотично с существительного на глагол, второй скрипт (scripts/discover_patterns.ts) повторно прошел по корпусам и извлек устойчивые синтаксические скелеты предложений:

\mathcal{S} = \langle K_1 \to K_2 \to \dots \to K_n \rangle

Вот реальный вывод из discover_patterns.ts:

Freq: 33 | Pattern: <МЕСТО> -> <ЦЕННЫЙ_ПРЕДМЕТ>
Freq: 29 | Pattern: <СУБЪЕКТ> -> <УГРОЗА>
Freq: 22 | Pattern: <МЕСТО> -> <УГРОЗА>
Freq: 18 | Pattern: <УГРОЗА> -> <МЕСТО>

Марковское ядро больше не блуждает «вслепую». Очерченный скелет указывает графу точный порядок категорий: например, сначала озвучиваем МЕСТО, затем ЦЕННЫЙ_ПРЕДМЕТ, а затем реакцию СУБЪЕКТА.

Если бы мы сразу знали куда нас это заведёт...
Если бы мы сразу знали куда нас это заведёт...

3. Векторизация и PCA-оси: Дорого vs Дешево, Опасно vs Безопасно

Но как НПС понимает, какой именно предмет или угрозу подставить в слот? Ведь и «гаусс-пушка», и «грязный бинт» принадлежат одной категории \langle \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle, но применить их в одинаковых ситуациях нельзя!

Здесь вступает в силу Метод Главных Компонент (PCA). Внутри каждого класса эквивалентности математика вычисляет оси дисперсии от -1.0 до +1.0:

Категория: <ЦЕННЫЙ_ПРЕДМЕТ>
  -1.0 (Дешевый мусор) <------------------------------------> +1.0 (Редчайший артефакт)
  [грязный бинт: -0.9, талон на воду: -0.7]  ...  [дозиметр: +0.4, гаусс-пушка: +1.0]

Категория: <УГРОЗА>
  -1.0 (Бытовой риск) <-------------------------------------> +1.0 (Смертельная катастрофа)
  [истощение: +0.2, черная слизь: +0.75]   ...   [бетонник: +0.85, Самосбор: +1.0]

В момент генерации движок собирает текущий игровой контекст НПС MarkovTextContext:

\vec{v}_{\text{context}} = \begin{pmatrix} v_{\text{danger}} \\ v_{\text{wealth}} \\ v_{\text{need}} \end{pmatrix} \in [-1.0, 1.0]^3

И скатывает марковский выбор по косинусному сходству векторов:

Score(A, \mathcal{C}) = \log P_{\text{interp}}(A \mid h) + \alpha \cdot \text{bitCount}(\text{Tags}_A \cap \text{Tags}_{\mathcal{C}}) + \beta \cdot \cos(\vec{v}_{\text{atom}}, \vec{v}_{\text{context}})

Если НПС нашел предмет стоимостью 5000 кредитов (v_{\text{wealth}} = +1.0), вероятностная матрица моментально гасит вес грязного бинта и выводит на вершину гаусс-пушку!

Хватит уже математики!
Хватит уже математики!

4. Разбор 10 Примеров Сгенерированной Речи НПС в ГИГАХРУЩЕ

Ниже приведены реальные результаты прогона нашего прототипа scripts/markov_core_prototype.ts.

Какие нехорошие культисты!
Какие нехорошие культисты!

Пример 1: Бытовой диалог обывателя у гермодвери

  • Игровой контекст \mathcal{C}: floorZ: 12, roomType: жилая_ячейка, occupation: обыватель, dangerLevel: 5, foundItemValue: 15 (талон на воду).

  • Вектор контекста: \vec{v}_{\text{context}} = (-0.9, -0.7, -0.5)^T.

  • Синтаксический скелет: \langle \text{МЕСТО} \to \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle

  • Результат генерации:

    “Если паразитический мицелий вырвется из талон на воду, бункер Гражданской Обороны придется опечатать на месяц.”

  • Разбор ядра: Мирный бытовой контекст направил Марковский граф по слотам инструкции ГО, вставив в слот предмета бытовой артефакт талон на воду, а в слот места — бункер Гражданской Обороны.


Пример 2: Тревога ликвидатора у вентиляционной шахты

  • Игровой контекст \mathcal{C}: floorZ: -4, roomType: вентиляционная_шахта, occupation: ликвидатор, dangerLevel: 85, roomMemory: ['черная_слизь'].

  • Вектор контекста: \vec{v}_{\text{context}} = (+0.85, +0.1, 0.0)^T.

  • Синтаксический скелет: \langle \text{УГРОЗА} \to \text{СУБЪЕКТ} \rangle

  • Результат генерации:

    “Там только что бетонник уже близко, надо уходить за трансформаторную ячейку. Во время тревоги тоннели могут перестраиваться, говорил старый ликвидатор.”

  • Разбор ядра: Высокий v_{\text{danger}} = 0.85 поднял веса категории \langle \text{УГРОЗА} \rangle. Движок выбрал бетоноеда и задействовал рефлекс ухода за укрытие.


Пример 3: Сирена САМОСБОРА (Паника и задраивание шлюзов)

  • Игровой контекст \mathcal{C}: floorZ: 42, roomType: коридор_сектора, occupation: дежурный_по_герме, dangerLevel: 100, isSamosborActive: true.

  • Вектор контекста: \vec{v}_{\text{context}} = (+1.0, -1.0, +0.8)^T.

  • Синтаксический скелет: \langle \text{УГРОЗА} \to \text{СУБЪЕКТ} \rangle

  • Результат генерации:

    “Появилась точная цель, но не сам Самосбор, а его обитатели. Мы ковыряемся в кровавом тупике жизнеобеспечения, выносите мясника из центра!”

  • Разбор ядра: При активной фазе Самосбора v_{\text{danger}} = 1.0 граф скатился в корпус канонических Самосбор-тредов, активировав атмосферу паники и зачистки тупиков.


Пример 4: Находка ценного хабара (Гаусс-пушка)

  • Игровой контекст \mathcal{C}: floorZ: -12, roomType: кровавый_тупик, occupation: проходчик, dangerLevel: 60, foundItemValue: 5000.

  • Вектор контекста: \vec{v}_{\text{context}} = (+0.6, +1.0, 0.0)^T.

  • Синтаксический скелет: \langle \text{СУБЪЕКТ} \to \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle

  • Результат генерации:

    “Проходчик долго осматривал гаусс-пушку. За такой хабар бригадир смены выставит три нормы пайкового концентрата у шлюза.”

  • Разбор ядра: Выходной параметр ценности предметов foundItemValue: 5000 дал зашкаливающий вектор по оси v_{\text{wealth}}, моментально выбравший топовое вооружение из слота ценностей.


Пример 5: Слесарь-параноик у мокрого шва гермодвери

  • Игровой контекст \mathcal{C}: floorZ: 88, roomType: трансформаторная, powerStatus: 'off', occupation: слесарь-параноик, dangerLevel: 40.

  • Вектор контекста: \vec{v}_{\text{context}} = (+0.4, -0.2, +0.4)^T.

  • Синтаксический скелет: \langle \text{СУБЪЕКТ} \to \text{МЕСТО} \rangle

  • Результат генерации:

    “Слесарь-параноик не верил тишине: у гермы шов мокрый. Сначала уплотнитель, потом разговор.”

  • Разбор ядра: Профессиональные теги гильдии слесарей repair вытащили из корпуса специфические бытовые действия ремонта и проверки герметичности.


Пример 6: Лаборант НИИ в продувочной прачечной

  • Игровой контекст \mathcal{C}: floorZ: 104, roomType: продувочная_прачечная, occupation: лаборант НИИ, dangerLevel: 15.

  • Вектор контекста: \vec{v}_{\text{context}} = (-0.7, +0.2, -0.8)^T.

  • Синтаксический скелет: \langle \text{СУБЪЕКТ} \to \text{МЕСТО} \to \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle

  • Результат генерации:

    “Лаборант НИИ установил дозиметр ДП-5В у продувочной прачечной. Реагенты в норме, инспекционный шлюз чист.”

  • Разбор ядра: Категория science подтянула строго научные приборы (дозиметр ДП-5В) и канцелярит НИИ “Щит”.


Пример 7: Раненый домком просит помощи (Критический голод и ранение)

  • Игровой контекст \mathcal{C}: floorZ: 3, roomType: жилая_ячейка, hp: 15, thirst: 95, recentTrauma: true, occupation: домком.

  • Вектор контекста: \vec{v}_{\text{context}} = (+0.3, -0.9, +0.95)^T.

  • Синтаксический скелет: \langle \text{СУБЪЕКТ} \to \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle

  • Результат генерации:

    “Домком едва дышал на полу: грязный бинт и талон на воду решают быстрее любого разговора.”

  • Разбор ядра: Критический уровень потребностей v_{\text{need}} = 0.95 сфокусировал ядро на предметах первой помощи (грязный бинт, талон на воду), проигнорировав оружие и хабар.


Пример 8: Мясник у Кровавого Алтаря (Пси-излучение)

  • Игровой контекст \mathcal{C}: floorZ: -69, roomType: алтарь_чернобожия, karma: -100, occupation: мясник, dangerLevel: 95.

  • Вектор контекста: \vec{v}_{\text{context}} = (+0.95, +0.5, +0.7)^T.

  • Синтаксический скелет: \langle \text{УГРОЗА} \to \text{СУБЪЕКТ} \rangle

  • Результат генерации:

    “Пси-излучение заполнило кровавый тупик. Мясник шептал слова бетона, пока процедурная аномалия не закрыла шлюз.”

  • Разбор ядра: Отрицательная карма (karma: -100) и специфическая глубинная комната запустили цепочку атомов с тегами mystic и blood.


Пример 9: Инструктаж бригадира смены у бункера ГО

  • Игровой контекст \mathcal{C}: floorZ: 0, roomType: бункер_ГО, karma: 80, occupation: бригадир смены, dangerLevel: 10.

  • Вектор контекста: \vec{v}_{\text{context}} = (-0.8, +0.4, -0.9)^T.

  • Синтаксический скелет: \langle \text{СУБЪЕКТ} \to \text{МЕСТО} \rangle

  • Результат генерации:

    “Бригадир смены проверил защитный шлюз: сначала снимите фильтр АФУ-300, затем доложите дежурному по блоку.”

  • Разбор ядра: Административный статус НПС и высокий показатель кармы задали уставной порядок команд Гражданской Обороны.


Пример 10: Дежурный электрик в сборочном цеху

  • Контекст мира \mathcal{C}: floorZ: 16, roomType: сборочный_цех, occupation: дежурный электрик, dangerLevel: 25.

  • Вектор контекста: \vec{v}_{\text{context}} = (-0.5, 0.0, -0.3)^T.

  • Синтаксический скелет: \langle \text{СУБЪЕКТ} \to \text{МЕСТО} \to \text{ЦЕННЫЙ\_ПРЕДМЕТ} \rangle

  • Результат генерации:

    “Дежурный электрик зашел в сборочный цех и достал герметизирующую мастику. Если щиток гудит — не трогай тумблер.”

  • Разбор ядра: Производственная зона направила граф по техническому словарю ремонта и работы со станками.

Эмиль в шоке от нашего марковского ядра!
Эмиль в шоке от нашего марковского ядра!

5. Ограничения и главное откровение

Для нас эта работа стала доказательством того, что математическая элегантность дистрибутивной алгебры избавляет инди-разработчика от написания тысяч строк хардкода.

Конечно, у этого подхода есть свои границы. Если вы ищете в процедурных репликах глубокого философского романа, длинных многоходовых логических цепочек или тонких детективных расследований — вы их там не найдёте. В своей основе это всё те же вероятностные Марковские цепи.

Но в мире с сотнями тысяч процедурных НПС этот метод, как никакой иной, создаёт осязаемый, живой, независимый от игрока, а главное — контекстно осмысленный слой текстов. Теперь у жителей мегаструктуры есть рот, и они наконец могут кричать то, как из их собственной точки отсчёта выглядит бесконечный ГИГАХРУЩ.

Ульяна хочет пить!
Ульяна хочет пить!

Полезные ссылки проекта:

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


  1. Void-Cowboy
    23.07.2026 01:43

    таки живы!

    вы кстати не думали опубликоваться в телеге как web-app? по идее ваш формат как раз идеально войдет


    1. TENEVIK Автор
      23.07.2026 01:43

      Блин, а ведь это гениальная идея! Почему то даже не думали в эту сторону!

      Спасибо за наводку, займёмся прям на этих выходных)


      1. Void-Cowboy
        23.07.2026 01:43

        могу еще странных идей подкинуть - например что бы сервер был доступен и из-под сети yggdrasil что бы всякие блокировки и тд не мешали наслаждаться гигахрущем)


  1. cyberxx
    23.07.2026 01:43

    Вместо этого алгоритм скользил окном контекста по тысячам страниц и строил вектор окружения для каждого слова

    Это что-то типа Word2vec?