From 8b7a415ef086f0ad10ab23f9bd417e675ce464cb Mon Sep 17 00:00:00 2001 From: Denis-Cosmin NUTIU Date: Sun, 21 Jan 2024 16:55:19 +0200 Subject: [PATCH] Implement the dice roller --- src/dice/__init__.py | 0 src/dice/dice.py | 53 +++++++++++++++++++++++++++++++++++++++++++ src/dice/parser.py | 36 +++++++++++++++++++++++++++++ src/dice/semantics.py | 33 +++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 src/dice/__init__.py create mode 100644 src/dice/dice.py create mode 100644 src/dice/parser.py create mode 100644 src/dice/semantics.py diff --git a/src/dice/__init__.py b/src/dice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dice/dice.py b/src/dice/dice.py new file mode 100644 index 0000000..fff4c8c --- /dev/null +++ b/src/dice/dice.py @@ -0,0 +1,53 @@ +import typing + +from src.dice.parser import DieParser + + +class DiceRoller: + """ + DiceRoller is a simple class that allows you to roll dices. + + A die can be rolled using the following expression: + - 1d20 will roll a 20-faceted die and output the result a random number between 1 and 20. + - 1d100 will roll a 100 faceted die. + - 2d20 will roll a two d20 dies and multiply the result by two. + - 2d20+5 will roll a two d20 dies and multiply the result by two and ads 5. + """ + _parser = DieParser() + + @staticmethod + def roll(expression: str, advantage: typing.Optional[bool]) -> int: + """ + Roll die and return the result. + :param expression: The die expression. + :param advantage: Optionally, rolls a die with advantage or disadvantage. + :return: The die result. + """ + if advantage is None: + return DiceRoller._parser.parse(expression) + elif advantage is True: + return DiceRoller.roll_with_advantage(expression) + elif advantage is False: + return DiceRoller.roll_with_disadvantage(expression) + + @staticmethod + def roll_with_advantage(expression: str) -> int: + """ + Roll two dies and return the highest result. + :param expression: The die expression. + :return: The die result. + """ + one = DiceRoller._parser.parse(expression) + two = DiceRoller._parser.parse(expression) + return max(one, two) + + @staticmethod + def roll_with_disadvantage(expression: str) -> int: + """ + Roll two dies and return the lowest result. + :param expression: The die expression. + :return: The die result. + """ + one = DiceRoller._parser.parse(expression) + two = DiceRoller._parser.parse(expression) + return min(one, two) diff --git a/src/dice/parser.py b/src/dice/parser.py new file mode 100644 index 0000000..4892b6f --- /dev/null +++ b/src/dice/parser.py @@ -0,0 +1,36 @@ +import tatsu + +from src.dice.semantics import DieSemantics + +DIE_GRAMMAR = ''' + @@grammar::Die + @@whitespace :: None + + start = [number_of_dies:number] die:die [modifier:die_modifier] $; + die = die_type:die_type die_number:number; + die_modifier = op:die_modifier_op modifier:number; + die_modifier_op = '+' | '-'; + die_type = 'd' | 'zd'; + number = /[0-9]+/ ; +''' + + +class DieParser: + """ + Parser for the die grammar defined above. + """ + + def __init__(self): + self._parser = tatsu.compile(DIE_GRAMMAR) + self._semantics = DieSemantics() + + def parse(self, expression: str) -> int: + """ + Parses the die expression and returns the result. + """ + clean_expression = "".join(expression.split()) + result = self._parser.parse(clean_expression, semantics=self._semantics) + number_of_dies = result.get("number_of_dies") or 1 + modifier = result.get("modifier") or 0 + die = result.get("die") + return number_of_dies * die + modifier diff --git a/src/dice/semantics.py b/src/dice/semantics.py new file mode 100644 index 0000000..980fa55 --- /dev/null +++ b/src/dice/semantics.py @@ -0,0 +1,33 @@ +import random +from tatsu.ast import AST + + +# noinspection PyMethodMayBeStatic +class DieSemantics: + def number(self, ast): + return int(ast) + + def die(self, ast): + if not isinstance(ast, AST): + return ast + die_type = ast.get("die_type") + die_number = ast.get("die_number", 1) + if die_number <= 0: + raise ValueError(f"Invalid die number: {die_number}") + # normal die + if die_type == "d": + return random.randint(1, die_number) + # zero-based die can output 0. + if die_type == "zd": + return random.randint(0, die_number) + raise ValueError(f"Invalid die type: {die_type}") + + def die_modifier(self, ast): + if not isinstance(ast, AST): + return ast + op = ast.get("op") + modifier = ast.get("modifier", 0) + if op == "+": + return modifier + else: + return -modifier