|  | @@ -0,0 +1,355 @@
 | 
	
		
			
				|  |  | +import re
 | 
	
		
			
				|  |  | +import random
 | 
	
		
			
				|  |  | +import struct
 | 
	
		
			
				|  |  | +import operator
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import aiohttp
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def _pyrandom(min_val, max_val, is_dice=False):
 | 
	
		
			
				|  |  | +    if is_dice:
 | 
	
		
			
				|  |  | +        return [random.randint(1, max_val) for _ in range(min_val)]
 | 
	
		
			
				|  |  | +    return random.randint(min_val, max_val)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def _get(url):
 | 
	
		
			
				|  |  | +    async with aiohttp.ClientSession() as session:
 | 
	
		
			
				|  |  | +        async with session.get(url) as resp:
 | 
	
		
			
				|  |  | +            return await resp.read()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def generate_unbiased_numbers(data, min_val, max_val, count):
 | 
	
		
			
				|  |  | +    mod = max_val - min_val + 1
 | 
	
		
			
				|  |  | +    max_acceptable = (1 << 64) // mod * mod
 | 
	
		
			
				|  |  | +    numbers = []
 | 
	
		
			
				|  |  | +    data_len = len(data)
 | 
	
		
			
				|  |  | +    bytes_needed = count * 8
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if data_len < bytes_needed:
 | 
	
		
			
				|  |  | +        return None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    for i in range(count):
 | 
	
		
			
				|  |  | +        chunk = data[i * 8 : (i + 1) * 8]
 | 
	
		
			
				|  |  | +        number = struct.unpack("<Q", chunk)[0]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        while number >= max_acceptable:
 | 
	
		
			
				|  |  | +            number = number >> 1
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        numbers.append((number % mod) + min_val)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return numbers
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def generate(source, min_val, max_val, count=1):
 | 
	
		
			
				|  |  | +    total_bytes = count * 8
 | 
	
		
			
				|  |  | +    try:
 | 
	
		
			
				|  |  | +        data = await _get(f"{source}{total_bytes}")
 | 
	
		
			
				|  |  | +    except Exception:
 | 
	
		
			
				|  |  | +        data = b""
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if len(data) >= total_bytes:
 | 
	
		
			
				|  |  | +        result = await generate_unbiased_numbers(data, min_val, max_val, count)
 | 
	
		
			
				|  |  | +        if result is not None:
 | 
	
		
			
				|  |  | +            return result
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if count == 1:
 | 
	
		
			
				|  |  | +        return [await _pyrandom(min_val, max_val)]
 | 
	
		
			
				|  |  | +    return await _pyrandom(min_val, max_val, is_dice=True)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def _trng_yebisu(min_val, max_val, is_dice=False):
 | 
	
		
			
				|  |  | +    count = min_val if is_dice else 1
 | 
	
		
			
				|  |  | +    actual_min = 1 if is_dice else min_val
 | 
	
		
			
				|  |  | +    actual_max = max_val if is_dice else max_val
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    numbers = await generate(
 | 
	
		
			
				|  |  | +        "https://yebi.su/api/pool?count=", actual_min, actual_max, count
 | 
	
		
			
				|  |  | +    )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return numbers if is_dice else numbers[0]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def rolldices(count, sides):
 | 
	
		
			
				|  |  | +    try:
 | 
	
		
			
				|  |  | +        return await _trng_yebisu(count, sides, is_dice=True)
 | 
	
		
			
				|  |  | +    except Exception:
 | 
	
		
			
				|  |  | +        return await _pyrandom(count, sides, is_dice=True)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def _roll(count, sides):
 | 
	
		
			
				|  |  | +    if count <= 0:
 | 
	
		
			
				|  |  | +        raise ValueError("Количество костей должно быть больше нуля.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if sides <= 0:
 | 
	
		
			
				|  |  | +        raise ValueError("Количество сторон должно быть больше нуля.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return await rolldices(count, sides)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +OPS = {
 | 
	
		
			
				|  |  | +    "+": operator.add,
 | 
	
		
			
				|  |  | +    "-": operator.sub,
 | 
	
		
			
				|  |  | +    "*": operator.mul,
 | 
	
		
			
				|  |  | +    "/": operator.truediv,
 | 
	
		
			
				|  |  | +    "%": operator.mod,
 | 
	
		
			
				|  |  | +    "^": operator.pow,
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +OPS_KEYS = "".join(OPS.keys()).replace("-", r"\-")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +T_NAME = re.compile(r"([abce-zа-ге-йл-яA-ZА-Я]+)")
 | 
	
		
			
				|  |  | +T_COLON = re.compile(r"(:)")
 | 
	
		
			
				|  |  | +T_DICE = re.compile(r"(d|д|к)")
 | 
	
		
			
				|  |  | +T_MINUS = re.compile(r"(-)")
 | 
	
		
			
				|  |  | +T_OP = re.compile(f"([{OPS_KEYS}])")
 | 
	
		
			
				|  |  | +T_DIGIT = re.compile(r"(\d+)")
 | 
	
		
			
				|  |  | +T_OPEN_PAREN = re.compile(r"(\()")
 | 
	
		
			
				|  |  | +T_CLOSE_PAREN = re.compile(r"(\))")
 | 
	
		
			
				|  |  | +T_WS = re.compile(r"([ \t\r\n]+)")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +TOKEN_NAMES = {
 | 
	
		
			
				|  |  | +    T_NAME: "имя",
 | 
	
		
			
				|  |  | +    T_COLON: "двоеточие",
 | 
	
		
			
				|  |  | +    T_DICE: "кость",
 | 
	
		
			
				|  |  | +    T_OP: "оператор",
 | 
	
		
			
				|  |  | +    T_DIGIT: "число",
 | 
	
		
			
				|  |  | +    T_OPEN_PAREN: "открывающая скобка",
 | 
	
		
			
				|  |  | +    T_CLOSE_PAREN: "закрывающая скобка",
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class Value:
 | 
	
		
			
				|  |  | +    def __init__(self, value):
 | 
	
		
			
				|  |  | +        if not isinstance(value, list):
 | 
	
		
			
				|  |  | +            value = [int(value)]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        self.value = value
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __int__(self):
 | 
	
		
			
				|  |  | +        return sum(map(int, self.value))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __repr__(self):
 | 
	
		
			
				|  |  | +        return str(self.value[0] if len(self.value) == 1 else self.value)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __iter__(self):
 | 
	
		
			
				|  |  | +        return iter(self.value)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __next__(self):
 | 
	
		
			
				|  |  | +        return next(self.value)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __index__(self, index):
 | 
	
		
			
				|  |  | +        return self.value[index]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __len__(self):
 | 
	
		
			
				|  |  | +        return len(self.value)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __eq__(self, other):
 | 
	
		
			
				|  |  | +        if not isinstance(other, Value):
 | 
	
		
			
				|  |  | +            return False
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return self.value == other.value
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def apply(self, what, *args):
 | 
	
		
			
				|  |  | +        args = list(map(int, args))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return Value(what(int(self), *args))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class Dices:
 | 
	
		
			
				|  |  | +    def __init__(self, text):
 | 
	
		
			
				|  |  | +        self.text = text.strip()
 | 
	
		
			
				|  |  | +        self.position = 0
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        self.names = {}
 | 
	
		
			
				|  |  | +        self.rolls = []
 | 
	
		
			
				|  |  | +        self.result = None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        self._rolls = []
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __repr__(self):
 | 
	
		
			
				|  |  | +        if self.result:
 | 
	
		
			
				|  |  | +            buffer = ""
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            for count, sides, roll in self._rolls:
 | 
	
		
			
				|  |  | +                buffer += f"{'' if count == 1 else count}d{sides}: {roll}\n"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            for roll, result in zip(self.rolls, self.result):
 | 
	
		
			
				|  |  | +                count, sides, roll = roll
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                buffer += f"{'' if count == 1 else count}d{sides}: "
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                roll_sum = int(roll)
 | 
	
		
			
				|  |  | +                result_sum = int(result)
 | 
	
		
			
				|  |  | +                if result_sum == roll_sum:
 | 
	
		
			
				|  |  | +                    results = ", ".join(map(str, result))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    if "," in results:
 | 
	
		
			
				|  |  | +                        results = f"[{results}]"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    buffer += results
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    if "," in results:
 | 
	
		
			
				|  |  | +                        buffer += f" ({result_sum})"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    buffer += "\n"
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    difference = result_sum - roll_sum
 | 
	
		
			
				|  |  | +                    results = f"{'' if len(roll) == 1 else f'{roll_sum} -> '}{roll_sum}{'' if difference < 0 else '+'}{difference}"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    buffer += f"{roll} -> {results} ({int(result)})\n"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return f"```\n{self.text}\n{buffer}= {int(self.result)}```"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return self.text
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _skip_ws(self):
 | 
	
		
			
				|  |  | +        match = T_WS.match(self.text, self.position)
 | 
	
		
			
				|  |  | +        if match:
 | 
	
		
			
				|  |  | +            self.position += len(match.group(0))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _done(self):
 | 
	
		
			
				|  |  | +        self._skip_ws()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return self.position >= len(self.text)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _match(self, what, skip_ws=True):
 | 
	
		
			
				|  |  | +        if skip_ws:
 | 
	
		
			
				|  |  | +            self._skip_ws()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        match = what.match(self.text, self.position)
 | 
	
		
			
				|  |  | +        if match:
 | 
	
		
			
				|  |  | +            self.position += len(match.group(0))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return match.groups()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _expected(self, expected):
 | 
	
		
			
				|  |  | +        raise SyntaxError(
 | 
	
		
			
				|  |  | +            f"Неожиданный ввод на позиции `#{self.position + 1}`: ожидалось: `{expected}`."
 | 
	
		
			
				|  |  | +        )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _expect(self, what):
 | 
	
		
			
				|  |  | +        match = self._match(what)
 | 
	
		
			
				|  |  | +        if not match:
 | 
	
		
			
				|  |  | +            self._expected(TOKEN_NAMES[what])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return match
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    async def _parse_dice(self, left=1):
 | 
	
		
			
				|  |  | +        if self._match(T_OPEN_PAREN):
 | 
	
		
			
				|  |  | +            right = await self._parse_expr()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            self._expect(T_CLOSE_PAREN)
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            right = await self._parse_atom()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        left = int(left)
 | 
	
		
			
				|  |  | +        right = int(right)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if left > 1000 or right > 1000:
 | 
	
		
			
				|  |  | +            raise SyntaxError("Слишком длинное число.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        roll = Value(await _roll(left, right))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        self._rolls.append((left, right, roll))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return roll
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    async def _parse_atom(self):
 | 
	
		
			
				|  |  | +        if self._match(T_OPEN_PAREN):
 | 
	
		
			
				|  |  | +            expr = await self._parse_expr()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            self._expect(T_CLOSE_PAREN)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if self._match(T_DICE, skip_ws=False):
 | 
	
		
			
				|  |  | +                return await self._parse_dice(expr)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return expr
 | 
	
		
			
				|  |  | +        elif self._match(T_MINUS):
 | 
	
		
			
				|  |  | +            value = await self._parse_atom()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return value.apply(operator.neg)
 | 
	
		
			
				|  |  | +        elif match := self._match(T_DIGIT):
 | 
	
		
			
				|  |  | +            try:
 | 
	
		
			
				|  |  | +                left = int(match[0])
 | 
	
		
			
				|  |  | +            except ValueError:
 | 
	
		
			
				|  |  | +                raise SyntaxError("Слишком длинное число.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if match := self._match(T_DICE, skip_ws=False):
 | 
	
		
			
				|  |  | +                return await self._parse_dice(left)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return Value(left)
 | 
	
		
			
				|  |  | +        elif self._match(T_DICE):
 | 
	
		
			
				|  |  | +            return await self._parse_dice()
 | 
	
		
			
				|  |  | +        elif match := self._match(T_NAME):
 | 
	
		
			
				|  |  | +            name = match[0].upper()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if name not in self.names:
 | 
	
		
			
				|  |  | +                raise NameError(f"Неизвестная переменная: `{match[0]}`.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            expr = self.names[name]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if self._match(T_DICE, skip_ws=False):
 | 
	
		
			
				|  |  | +                return await self._parse_dice(expr)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            return expr
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        self._expected("число, кость или переменная")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    async def _parse_expr(self):
 | 
	
		
			
				|  |  | +        left = await self._parse_atom()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if op := self._match(T_OP):
 | 
	
		
			
				|  |  | +            op = OPS[op[0]]
 | 
	
		
			
				|  |  | +            right = await self._parse_expr()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            left = left.apply(op, right)
 | 
	
		
			
				|  |  | +        elif self._match(T_COLON):
 | 
	
		
			
				|  |  | +            right = self._expect(T_NAME)[0].upper()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            self.names[right] = left
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return left
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    async def _parse_exprs(self):
 | 
	
		
			
				|  |  | +        exprs = []
 | 
	
		
			
				|  |  | +        while not self._done():
 | 
	
		
			
				|  |  | +            if len(exprs) >= 10:
 | 
	
		
			
				|  |  | +                raise SyntaxError("Слишком длинное число.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            rolls_count = len(self._rolls)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            expr = await self._parse_expr()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if len(self._rolls) == rolls_count:
 | 
	
		
			
				|  |  | +                raise SyntaxError("Выражение не содержит бросков.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            self.rolls.append(self._rolls.pop(-1))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            exprs.append(expr)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if not exprs:
 | 
	
		
			
				|  |  | +            raise SyntaxError("Выражение не должно быть пустым.")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return Value(exprs)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    async def roll(self, vars={}):
 | 
	
		
			
				|  |  | +        self.names = {str(k).upper(): Value(vars[k]) for k in vars}
 | 
	
		
			
				|  |  | +        self.position = 0
 | 
	
		
			
				|  |  | +        self.rolls = []
 | 
	
		
			
				|  |  | +        self._rolls = []
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        self.result = await self._parse_exprs()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return self
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +async def roll_dices(dices, vars={}):
 | 
	
		
			
				|  |  | +    dices = Dices(dices)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try:
 | 
	
		
			
				|  |  | +        await dices.roll(vars=vars)
 | 
	
		
			
				|  |  | +    except (ValueError, SyntaxError, NameError) as e:
 | 
	
		
			
				|  |  | +        return str(e)
 | 
	
		
			
				|  |  | +    except ZeroDivisionError:
 | 
	
		
			
				|  |  | +        raise "Попытка деления на ноль."
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return str(dices)
 |