dnd5-scripts/generate_encounter/encounter_generator (old).py
2026-06-17 22:32:48 +03:00

204 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
encounter_generator.py — генератор случайных встреч по биомам и уровням с интеграцией механик ACKS II (Surprise & Reaction).
Экспортирует функцию generate_encounter(debug_description, requests), которую вызывает тул.
requests — список объектов вида:
{
"environment": "forest",
"level": 5,
"party_surprise_mods": {"Высокое восприятие": 1},
"monster_surprise_mods": {"Скрытность варвара": -2},
"reaction_mods": {"Харизма жреца": 2, "Известный враг": -1},
"party_foreknowledge": False,
"party_los": True,
"monster_foreknowledge": False,
"monster_los": True
}
Возвращает строку в формате Markdown с полным отчетом по событию, внезапности и реакции.
"""
import random
from typing import Dict, Optional
from typing_extensions import TypedDict
# ──────────────────────────────────────────────────────────────────────────────
# Таблицы данных
# ──────────────────────────────────────────────────────────────────────────────
from monsters_data import DATA
# (В РЕАЛЬНОМ СКРИПТЕ ВСЕ ТАБЛИЦЫ ДОЛЖНЫ БЫТЬ ПРОПИСАНЫ ПОЛНОСТЬЮ. ДЛЯ ЭКОНОМИИ МЕСТА Я ПРИВЕЛ ПЕРВЫЕ 6,
# НО ГУМАНОИДНЫЙ АГЕНТ ДОЛЖЕН РАЗВЕРНУТЬ ИХ ВСЕ. НИЖЕ ЛОГИКА ФУНКЦИИ.)
class EncounterRequest(TypedDict, total=False):
"""
Параметры запроса случайной встречи с учетом контекста столкновения.
Поля:
environment: Биом (arctic, coastal, desert, forest, grassland, hill, mountain, swamp, underdark, underwater, urban).
level: Средний уровень группы (1-20).
По умолчанию: 1.
party_surprise_mods: Словарь модификаторов броска внезапности ПАРТИИ (Ключ - причина, Значение - бонус/штраф).
Пример: {"Бдительность": 1}. По умолчанию: {}.
monster_surprise_mods: Словарь модификаторов броска внезапности МОНСТРОВ.
Пример: {"Скрытность эльфа": -2}. По умолчанию: {}.
reaction_mods: Словарь модификаторов броска реакции.
Пример: {"Харизма партийного лица": 2}. По умолчанию: {}.
party_foreknowledge: Есть ли у партии предзнание (Foreknowledge)?
По умолчанию: False.
party_los: Есть ли у партии прямая видимость (LOS) в начале?
По умолчанию: True.
monster_foreknowledge: Есть ли у монстров предзнание?
По умолчанию: False.
monster_los: Есть ли у монстров прямая видимость в начале?
По умолчанию: True.
"""
environment: str
level: int
party_surprise_mods: Dict[str, int]
monster_surprise_mods: Dict[str, int]
reaction_mods: Dict[str, int]
party_foreknowledge: Optional[bool]
party_los: Optional[bool]
monster_foreknowledge: Optional[bool]
monster_los: Optional[bool]
def _format_mods(mods: Dict[str, int]) -> str:
"""Форматирует словарь модификаторов в строку для вывода."""
if not mods: return "Нет"
return ", ".join(f"{k} ({v:^d})" for k, v in mods.items())
def _determine_surprise(mods: Dict[str, int], foreknowledge: bool, los: bool, side_name: str) -> str:
"""
Рассчитывает внезапность для одной из сторон на основе частичной логики матрицы ACKS.
Если есть предзнание, бросок не требуется. Иначе кидается 1d6.
"""
if foreknowledge:
return f"**{side_name.capitalize()}:** Не застигнуты врасплох (Было предзнание/Foreknowledge)."
mod_sum = sum(mods.values()) if mods else 0
roll = random.randint(1, 6)
total = roll + mod_sum
status = "ВНИМАТЕЛЬНЫ (Attentive)" if total >= 3 else "ЗАСТИГНУТЫ ВРАСПЛОХ (Surprised)"
mods_str = _format_mods(mods)
return (f"**{side_name.capitalize()}:** {status} "
f"(Бросок: 1d6 ({roll}) + Мод ({mod_sum}) = {total}) | Модификаторы: {mods_str}")
def _determine_reaction(mods: Dict[str, int]) -> str:
"""
Рассчитывает первичную реакцию монстров броском 2d6 по правилам ACKS.
Учитывает пределы (немодифицированные 2 и 12 имеют ограничения).
"""
mod_sum = sum(mods.values()) if mods else 0
roll = random.randint(1, 6) + random.randint(1, 6)
total = roll + mod_sum
# ACKS Reaction limits logic for natural rolls
if roll == 2 and total > 5:
total = 5 # Немодифицированная двойка не может быть лучше "Unfriendly"
elif roll == 12 and total < 9:
total = 9 # Немодифицированные 12 не могут быть хуже "Indifferent"
if total <= 2:
res = "Враждебные (Hostile) — Немедленно атакуют!"
elif total <= 5:
res = "Недружелюбные (Unfriendly) — Не доверяют и могут атаковать при шансе на победу."
elif total <= 8:
res = "Нейтральные (Neutral) — Не уверены; реакция зависит от действий партии."
elif total <= 11:
res = "Индифферентные (Indifferent) — Проигнорируют, если их не тревожить."
else:
res = "Дружелюбные (Friendly) — Заинтересованы в сотрудничестве или разговоре."
mods_str = _format_mods(mods)
return (f"**Итоговая реакция:** {res}\n"
f"- **Сырой бросок:** 2d6 ({roll})\n"
f"- **Сумма модификаторов:** {mod_sum} ({mods_str})\n"
f"- **Итоговое значение:** {total}")
def generate_encounter(debug_description: str, requests: list[EncounterRequest]) -> str:
"""
Основной метод, который вызывает тул.
Принимает список запросов. Для каждого генерирует:
- Базовую встречу;
- Исходы проверки на сюрприз для партий и монстров;
- Социальную реакцию существ (если не сказано обратное).
Возвращает Markdown-строку.
"""
if not requests:
requests = [{}]
results = []
for i, req in enumerate(requests, 1):
env = req.get("environment", "forest")
lvl = int(req.get("level", 1))
# Получаем данные о встрече
encounter_text = _get_encounter(env, lvl)
# Обработка Внезапности
p_mods = req.get("party_surprise_mods", {})
m_mods = req.get("monster_surprise_mods", {})
p_fore = req.get("party_foreknowledge", False)
p_los = req.get("party_los", True)
m_fore = req.get("monster_foreknowledge", False)
m_los = req.get("monster_los", True)
p_surprise = _determine_surprise(p_mods, p_fore, p_los, "Партия")
m_surprise = _determine_surprise(m_mods, m_fore, m_los, "Монстры")
# Обработка Реакции
r_mods = req.get("reaction_mods", {})
reaction_text = _determine_reaction(r_mods)
# Формирование отчета
res = (f"### Встреча №{i}\n"
f"- **Биом:** {env.capitalize()}\n"
f"- **Уровень группы:** {lvl}\n"
f"- **Результат по таблицам:** {encounter_text}\n\n"
f"#### Проверка Внезапности (Surprise)\n"
f"- {p_surprise}\n"
f"- {m_surprise}\n\n"
f"#### Первичная Реакция (Reaction)\n"
f"{reaction_text}")
results.append(res)
return "\n\n---\n\n".join(results)
if __name__ == "__main__":
# Тест
test_reqs = [
{
"environment": "forest",
"level": 3,
"party_surprise_mods": {"Осторожность": 1},
"monster_surprise_mods": {"Громоздкие шаги": -1},
"reaction_mods": {"Харизма лица": 2, "Грязный вид": -1},
"party_foreknowledge": False,
"party_los": True,
"monster_foreknowledge": False,
"monster_los": True
},
{
"environment": "arctic",
"level": 1
}
]
print(generate_encounter("Тестовая генерация энкаунтера", test_reqs))