Implement the dice roller
This commit is contained in:
parent
2cc00bdf4f
commit
8b7a415ef0
4 changed files with 122 additions and 0 deletions
0
src/dice/__init__.py
Normal file
0
src/dice/__init__.py
Normal file
53
src/dice/dice.py
Normal file
53
src/dice/dice.py
Normal file
|
@ -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)
|
36
src/dice/parser.py
Normal file
36
src/dice/parser.py
Normal file
|
@ -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
|
33
src/dice/semantics.py
Normal file
33
src/dice/semantics.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue