- BrainTools - https://www.braintools.ru -
В прошлых статьях про инженерную архитектуру ГИГАХРУЩА мы разбирали, как в одном браузерном файле на чистом TypeScript держится тороидальный клеточный мир 1024x1024, кастомный WebGL-рэйкастер, симуляция A-Life и объемный генератор MESH PASS.
Сегодня мы расскажем историю создания речевого ядра проекта — историю о том, как попытка уйти от хардкода привела нас к теории категорий, дистрибутивной семантике и ошеломляющим открытиям: оказывается, если взглянуть на художественные тексты через чистую линейную алгебру, математика [1] сама начинает «понимать» смысл слов и строить сюжетные конструкции.
В этой статье мы покажем, как с помощью векторного анализа корпусов, классов эквивалентности и PCA-осей нам удалось заставить Марковское ядро генерировать живую, контекстную речь для тысяч НПС — без сторонних фреймворков ("dependencies": {}), с задержкой 0 миллисекунд и 100% детерминизмом по SeedRng.
Когда мы только подступили к задаче озвучки НПС (диалоги, баблы над головами, лог речи, посты в локальную соцсеть «Инфосеть Демос»), перед нами встала дилемма.
Жесткий хардкод (If-Else / шаблоны): Написать 5000 заготовок "{npc} пришел в {place}". Скучно, видно швы, игроку надоедает через полчаса.
Наивный Марков по сырому тексту: Выдает колоссальное разнообразие, но страдает «синтаксической шизофренией» — НПС придумывает ложные факты, путает термины и уходит в бессвязный бред.
LLM в облаке: Недопустимо. Контракт ГИГАХРУЩА — автономный браузерный билд, который должен работать 60 FPS даже на кассовом терминале без доступа к сети.
Мы задались вопросом: можно ли обойтись без захардкоженных словарей и регексов, заставив математику саму извлечь структуру человеческого языка из литературы?
Мы взяли 11 обучающих корпусов (16,900+ предложений из «Пикника на обочине», «Самосбор-тредов», «Метро 2033», «S.T.A.L.K.E.R.», «Града обреченного», «Соляриса», советских инструкций Гражданской Обороны и ГОСТов) и написали офлайн-исследователь scripts/discover_categories.ts.
Мы не кормили алгоритму готовые словари. Вместо этого алгоритм скользил окном контекста по тысячам страниц и строил вектор окружения для каждого слова .
Затем мы ввели на словаре отношение эквивалентности :
И произошло то, что поразило нас самих: математика без знания русского языка сама сгруппировала слова в идеальные смысловые кластеры (фактор-множество )!
Вот реальный фрагмент вывода нашего скрипта 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»:
«Сидорович сидит за гермодверью бункера и просит Меченого принести редкий артефакт из вентиляционной шахты.»
Алгоритм не хардкодит имена. При факторизации текста по предложение превращается в абстрактную матричную цепочку:
Формируется чистый грамматический скелет:
Когда движок ГИГАХРУЩА выполняет Категориальный Изоморфизм в рантайме, он подставляет персонажей и предметы нашей мегаструктуры:
Ольга Дмитриевна (врач санпропускника) или Сержант Баринов (начальник оружейки)
проходчик Яков или ликвидатор
дозиметр ДП-5В, чистый фильтр АФУ-300, малахитовый артефакт
защитный шлюз, приёмный покой
И на выходе речевой роутер выдает аутентичную речь ГИГАХРУЩА:
“Ольга Дмитриевна сидит у защитного шлюза санпропускника и просит ликвидатора принести чистый фильтр АФУ-300 из вентиляционной шахты.”
Никакого regex-хардкода: мы взяли живую драму и синтаксис человеческой литературы, но в слоты встали персонажи нашей игры — Ольга Дмитриевна, Сержант Баринов, Яков и Майор Громов!
Чтобы Марковская цепь движка не перескакивала хаотично с существительного на глагол, второй скрипт (scripts/discover_patterns.ts) повторно прошел по корпусам и извлек устойчивые синтаксические скелеты предложений:
Вот реальный вывод из discover_patterns.ts:
Freq: 33 | Pattern: <МЕСТО> -> <ЦЕННЫЙ_ПРЕДМЕТ>
Freq: 29 | Pattern: <СУБЪЕКТ> -> <УГРОЗА>
Freq: 22 | Pattern: <МЕСТО> -> <УГРОЗА>
Freq: 18 | Pattern: <УГРОЗА> -> <МЕСТО>
Марковское ядро больше не блуждает «вслепую». Очерченный скелет указывает графу точный порядок категорий: например, сначала озвучиваем МЕСТО, затем ЦЕННЫЙ_ПРЕДМЕТ, а затем реакцию [2] СУБЪЕКТА.
Но как НПС понимает, какой именно предмет или угрозу подставить в слот? Ведь и «гаусс-пушка», и «грязный бинт» принадлежат одной категории , но применить их в одинаковых ситуациях нельзя!
Здесь вступает в силу Метод Главных Компонент (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:
И скатывает марковский выбор по косинусному сходству векторов:
Если НПС нашел предмет стоимостью 5000 кредитов (), вероятностная матрица моментально гасит вес
грязного бинта и выводит на вершину гаусс-пушку!
Ниже приведены реальные результаты прогона нашего прототипа scripts/markov_core_prototype.ts.
Игровой контекст :
floorZ: 12, roomType: жилая_ячейка, occupation: обыватель, dangerLevel: 5, foundItemValue: 15 (талон на воду).
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Если паразитический мицелий вырвется из талон на воду, бункер Гражданской Обороны придется опечатать на месяц.”
Разбор ядра: Мирный бытовой контекст направил Марковский граф по слотам инструкции ГО, вставив в слот предмета бытовой артефакт талон на воду, а в слот места — бункер Гражданской Обороны.
Игровой контекст :
floorZ: -4, roomType: вентиляционная_шахта, occupation: ликвидатор, dangerLevel: 85, roomMemory: ['черная_слизь'].
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Там только что бетонник уже близко, надо уходить за трансформаторную ячейку. Во время тревоги тоннели могут перестраиваться, говорил старый ликвидатор.”
Разбор ядра: Высокий поднял веса категории
. Движок выбрал бетоноеда и задействовал рефлекс [3] ухода за укрытие.
Игровой контекст :
floorZ: 42, roomType: коридор_сектора, occupation: дежурный_по_герме, dangerLevel: 100, isSamosborActive: true.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Появилась точная цель, но не сам Самосбор, а его обитатели. Мы ковыряемся в кровавом тупике жизнеобеспечения, выносите мясника из центра!”
Разбор ядра: При активной фазе Самосбора граф скатился в корпус канонических Самосбор-тредов, активировав атмосферу паники и зачистки тупиков.
Игровой контекст :
floorZ: -12, roomType: кровавый_тупик, occupation: проходчик, dangerLevel: 60, foundItemValue: 5000.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Проходчик долго осматривал гаусс-пушку. За такой хабар бригадир смены выставит три нормы пайкового концентрата у шлюза.”
Разбор ядра: Выходной параметр ценности предметов foundItemValue: 5000 дал зашкаливающий вектор по оси , моментально выбравший топовое вооружение из слота ценностей.
Игровой контекст :
floorZ: 88, roomType: трансформаторная, powerStatus: 'off', occupation: слесарь-параноик, dangerLevel: 40.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Слесарь-параноик не верил тишине: у гермы шов мокрый. Сначала уплотнитель, потом разговор.”
Разбор ядра: Профессиональные теги гильдии слесарей repair вытащили из корпуса специфические бытовые действия ремонта и проверки герметичности.
Игровой контекст :
floorZ: 104, roomType: продувочная_прачечная, occupation: лаборант НИИ, dangerLevel: 15.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Лаборант НИИ установил дозиметр ДП-5В у продувочной прачечной. Реагенты в норме, инспекционный шлюз чист.”
Разбор ядра: Категория science подтянула строго научные приборы (дозиметр ДП-5В) и канцелярит НИИ “Щит”.
Игровой контекст :
floorZ: 3, roomType: жилая_ячейка, hp: 15, thirst: 95, recentTrauma: true, occupation: домком.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Домком едва дышал на полу: грязный бинт и талон на воду решают быстрее любого разговора.”
Разбор ядра: Критический уровень потребностей [4] сфокусировал ядро на предметах первой помощи (
грязный бинт, талон на воду), проигнорировав оружие и хабар.
Игровой контекст :
floorZ: -69, roomType: алтарь_чернобожия, karma: -100, occupation: мясник, dangerLevel: 95.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Пси-излучение заполнило кровавый тупик. Мясник шептал слова бетона, пока процедурная аномалия не закрыла шлюз.”
Разбор ядра: Отрицательная карма (karma: -100) и специфическая глубинная комната запустили цепочку атомов с тегами mystic и blood.
Игровой контекст :
floorZ: 0, roomType: бункер_ГО, karma: 80, occupation: бригадир смены, dangerLevel: 10.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Бригадир смены проверил защитный шлюз: сначала снимите фильтр АФУ-300, затем доложите дежурному по блоку.”
Разбор ядра: Административный статус НПС и высокий показатель кармы задали уставной порядок команд Гражданской Обороны.
Контекст мира :
floorZ: 16, roomType: сборочный_цех, occupation: дежурный электрик, dangerLevel: 25.
Вектор контекста: .
Синтаксический скелет:
Результат генерации:
“Дежурный электрик зашел в сборочный цех и достал герметизирующую мастику. Если щиток гудит — не трогай тумблер.”
Разбор ядра: Производственная зона направила граф по техническому словарю ремонта и работы со станками.
Для нас эта работа стала доказательством того, что математическая элегантность дистрибутивной алгебры избавляет инди-разработчика от написания тысяч строк хардкода.
Конечно, у этого подхода есть свои границы. Если вы ищете в процедурных репликах глубокого философского романа, длинных многоходовых логических цепочек или тонких детективных расследований — вы их там не найдёте. В своей основе это всё те же вероятностные Марковские цепи.
Но в мире с сотнями тысяч процедурных НПС этот метод, как никакой иной, создаёт осязаемый, живой, независимый от игрока, а главное — контекстно осмысленный слой текстов. Теперь у жителей мегаструктуры есть рот, и они наконец могут кричать то, как из их собственной точки отсчёта выглядит бесконечный ГИГАХРУЩ.
Telegram-паблик разработчиков (обновления и девлоги): https://t.me/gigah_rush [5]
Онлайн-версия игры (прямо в браузере): https://gigahrush.github.io/ [6]
Русская версия на Пикабу Игры: https://games.pikabu.ru/game/gigakhrushch [7]
Автор: TENEVIK
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/33413
URLs in this post:
[1] математика: http://www.braintools.ru/article/7620
[2] реакцию: http://www.braintools.ru/article/1549
[3] рефлекс: http://www.braintools.ru/article/9352
[4] потребностей: http://www.braintools.ru/article/9534
[5] https://t.me/gigah_rush: https://t.me/gigah_rush
[6] https://gigahrush.github.io/: https://gigahrush.github.io/
[7] https://games.pikabu.ru/game/gigakhrushch: https://games.pikabu.ru/game/gigakhrushch
[8] Источник: https://habr.com/ru/articles/1062060/?utm_campaign=1062060&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.