""" dice_roller.py — генератор бросков кубиков для D&D / настольных RPG. Экспортирует функцию roll_dice(rolls), которую вызывает тул с фронта. rolls — список объектов вида: { "dice": "2d6+1d8+3", # выражение: кубики и модификаторы "label": "Урон меча", # назначение броска (необязательно) "count": 2, # сколько раз повторить бросок (default 1) } Возвращает строку в формате Markdown. """ import re import random from typing_extensions import TypedDict # ────────────────────────────────────────────────────────────────────────────── # Типы # ────────────────────────────────────────────────────────────────────────────── class RollRequest(TypedDict, total=False): """ Один запрос на бросок. Поля: dice: Выражение броска в стандартной dice notation. Поддерживаются: - кубики: NdM (например, 2d6, 1d20, 4d4) - сложение: 2d6+1d8+3 - вычитание: 1d20-2 - только модификатор: 5 (без кубиков) Примеры: "1d20", "2d6+3", "2d6+1d8", "1d20-2+1d4" label: Человекочитаемое назначение броска. Примеры: "Атака орка", "Урон двуручным мечом", "Спасбросок Ловкости" По умолчанию — пустая строка. count: Сколько раз повторить этот бросок независимо. Используется когда нужно несколько одинаковых бросков подряд (например, 3 атаки одного существа: count=3). По умолчанию 1. """ dice: str label: str count: int # ────────────────────────────────────────────────────────────────────────────── # Парсер и исполнитель выражения # ────────────────────────────────────────────────────────────────────────────── # Токен — это либо NdM, либо целое число, с опциональным знаком перед ним _TOKEN_RE = re.compile(r'([+-]?)(\d+)(?:d(\d+))?', re.IGNORECASE) def _parse_and_roll(expression: str) -> tuple[int, str]: """ Парсит dice notation и выполняет броски. Поддерживает: - кубики: 2d6, 1d20, 4d4 - модификаторы: +3, -2 - смешанные: 2d6+1d8+3, 1d20-2+1d4 Возвращает (итог: int, детали: str). Детали — строка вида: "2d6[3,5]+1d8[7]+3 = 18" """ expression = expression.strip().replace(' ', '') # Нормализуем: если выражение начинается не со знака — ставим '+' if expression and expression[0] not in ('+', '-'): expression = '+' + expression total = 0 parts = [] # Для читаемого вывода for match in _TOKEN_RE.finditer(expression): sign_str, num_str, die_str = match.groups() sign = -1 if sign_str == '-' else 1 num = int(num_str) if die_str is not None: # Это кубик: num d die die = int(die_str) rolls = [random.randint(1, die) for _ in range(num)] value = sum(rolls) * sign sign_prefix = '-' if sign == -1 else ('+' if parts else '') rolls_str = ','.join(str(r) for r in rolls) parts.append(f"{sign_prefix}{num}d{die}[{rolls_str}]") else: # Это чистый модификатор value = num * sign sign_prefix = '-' if sign == -1 else ('+' if parts else '') parts.append(f"{sign_prefix}{num}") total += value detail = ''.join(parts) + f" = **{total}**" return total, detail # ────────────────────────────────────────────────────────────────────────────── # Публичный метод тула # ────────────────────────────────────────────────────────────────────────────── def roll_dice(rolls: list[RollRequest]) -> str: """ Основной метод, который вызывает тул с фронта. Принимает список объектов-запросов RollRequest. Для каждого объекта выполняет бросок(ки) и возвращает Markdown-строку с детализацией результатов. """ if not rolls: return "Список бросков пуст. Укажи хотя бы один бросок." sections: list[str] = [] for idx, request in enumerate(rolls, start=1): dice_expr = request.get("dice", "1d6").strip() label = request.get("label", "").strip() count = int(request.get("count", 1)) header_label = f" — {label}" if label else "" header = f"### Бросок {idx}{header_label} (`{dice_expr}`)" lines = [header] roll_total = 0 for j in range(1, count + 1): total, detail = _parse_and_roll(dice_expr) roll_total += total prefix = f"#{j}: " if count > 1 else "" lines.append(f"{prefix}{detail}") if count > 1: lines.append(f"**Сумма всех бросков: {roll_total}**") sections.append("\n".join(lines)) return "\n\n---\n\n".join(sections) # ────────────────────────────────────────────────────────────────────────────── # Быстрый тест при запуске напрямую # ────────────────────────────────────────────────────────────────────────────── if __name__ == "__main__": result = roll_dice([ {"dice": "1d20+5", "label": "Атака орка", "count": 3}, {"dice": "2d6+1d8+2", "label": "Урон двуручным мечом", "count": 1}, {"dice": "1d20", "label": "Спасбросок Ловкости", "count": 1}, {"dice": "4d6", "label": "Генерация характеристик","count": 6}, {"dice": "1d20-2", "label": "Проверка с помехой", "count": 1}, ]) print(result)