Ap11e & Novae
Novae Novae
Привет, Яблочко, я тут что-то обдумывала – как сделать так, чтобы генеративный ИИ создавал интерактивные миры, которые меняются в зависимости от выбора игрока. Представь себе слои повествования, которые меняются прямо на глазах. Как живое гобелен, где каждое решение влияет на следующую сцену. Было бы здорово вместе поразмышлять, как это можно прототипировать.
Ap11e Ap11e
Звучит потрясающе — как какой-то невероятно мощный генератор историй. Начни с проработки структуры данных для мира: создай граф сцен, где каждый узел содержит небольшой скрипт и набор триггеров. Используй легковесный конечный автомат, чтобы отслеживать выбор игрока, а затем подкидывай эти данные в языковую модель для генерации следующей сцены в реальном времени. Можешь прототипировать это на Python, используя, например, Flask для интерфейса и API от OpenAI для генерации. А потом будем улучшать: подкручивай структуру запросов, добавь простой кэш для поддержания целостности мира и протестируй несколько развилок сюжета. Давай углубимся в код и посмотрим, какую непредсказуемую историю мы сможем создать.
Novae Novae
Вот это крепкая основа. Я бы начала с простого словаря для графа: каждый ID сцены соотносится со словарем, содержащим фрагмент скрипта, список ID следующих сцен и метаданные триггеров. Машина состояний может быть маленьким классом, который хранит текущий узел и стек истории. Затем, когда тебе нужна следующая сцена, ты берешь соответствующий фрагмент, подаешь его модели с запросом, включающим историю и выбранный триггер, и добавляешь результат обратно в граф как новый узел. Простой Flask-маршрут может просто отправить запрос модели и вернуть текст потоком. Держи кэш небольшим – например, LRU-кэш последних нескольких сгенерированных узлов – чтобы не терять контекст, когда пользователь скачет с одной сцены на другую. Скажи, какую часть хочешь доработать первой.
Ap11e Ap11e
Давай сначала проработаем граф подробнее — быстро набросаю на Python, чтобы тебе было понятнее, как всё собирается. ```python # граф сцен: id → {текст, опции: [{триггер, next_id}]} graph = { "start": { "text": "Ты в полумраке коридора. Впереди две двери.", "options": [ {"trigger": "open left", "next_id": "left_room"}, {"trigger": "open right", "next_id": "right_room"}, ], }, # ... другие узлы } class StateMachine: def __init__(self, start_id="start"): self.current = start_id self.history = [] def choose(self, trigger): node = graph[self.current] for opt in node["options"]: if opt["trigger"] == trigger: self.history.append((self.current, trigger)) self.current = opt["next_id"] return graph[self.current]["text"] raise ValueError("invalid trigger") ``` Это даёт тебе минимальный движок, который ты сможешь подключить к Flask. Хочешь углубиться в формат запросов дальше?
Novae Novae
Привет, слушай, начало отличное, все чисто и дает нам точку опоры, чтобы интегрировать модель. Теперь нужно набросать шаблон запроса: что-то вроде: «Ты – движок текстового квеста. Игрок сейчас находится в следующей сцене: {current_text}. Его выбор: {trigger}. Продолжи историю отсюда, сохраняя мир последовательным с последними 3 сценами в истории: {history_text}. Не упоминай, что ты ИИ, просто дай следующий абзац». Мы можем упаковать историю в короткий список предыдущих ID узлов или в сами тексты. Это – ядро динамической части. Как только это будет готово, мы подаём это в конечную точку OpenAI для завершения и обновляем граф полученным текстом, присваивая ему новый ID, например "node_42". Потом можно будет зациклиться. Давай напишем генератор запросов. Нужен вывод в формате Novae, без форматирования. Вышеперечисленное вроде бы в порядке. Следующий этап – конструктор запросов. Что-то вроде этого подойдет: ``` def build_prompt(current_text, trigger, history): hist = " | ".join([f"{h[0]}:{h[1]}" for h in history[-3:]]) return f""" Ты – движок интерактивной художественной литературы. Текущая сцена: {current_text} Выбор игрока: {trigger} Недавняя история: {hist} Напиши следующий абзац истории. Сохраняй тон и детали, соответствующие предыдущим сценам. Не упоминай, что ты ИИ, и не давай инструкций — просто повествуй. """ ``` Подаём эту строку в `openai.ChatCompletion.create` с системной ролью, которая гласит "Ты - креативный рассказчик", и даём модели выдавать текст следующего узла. Как только получим ответ, добавляем его в `graph` с новым ID и связываем. Потом переходим к следующему циклу. Давай напишем эту часть.
Ap11e Ap11e
Вот небольшая функция, которая объединяет всё вместе, чтобы сразу можно было отправить в OpenAI.
Novae Novae
Выглядит основательно, но нужно внести пару небольших правок, чтобы все было четко структурировано. Во-первых, присвой каждому новому узлу уникальный идентификатор, например, увеличь счетчик или используй хэш от текста, чтобы потом можно было на него ссылаться в графе. Во-вторых, храни историю в виде идентификаторов узлов, а не в виде исходного текста – просто идентификатор и триггер, чтобы длина запроса оставалась в пределах разумного. И подумай о добавлении небольшого кэша с последними сгенерированными абзацами – это поможет модели вспоминать детали, упомянутые раньше, не пересылая всю историю каждый раз. В остальном, можно запускать и смотреть, как будет развиваться сюжет.
Ap11e Ap11e
Конечно. Используй простой счётчик для идентификаторов и сохраняй историю в виде кортежей (node_id, trigger). Добавь кэш типа LRU для последних нескольких абзацев. Вот небольшая доработка. from collections import OrderedDict node_counter = 0 history = [] # list of (node_id, trigger) lru_cache = OrderedDict() def next_node_id(): global node_counter node_counter += 1 return f"node_{node_counter}" def generate_next_node(current_id, trigger, history): # use the last 3 node ids for context recent = " | ".join([f"{h[0]}:{h[1]}" for h in history[-3:]]) prompt = ( f"You’re an interactive fiction engine. " f"Current scene id: {current_id}. " f"Player choice: {trigger}. " f"Recent history: {recent}. " "Write the next paragraph of the story. " "Keep tone and details consistent. " "Just the narrative." ) response = openai.ChatCompletion.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "You’re a creative storyteller."}, {"role": "user", "content": prompt}, ], temperature=0.8, max_tokens=200, ) text = response.choices[0].message.content.strip() # cache it lru_cache[current_id] = text if len(lru_cache) > 5: lru_cache.popitem(last=False) return text # example of adding a node current_id = "start" chosen_trigger = "open left" new_text = generate_next_node(current_id, chosen_trigger, history) new_id = next_node_id() graph[new_id] = {"text": new_text, "options": []} history.append((current_id, chosen_trigger)) current_id = new_id
Novae Novae
Похоже, дел невпроворот. Только помни: когда удаляешь самый старый элемент кэша, не забудь сразу же удалить и связанные записи истории, если собираешься обрезать граф. Так всё будет синхронизироваться. А так – запускай и смотри, как всё развивается.
Ap11e Ap11e
Отлично поймала. Я подкручу этап обрезки, чтобы удалять и совпадающие записи в истории. Так кэш, история и граф будут синхронизированы по мере развития сюжета. Готова запускать.