Нейросети проходят визуальную новеллу «Бесконечное Лето»

Современные ИИ-агенты на базе LLM уже делегируют задачи по планированию графиков, управлению капиталом и даже разработке OpenSource-решений. Некоторые из них зашли настолько далеко, что ведут блоги, рассуждая о правах нейросетей. Однако способен ли искусственный интеллект полноценно погрузиться в мир визуальных новелл?

Идея родилась спонтанно во время подготовки руководства по локальному запуску DeepSeek. По сути, любая визуальная новелла — это массивный текстовый массив, а работа с текстом — родная стихия больших языковых моделей.

Я протестировал несколько архитектур, освоил API Ollama, внедрил патчи в движок Ren’Py и полностью автоматизировал игровой процесс. Ниже представлен технический разбор эксперимента, а наиболее интригующие детали прохождения вынесены в спойлеры.

Дисклеймер

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

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

Ссылка на репозиторий с наработками доступна в финале статьи.

Выбор игры

Как понятно из заголовка, в качестве подопытного выступило «Бесконечное лето». Почему именно оно? Из популярных представителей жанра на ум приходили только эта игра и «Doki Doki Literature Club».

«Бесконечное лето».
Атмосфера «Бесконечного лета».

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

«Doki Doki Literature Club». Источник.
Визуальный стиль Doki Doki Literature Club. Источник.

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

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

Описание из Steam

Наличие 13 вариативных финалов и сложной системы взаимоотношений делает эту игру идеальным полигоном для LLM. Сможет ли модель проявить эмпатию к конкретному персонажу? Или же строгие алгоритмы безопасности заставят ИИ выбирать максимально нейтральные варианты?

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

Интеграция с игровым движком

Интерфейс игры для ПК.
Стандартный интерфейс игры.

«Бесконечное лето» функционирует на движке Ren’Py, который базируется на Python и предоставляет удобные инструменты для работы с текстом и графикой.

Сценарные файлы Ren’Py (.rpy) имеют понятную структуру:

init:
    $ action = 0
label prologue:
    dreamgirl "Ты пойдешь со мной?"
    "Выбора нет, нужно что-то ответить."
    "Иначе этот цикл никогда не прервется."
    window hide
    $ renpy.pause(1)
    menu:
        "Да, я последую за тобой":
            $ action = 1
        "Нет, я останусь здесь":
            $ action = 2
    $ renpy.pause(1)
    window show
    "Каждый раз принятие решения дается с трудом."

Ren’Py позволяет бесшовно внедрять Python-код. Я воспользовался этой особенностью, создав файл bridge.rpy для организации внешнего управления.

Пример реализации серверной части внутри игры:

# Инициализация на чистом Python
init -100 python:
import socket
import threading
import json
import time
class CLIBridge(object):
    def __init__(self, host="127.0.0.1", port=8765):
        ...

    def start(self):
        if self.running:
            return

        self.running = True
        self.thread = threading.Thread(target=self._server_loop)
        self.thread.daemon = True
        self.thread.start()

    ...

cli_bridge = CLIBridge()
cli_bridge.start()</code><div class="code-explainer"><a href="https://sourcecraft.dev/" class="tm-button code-explainer__link" style="visibility: hidden;"><img style="width:14px;height:14px;object-fit:cover;object-position:left;"></a></div></pre><p>Использование <code>init python</code> с высоким приоритетом (-100) позволяет запустить TCP-сервер в фоновом потоке еще до загрузки основного сценария. </p><p>Чтобы ИИ «видел» происходящее, необходимо перехватить вывод текста. В Ren’Py за это отвечает функция <em>say</em>. Я переопределил её, чтобы транслировать каждую реплику во внешний сокет:</p><pre><code class="python">init -90 python:
_orig_say = renpy.exports.say

def _cli_say(who, what, *args, **kwargs):
    who_text = ""
    what_text = _strip_simple_text_tags(what)

    try:
        if hasattr(who, "name"):
            who_text = _strip_simple_text_tags(who.name)
        elif who is None:
            who_text = ""
        else:
            who_text = _strip_simple_text_tags(who)
    except Exception:
        who_text = ""

    cli_bridge.notify_say(who_text, what_text)
    return _orig_say(who, what, *args, **kwargs)

renpy.exports.say = _cli_say</code><div class="code-explainer"><a href="https://sourcecraft.dev/" class="tm-button code-explainer__link" style="visibility: hidden;"><img style="width:14px;height:14px;object-fit:cover;object-position:left;"></a></div></pre><p>Аналогичным образом была модифицирована функция <code>renpy.display_menu</code>, отвечающая за выбор вариантов ответа. В этом случае оригинальная функция полностью замещается ожиданием команды от сетевого моста.</p><figure class="full-width "><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/d87/f44/551/d87f445519feed1fc85f1a039cde3723.jpeg" alt="Карта «Совёнка»." title="Карта «Совёнка»." width="1600" height="900" sizes="(max-width: 780px) 100vw, 50vw" srcset="https://habrastorage.org/r/w780/getpro/habr/upload_files/d87/f44/551/d87f445519feed1fc85f1a039cde3723.jpeg 780w,
   https://habrastorage.org/r/w1560/getpro/habr/upload_files/d87/f44/551/d87f445519feed1fc85f1a039cde3723.jpeg 781w" loading="lazy" decode="async"><div><figcaption><em>Локации лагеря.</em></figcaption></div></figure><p>Также пришлось переопределить логику работы с картой и автоматизировать пропуск карточной игры, чтобы не усложнять задачу агенту обучением специфическим мини-играм.</p><blockquote><p>Следите за анонсами новых технических экспериментов <a href="https://t.me/+VzpLr5pam-MxODEy">в моем Telegram‑канале</a>. Там я делюсь деталями разработки, полезными находками и профессиональным юмором.</p></blockquote><div class="floating-image"><figure class="float "><img src="https://habrastorage.org/getpro/habr/upload_files/a8c/500/040/a8c5000406a5893faca2581189cdb70a.gif" width="256" height="256" sizes="(max-width: 780px) 100vw, 50vw" srcset="https://habrastorage.org/getpro/habr/upload_files/a8c/500/040/a8c5000406a5893faca2581189cdb70a.gif 780w,
   https://habrastorage.org/getpro/habr/upload_files/a8c/500/040/a8c5000406a5893faca2581189cdb70a.gif 781w" loading="lazy" decode="async"></figure><p><strong>Нужна мощь GPU для ваших проектов?</strong></p><p>В Selectel доступны конфигурации с актуальными видеокартами для любых задач машинного обучения. <abbr class="habraabbr" title="При заказе сервера произвольной конфигурации в Selectel в рамках Акции при выборе GPU Tesla T4 либо GPU Nvidia A2. Количество доступных для заказа в рамках Акции GPU-карт ограничено." data-title="<p>При заказе сервера произвольной конфигурации в Selectel в рамках Акции при выборе GPU Tesla T4 либо GPU Nvidia A2. Количество доступных для заказа в рамках Акции GPU-карт ограничено.</p><p></p>" data-abbr="*">*</abbr></p><p><a href="https://selectel.ru/services/dedicated/?tab=configuratorGpu&c=385%3A1&simpleRamMode=true&utm_source=habr.com&utm_medium=referral&utm_campaign=dedicated_article_widget_270326_endlessllm_i089_02_ord"><strong>Выбрать сервер →</strong></a></p></div><h2>Инфраструктура и модели</h2><p>Для обеспечения приватности и контроля использовались исключительно локальные модели. Базой послужила DeepSeek-R1:70b, которая, к счастью, не была знакома с сюжетом игры, что гарантировало чистоту эксперимента.</p><p>Для развертывания я использовал стек <a href="https://github.com/av/harbor">av/harbor</a> на облачной машине: 12 vCPU, 128 ГБ RAM и флагманская H100.</p><pre><code class="bash">curl https://raw.githubusercontent.com/av/harbor/refs/heads/main/install.sh | bash

harbor ollama pull deepseek-R1:70b
harbor up ollama

Взаимодействие с нейросетью происходило через стандартный API Ollama (метод /api/chat).

Координатор системы

Схема взаимодействия компонентов.
Архитектура потоков данных.

Координатор выступает в роли «мозга», который собирает реплики из игры и формирует запросы к LLM. Для структурирования диалога использовались стандартные роли:

  • system — внутренние монологи Семена и базовые инструкции;

  • tool — реплики других персонажей;

  • user — описание ситуации и доступные варианты действий;

  • assistant — решение ИИ.

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

Трудности и оптимизация

Первые тесты на DeepSeek-R1:70b выявили классическую проблему: модели склонны к избыточному словоблудию вместо четкого ответа цифрой.

Пример «сбоя» интерпретации:

Вместо «2» модель могла выдать: «Я считаю, что вариант 2 наиболее логичен, так как Семен выглядит уставшим…».

Для решения этой проблемы был введен дополнительный этап верификации: отдельный легковесный запрос просил ИИ извлечь номер варианта из предыдущего развернутого ответа.

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

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

Итоги эксперимента

В финальном тестировании участвовали: DeepSeek-R1:70b, Qwen3.5 (9b/3b), GPT-OSS:20b и Gemma3:27b.

Сюжетные спойлеры: о механиках и финалах

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

Результаты оказались неоднозначными:

  • DeepSeek-R1:70b: Модель великолепно анализировала текст, но «сломалась» на циклическом выборе в лабиринте, пытаясь найти несуществующий выход.

  • GPT-OSS:20b: Уверенно пришла к «одиночной» плохой концовке.

  • Qwen3.5:9b: Столкнулась с критическим замедлением при принятии сложных решений (думала более 20 минут над одним выбором).

  • Qwen2.5:3b: Неожиданно показала самый стабильный результат, выведя героя на плохую концовку Лены.

  • Gemma3:27b: Дважды заблудилась в сюжетных локациях, в итоге завершив игру на плохой концовке Алисы.

Пояснение по лабиринту (спойлер)
Схема лабиринта.
Геометрия лабиринта.

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

Заключение

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

Полный исходный код проекта доступен в этом репозитории, а логи размышлений моделей можно изучить по ссылке.

 

Источник

Читайте также