- PVSM.RU - https://www.pvsm.ru -
Привет!
Хочу рассказать о генераторе квестов, который я делаю для своей браузерной ZPG.

Несмотря на то, что вопрос автоматической генерации заданий в RPG достаточно древний, общедоступных работающих версий таких генераторов почти нет (скорее совсем нет), если не считать совсем примитивных вариантов. Работ по этой теме тоже не много, хотя, если активно гуглить, кое-что можно откопать. Поэтому надеюсь, что этот текст (и сам генератор, ссылка на репозиторий есть в конце статьи) будет полезен.
Для торопливых: визуализация одного из полученных заданий [1].
Персонаж игрока (герой) в игре действует полностью самостоятельно и основным его занятием является, конечно, выполнение заданий NPC.
Ключевым моментом является то, что задания нелинейные и игрок должен делать выбор, какому NPC его герой будет помогать, а какому вредить. От этого напрямую зависит «судьба мира» (например, NPC может покинуть игру, если ему многие будут вредить).
Кроме того, герой обладает «характером», который может влиять на его действия при выполнении задания (например, можно указать, что он будет стремиться помогать конкретному NPC).
Опыт герой получает только за выполнение заданий.
Исходя из этого, был нужен механизм создания интересных и сложных заданий, не противоречащих здравому смыслу и требующих от игрока подумать, прежде чем делать выбор.
Далее вместо «квест» я буду использовать термин «история», как более удобный для объяснения (каждый квест и есть история, ограниченная парой условностей, поэтому разумнее говорить именно о генераторе историй).
Требования, которые я выдвинул к историям, можно сформулировать так:
Кроме требований к самим историям есть ещё несколько требований непосредственно к генератору:
Определившись с требованиями, следовало определиться с тем, что, собственно, такое квест или история. В итоге я пришёл к следующему определению:
История — это направленный ацикличный связный граф. Узлы которого описывают состояние (требования к состоянию) объектов-участников и окружающей среды на конкретном этапе истории, а рёбра определяют возможные переходы между этими этапами.
Из определения плавно вытекает идея реализации истории в виде машины состояний, которая отдаётся под управление игре.
В итоге наш генератор должен на основе информации о текущем состоянии мира создавать граф истории, обладающей перечисленными ранее свойствами.
В свою очередь, полученный граф должен интерпретироваться логикой игры. Которая на основе информации о текущем состоянии истории и ожидаемом будущем состоянии будет инициировать необходимые изменения в действиях героя или окружающей среде, ведущие к выполнению всех требований, необходимых для перехода истории на следующий этап.
Сам интерпретатор реализуется достаточно тривиально (в конце статьи будет ссылка на пример реализации).
Итак, история — это граф, состоящий из узлов и рёбер. Каждый узел обладает списком требований (или проверок, если хотите), которые должны выполняться, чтобы история могла перейти в состояние, соответствующее узлу. Требованием может быть нахождение героя в конкретном месте или наличие у него нужной суммы денег.
Кроме самих требований к состоянию «мира» для каждого узла добавлен список действий, которые необходимо выполнить, когда история окажется в этом узле. Конечно, их можно было бы оформить и в качестве отдельных узлов с требованиями, но это значительно увеличило бы сам граф и усложнило его анализ разработчиками. Действием, в данном случае, может быть отправка сообщения игроку, начало сражения с монстром или выдача награды герою. Такие же списки действий назначены на начало и конец движения по ребру.

Примерно вот так может выглядеть простая история.
Перемещение между узлами можно представить в виде цикла:
Узлы истории удобно разделить на типы, определяющие их роль:

А вот так выглядит более сложная история.
История может состоять из нескольких «атомарных» заданий. Например, больной NPC может отправить героя за лекарством к ведьме, которая потребует за него услугу (выполнение другого «вложенного» задания).
Поэтому строится она из набора «атомарных» шаблонов. Шаблон представляет собой такой же граф истории, но со всеми вариантами развития событий (даже теми, которые могут сделать историю противоречивой).
Важным моментом здесь является соединение шаблонов (помним, что в итоге у нас должен быть связный граф):
После долгих размышлений было решено, что все истории, в общем-то, характеризуются набором ключевых «объектов» (мест, персонажей, предметов). Поэтому для каждого шаблона создаётся набор конструкторов, принимающих данные фиксированного формата. Вот несколько примеров конструкторов:
При генерации дочерней истории, родительская фильтрует все шаблоны по наличию необходимого ей конструктора, из которых уже выбирает случайный.
Связывание родительской истории с началом дочерней происходит просто — родительская создаёт ребро из нужного узла в единственный стартовый узел дочерней истории.
Конечных же узлов может быть несколько, и они могут иметь разный семантический смысл для истории. Например, в истории про ведьму герой может не только выполнить её задание, но и провалить его, соответственно, дуги из разных конечных узлов необходимо вести строго в соответствующие им по смыслу узлы родительской истории.
Для этого в каждом конечном узле указывается список результатов задания для каждого из объектов участников. На текущий момент возможных результатов три:
Благодаря этому, для каждого конечного узла можно определить его общий смысл и правильно связать его с родительской историей.
После описанных выше действий, у нас на руках будет визуально правильный граф истории, но без гарантии непротиворечивости. В таком графе содержатся все варианты развития событий, даже противоречащие текущему состоянию мира.
Например, в одной из веток герой может вредить своему другу. Или два NPC, отмеченные врагами, могут действовать сообща.
Поэтому историю надо дополнительно обработать. Это предполагает следующие действия:
Полученный граф будет как минимум непротиворечивым. Но он может стать невыполнимым или несвязанным (история станет нецельной). Поэтому следующим шагом является проверка всех необходимых свойств полученной истории.
Если проверка прошла успешно, то у нас появилась новая история.
Если нет — начинаем создавать её сначала.
Подход с полным откатом может привести к очень длительной генерации, в случае, если мир игры очень маленький и обладает большим количеством связей. Но на практике обычно объектов в мире много, а связей между ними мало, поэтому проблем нет.
В случае частых ошибок, игра, использующая генератор, может уменьшить количество устанавливаемых свойств мира (например, перестать учитывать отношение дружбы). Сам генератор не пытается делать дополнительных предположений о состоянии мира.
Генератор написан на Python, выложен на github под BSD лицензией:
Автор: Tiendil
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/48055
Ссылки в тексте:
[1] визуализация одного из полученных заданий: http://tiendil.org/static/habrahabr/large_quest.svg
[2] questgen: https://github.com/Tiendil/questgen
[3] example.py: https://github.com/Tiendil/questgen/blob/master/helpers/example.py
[4] http://the-tale.org: http://the-tale.org
[5] Источник: http://habrahabr.ru/post/201680/
Нажмите здесь для печати.