""" 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))