-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from Vlek/pyparsing
Although the github tests do not work, it's because we are no longer using setup files and instead use poetry. I will have to include generated ones from here on out.
- Loading branch information
Showing
11 changed files
with
827 additions
and
193 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
[tool.poetry] | ||
name = "pyroll" | ||
version = "1.0.1" | ||
description = "Dice roller with all of the features you could want." | ||
authors = ["Derek 'Vlek' McCammond <[email protected]>"] | ||
license = "gpl-3.0" | ||
readme = "README.md" | ||
homepage = "https://github.com/vlek/roll" | ||
repository = "https://github.com/vlek/roll" | ||
keywords = [ | ||
"dice", "die", "roll", "rolling", "game", | ||
"gaming", "rp", "rpg", "parse", "parser", "parsing", | ||
"cli", "terminal" | ||
] | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.6" | ||
typing_extensions = "^3.7.4" | ||
toml = "^0.10.1" | ||
pyparsing = "^2.4.7" | ||
click = "^7.1.2" | ||
parsley = "^1.3" | ||
|
||
[tool.poetry.dev-dependencies] | ||
pytest = "^5.4.2" | ||
pytest-cov = "^2.9.0" | ||
coverage = "^5.3" | ||
codecov = "^2.1.10" | ||
|
||
[build-system] | ||
requires = ["poetry>=0.12"] | ||
build-backend = "poetry.masonry.api" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
#!/usr/bin/env python3 | ||
|
||
""" | ||
Dice roller CLI Script. | ||
Makes it easy to roll dice via command line and is able handle the basic | ||
math functions, including parens! | ||
1d20 -> 19 | ||
1d8 + 3d6 + 5 -> 15 | ||
d% -> 42 | ||
<Nothing> -> 14 (Rolls a d20) | ||
etc. | ||
""" | ||
|
||
from typing import List | ||
|
||
import click | ||
from roll import roll | ||
|
||
|
||
@click.command() | ||
@click.argument('expression', nargs=-1, type=str) | ||
@click.option('-v', '--verbose', 'verbose', is_flag=True, | ||
help='Print the individual die roll values') | ||
def roll_cli(expression: List[str] = None, verbose: bool = False) -> None: | ||
""" | ||
CLI dice roller. | ||
Usage: roll [EXPRESSION] | ||
A cli command for rolling dice and adding modifiers in the | ||
same fashion as the node.js version on npm. | ||
Examples: | ||
roll - Rolls 1d20 | ||
roll <expression> - Rolls all dice + does math | ||
Expressions: | ||
1d20 - Rolls one 20-sided die | ||
d20 - Does not require a '1' in front of 'd' | ||
d% - Rolls 1d100 | ||
d8 + 3d6 + 5 - Rolls 1d8, 3d6, and adds everything together | ||
(1d4)d6 - Rolls 1d4 d6 die | ||
""" | ||
command_input = ' '.join(expression) if expression is not None else '' | ||
|
||
click.echo(roll(command_input, verbose)) | ||
|
||
|
||
if __name__ == '__main__': | ||
roll_cli() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
#!/user/bin/env python3 | ||
|
||
""" | ||
Dice rolling PyParser grammar. | ||
Grammar: | ||
Digit ::= [1234567890] | ||
Number ::= ( '-' )? Digit Digit* ( '.' Digit Digit*)? | ||
Add ::= Number '+' Number | ||
Sub ::= Number '-' Number | ||
AddOrSub ::= Add | Sub | ||
Mult ::= Number '*' Number | ||
Div ::= Number '/' Number | ||
Mod ::= Number '%' Number | ||
IntDiv ::= Number '//' Number | ||
MultOrDiv ::= Mult | Div | Mod | IntDiv | ||
Exponent ::= Number '**' Number | ||
PercentDie ::= Number? 'd%' | ||
Die ::= Number? 'd' Number | ||
Dice ::= Die | PercentDie | ||
Parens ::= Number? '(' Expression ')' | ||
Expression ::= (Parens | Exponent | Dice | MultOrDiv | AddOrSub | Number)+ | ||
Main ::= Expression | ||
Website used to do railroad diagrams: https://www.bottlecaps.de/rr/ui | ||
""" | ||
|
||
from math import ceil, e, factorial, pi | ||
from operator import add, floordiv, mod, mul, sub, truediv | ||
from random import randint | ||
from typing import List, Union | ||
|
||
from pyparsing import (CaselessKeyword, CaselessLiteral, Forward, Literal, | ||
Optional, ParserElement, ParseResults, oneOf, opAssoc, | ||
operatorPrecedence, pyparsing_common) | ||
|
||
ParserElement.enablePackrat() | ||
|
||
|
||
def _roll_dice( | ||
num_dice: Union[int, float], | ||
sides: Union[int, float], | ||
debug_print: bool = False) -> Union[int, float]: | ||
"""Calculate value of dice roll notation.""" | ||
starting_num_dice = num_dice | ||
starting_sides = sides | ||
|
||
# If it's the case that we were given a dice with negative sides, | ||
# then that doesn't mean anything in the real world. I cannot | ||
# for the life of me figure out a possible scenario where that | ||
# would make sense. We will just error out. | ||
if sides < 0: | ||
raise ValueError('The sides of a die must be positive or zero.') | ||
|
||
if isinstance(num_dice, float): | ||
sides *= num_dice | ||
|
||
# 0.5d20 == 1d10, so, after we've changed the value, | ||
# we need to set the left value to 1. | ||
num_dice = 1 | ||
|
||
result_is_negative = num_dice < 0 | ||
|
||
if result_is_negative: | ||
num_dice = abs(num_dice) | ||
|
||
sides = ceil(sides) | ||
|
||
rolls = [ | ||
randint(1, sides) for _ in range(num_dice) | ||
] if sides != 0 else [] | ||
|
||
rolls_total = sum(rolls) | ||
|
||
if result_is_negative: | ||
rolls_total *= -1 | ||
|
||
if debug_print: | ||
debug_message = [ | ||
f'{starting_num_dice}d{starting_sides}:', | ||
f'{rolls_total}', | ||
(f'{rolls}' if len(rolls) > 1 else '') | ||
] | ||
|
||
return rolls_total | ||
|
||
|
||
class DiceParser: | ||
"""Parser for evaluating dice strings.""" | ||
|
||
operations = { | ||
"+": add, | ||
"-": sub, | ||
"*": mul, | ||
"/": truediv, | ||
"//": floordiv, | ||
"%": mod, | ||
"^": pow, | ||
"**": pow, | ||
"d": _roll_dice, | ||
"!": factorial | ||
} | ||
|
||
constants = { | ||
"pi": pi, | ||
"e": e | ||
} | ||
|
||
def __init__(self: "DiceParser") -> None: | ||
"""Initialize a parser to handle dice strings.""" | ||
self._parser = self._create_parser() | ||
|
||
@staticmethod | ||
def _create_parser() -> Forward: | ||
"""Create an instance of a dice roll string parser.""" | ||
atom = ( | ||
CaselessLiteral("d%") | | ||
pyparsing_common.number | | ||
CaselessKeyword("pi") | | ||
CaselessKeyword("e") | ||
) | ||
|
||
expression = operatorPrecedence(atom, [ | ||
(oneOf('^ **'), 2, opAssoc.RIGHT), | ||
|
||
(Literal('!'), 1, opAssoc.LEFT), | ||
|
||
# (Literal('-'), 1, opAssoc.RIGHT), | ||
|
||
(CaselessLiteral('d%'), 1, opAssoc.LEFT), | ||
(CaselessLiteral('d'), 2, opAssoc.RIGHT), | ||
|
||
# This line causes the recursion debug to go off. | ||
# Will have to find a way to have an optional left | ||
# operator in this case. | ||
(CaselessLiteral('d'), 1, opAssoc.RIGHT), | ||
|
||
(oneOf('* / % //'), 2, opAssoc.LEFT), | ||
|
||
(oneOf('+ -'), 2, opAssoc.LEFT), | ||
]) | ||
|
||
return expression | ||
|
||
def parse(self: "DiceParser", dice_string: str) -> List[Union[str, int]]: | ||
"""Parse well-formed dice roll strings.""" | ||
return self._parser.parseString(dice_string, parseAll=True) | ||
|
||
def evaluate( | ||
self: "DiceParser", | ||
parsed_values: Union[List[Union[str, int]], str] | ||
) -> Union[int, float]: | ||
"""Evaluate the output parsed values from roll strings.""" | ||
if isinstance(parsed_values, str): | ||
parsed_values = self.parse(parsed_values) | ||
|
||
result = None | ||
operator = None | ||
|
||
for val in parsed_values: | ||
if ( | ||
isinstance(val, (int, float, ParseResults)) or | ||
val in self.constants | ||
): | ||
if val in self.constants: | ||
val = self.constants[val] | ||
elif isinstance(val, ParseResults): | ||
val = self.evaluate(val) | ||
|
||
if operator is not None: | ||
result = operator(result if result is not None else 1, val) | ||
else: | ||
if result is None: | ||
result = val | ||
else: | ||
result += val | ||
|
||
elif val in self.operations: | ||
if val == "!": | ||
result = factorial(result) | ||
continue | ||
|
||
operator = self.operations[val] | ||
|
||
elif val in ["D%", "d%"]: | ||
result = _roll_dice(result if result is not None else 1, 100) | ||
|
||
else: | ||
raise Exception("Unable to evaluate input.") | ||
|
||
return result | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = DiceParser() | ||
|
||
# print("Recursive issues:", parser._parser.validate()) | ||
roll_strings = [ | ||
"5-3", | ||
"3-5", | ||
"3--5", | ||
"1d2d3", | ||
"5^2d1", | ||
"0!d20", | ||
"5 + 2!", | ||
"5**(2)", | ||
"5**2 * 7", | ||
"2 + 5 d 6", | ||
"(2)d6", | ||
"2d(6)", | ||
"3", | ||
"-3", | ||
"--3", | ||
"100.", | ||
"1 2", | ||
# "--7", # Currently have an issue with this. | ||
"9.0", | ||
"-12.05", | ||
"1 + 2", | ||
"2 - 1", | ||
"100 - 3", | ||
"12 // 4", | ||
"3+2*4", | ||
"2^4", | ||
"3 + 2 * 2^1", | ||
"9-1+27+(3-5)+9", | ||
"1d%", | ||
"d%", | ||
"D%", | ||
"1d20", | ||
"d20", | ||
"d20 + 5", | ||
"2d6 + 1d8 + 4", | ||
"5!", | ||
"pi", | ||
"pi + 2", | ||
"pi * e", | ||
"(2 + 8 / (9 - 5)) * 3", | ||
"100 - 21 / 7", | ||
# "((((((3))))))", | ||
] | ||
|
||
for rs in roll_strings: | ||
try: | ||
parsed_string = parser.parse(rs) | ||
|
||
# print(rs) | ||
# print(parsed_string) | ||
print(parser.evaluate(parsed_string)) | ||
except Exception: | ||
print("Exception occured parsing: " + rs) |
Oops, something went wrong.