expr-dice-roller 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- expr_dice_roller-0.0.1/LICENSE +9 -0
- expr_dice_roller-0.0.1/PKG-INFO +13 -0
- expr_dice_roller-0.0.1/pyproject.toml +28 -0
- expr_dice_roller-0.0.1/setup.cfg +4 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller/__init__.py +45 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller/dice_roller.py +100 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller/evaluator.py +304 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller/lexer.py +153 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller/modifiers.py +114 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller/parser.py +257 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller.egg-info/PKG-INFO +13 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller.egg-info/SOURCES.txt +13 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller.egg-info/dependency_links.txt +1 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller.egg-info/requires.txt +2 -0
- expr_dice_roller-0.0.1/src/expr_dice_roller.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: expr_dice_roller
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Unofficial port of dice-roller/rpg-dice-roller to Python.
|
|
5
|
+
Author: HellishBro
|
|
6
|
+
Project-URL: Homepage, https://github.com/HellishBro/dice_roller
|
|
7
|
+
Keywords: dice,expressions
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: build>=1.4.0
|
|
12
|
+
Requires-Dist: setuptools>=77.0.3
|
|
13
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "expr_dice_roller"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "HellishBro" }
|
|
10
|
+
]
|
|
11
|
+
license-files = [
|
|
12
|
+
"LICENSE"
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"dice", "expressions"
|
|
16
|
+
]
|
|
17
|
+
description = "Unofficial port of dice-roller/rpg-dice-roller to Python."
|
|
18
|
+
dependencies = [
|
|
19
|
+
"build>=1.4.0",
|
|
20
|
+
"setuptools>=77.0.3",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/HellishBro/dice_roller"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from lexer import Lexer
|
|
2
|
+
from parser import Parser
|
|
3
|
+
from evaluator import Evaluator, Printer, Environment
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
__all__ = ["format_expression", "EvaluationResult", "evaluate"]
|
|
7
|
+
|
|
8
|
+
def format_expression(expression: str) -> str:
|
|
9
|
+
t = Parser(Lexer(expression).lex()).expression()
|
|
10
|
+
if t:
|
|
11
|
+
return Printer().visit(t)
|
|
12
|
+
return ""
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class EvaluationResult:
|
|
16
|
+
environment: Environment
|
|
17
|
+
representation: str
|
|
18
|
+
value: float | None
|
|
19
|
+
|
|
20
|
+
def evaluate(expression: str, environment: Environment | None = None, assign_last_eval: bool = False) -> EvaluationResult:
|
|
21
|
+
t = Parser(Lexer(expression).lex()).expression()
|
|
22
|
+
if t:
|
|
23
|
+
expr_eval = Evaluator(environment)
|
|
24
|
+
rep, val = expr_eval.visit(t)
|
|
25
|
+
if assign_last_eval:
|
|
26
|
+
expr_eval.environment.assign("_", val)
|
|
27
|
+
if isinstance(val, (float, int)):
|
|
28
|
+
val = int(val) if val.is_integer() else val
|
|
29
|
+
else:
|
|
30
|
+
val = None
|
|
31
|
+
return EvaluationResult(expr_eval.environment, rep, val)
|
|
32
|
+
return EvaluationResult(environment, "", None)
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
env = None
|
|
36
|
+
|
|
37
|
+
while True:
|
|
38
|
+
inp = input("> ")
|
|
39
|
+
try:
|
|
40
|
+
print(format_expression(inp))
|
|
41
|
+
res = evaluate(inp, env, True)
|
|
42
|
+
env = res.environment
|
|
43
|
+
print(res.representation, "=", res.value)
|
|
44
|
+
except ValueError as e:
|
|
45
|
+
print(e)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from random import randint
|
|
3
|
+
import modifiers as mods
|
|
4
|
+
from modifiers import MAX_ITERATIONS
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Predicate:
|
|
9
|
+
predicate: str
|
|
10
|
+
comp: float
|
|
11
|
+
|
|
12
|
+
def meet(self, value: int) -> bool:
|
|
13
|
+
if self.predicate == "<>":
|
|
14
|
+
return value != self.comp
|
|
15
|
+
elif self.predicate == "=":
|
|
16
|
+
return value == self.comp
|
|
17
|
+
elif self.predicate == ">":
|
|
18
|
+
return value > self.comp
|
|
19
|
+
elif self.predicate == ">=":
|
|
20
|
+
return value >= self.comp
|
|
21
|
+
elif self.predicate == "<":
|
|
22
|
+
return value < self.comp
|
|
23
|
+
elif self.predicate == "<=":
|
|
24
|
+
return value <= self.comp
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Roll:
|
|
29
|
+
def __init__(self, value: int):
|
|
30
|
+
self.value = value
|
|
31
|
+
self.rep = "{}"
|
|
32
|
+
self.value_override = None
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return self.rep.replace("{}", str(self.value))
|
|
36
|
+
|
|
37
|
+
def reroll(self, new_roll):
|
|
38
|
+
self.value = new_roll.value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Dice:
|
|
43
|
+
count: int | None
|
|
44
|
+
side: int
|
|
45
|
+
drop_high: int | None = None
|
|
46
|
+
drop_low: int | None = None
|
|
47
|
+
keep_high: int | None = None
|
|
48
|
+
keep_low: int | None = None
|
|
49
|
+
minimum: int | None = None
|
|
50
|
+
maximum: int | None = None
|
|
51
|
+
explode: Predicate | bool | None = None
|
|
52
|
+
penetrate: Predicate | bool | None = None
|
|
53
|
+
reroll: Predicate | bool | None = None
|
|
54
|
+
reroll_once: bool = False
|
|
55
|
+
success: Predicate | None = None
|
|
56
|
+
failure: Predicate | None = None
|
|
57
|
+
|
|
58
|
+
def roll_once(self) -> Roll:
|
|
59
|
+
if self.side == 0: return Roll(0)
|
|
60
|
+
if self.side >= 1: return Roll(randint(1, self.side))
|
|
61
|
+
return Roll(randint(self.side, -1))
|
|
62
|
+
|
|
63
|
+
class RollResult:
|
|
64
|
+
def __init__(self, rolls: list[Roll]):
|
|
65
|
+
self.rolls = rolls
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def value(self) -> int:
|
|
69
|
+
return sum(roll.value_override if roll.value_override is not None else roll.value for roll in self.rolls)
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return "[" + ", ".join(repr(roll) for roll in self.rolls) + "]"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DiceRoller:
|
|
76
|
+
def __init__(self, dice: Dice):
|
|
77
|
+
self.dice = dice
|
|
78
|
+
|
|
79
|
+
def roll(self) -> RollResult:
|
|
80
|
+
res = RollResult([])
|
|
81
|
+
count = self.dice.count if self.dice.count is not None else 1
|
|
82
|
+
if count > 127:
|
|
83
|
+
raise ValueError(f"{count} exceeds the dice limit.")
|
|
84
|
+
|
|
85
|
+
for _ in range(count):
|
|
86
|
+
res.rolls.append(self.dice.roll_once())
|
|
87
|
+
|
|
88
|
+
modifiers: list[type[mods.Mod]] = []
|
|
89
|
+
if self.dice.minimum: modifiers.append(mods.Min)
|
|
90
|
+
if self.dice.maximum: modifiers.append(mods.Max)
|
|
91
|
+
if self.dice.explode: modifiers.append(mods.Explode)
|
|
92
|
+
if self.dice.penetrate: modifiers.append(mods.Penetrate)
|
|
93
|
+
if self.dice.reroll: modifiers.append(mods.ReRoll)
|
|
94
|
+
if self.dice.keep_high or self.dice.keep_low: modifiers.append(mods.Keep)
|
|
95
|
+
if self.dice.drop_high or self.dice.drop_low: modifiers.append(mods.Drop)
|
|
96
|
+
if self.dice.success or self.dice.failure: modifiers.append(mods.Success)
|
|
97
|
+
for modifier in modifiers:
|
|
98
|
+
modifier(self.dice).run(res)
|
|
99
|
+
|
|
100
|
+
return res
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from lexer import TT
|
|
2
|
+
from parser import Expr, BinOp, Unary, Number, Dice, Grouping, Predicate, FuncCall, Variable, Function
|
|
3
|
+
from dice_roller import Dice as DDice, DiceRoller, Predicate as DPredicate
|
|
4
|
+
|
|
5
|
+
class IEvaluator:
|
|
6
|
+
def __init__(self, environment=None):
|
|
7
|
+
self.environment = environment or Environment()
|
|
8
|
+
|
|
9
|
+
def enter(self):
|
|
10
|
+
self.environment = self.environment.enter()
|
|
11
|
+
|
|
12
|
+
def exit(self):
|
|
13
|
+
self.environment = self.environment.exit()
|
|
14
|
+
|
|
15
|
+
def visit(self, node: Expr):
|
|
16
|
+
return getattr(self, "visit_" + node.__class__.__name__)(node)
|
|
17
|
+
|
|
18
|
+
class EvalFunc:
|
|
19
|
+
def __init__(self, parameters: list[str], body: Expr, definition: Function):
|
|
20
|
+
self.parameters = parameters
|
|
21
|
+
self.body = body
|
|
22
|
+
self.definition = definition
|
|
23
|
+
|
|
24
|
+
def call(self, evaluator: IEvaluator, arguments: list[float]) -> float:
|
|
25
|
+
evaluator.enter()
|
|
26
|
+
for param, arg in zip(self.parameters, arguments):
|
|
27
|
+
evaluator.environment.assign(param, arg)
|
|
28
|
+
_, val = evaluator.visit(self.body)
|
|
29
|
+
evaluator.exit()
|
|
30
|
+
return val
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return Printer().visit_Function(self.definition)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Environment:
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.parent: Environment | None = None
|
|
39
|
+
self.variables: dict[str, float | EvalFunc] = {}
|
|
40
|
+
|
|
41
|
+
def enter(self):
|
|
42
|
+
new_env = Environment()
|
|
43
|
+
new_env.parent = self
|
|
44
|
+
return new_env
|
|
45
|
+
|
|
46
|
+
def get(self, name: str) -> float | EvalFunc:
|
|
47
|
+
if self.parent is None:
|
|
48
|
+
return self.variables.get(name, 0)
|
|
49
|
+
return self.variables.get(name, self.parent.get(name))
|
|
50
|
+
|
|
51
|
+
def assign(self, name: str, value: float | EvalFunc):
|
|
52
|
+
self.variables[name] = value
|
|
53
|
+
|
|
54
|
+
def exit(self):
|
|
55
|
+
p = self.parent
|
|
56
|
+
self.parent = None
|
|
57
|
+
return p
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ReprValue = tuple[str, float | EvalFunc]
|
|
61
|
+
class Evaluator(IEvaluator):
|
|
62
|
+
def __init__(self, environment=None):
|
|
63
|
+
super().__init__(environment)
|
|
64
|
+
self.function_depth = 0
|
|
65
|
+
|
|
66
|
+
def visit_BinOp(self, node: BinOp) -> ReprValue:
|
|
67
|
+
left_repr, left_val = self.visit(node.left)
|
|
68
|
+
right_repr, right_val = self.visit(node.right)
|
|
69
|
+
left_val = self._float(left_val)
|
|
70
|
+
right_val = self._float(right_val)
|
|
71
|
+
operator = node.operator
|
|
72
|
+
op = ""
|
|
73
|
+
res = 0
|
|
74
|
+
if operator.tt == TT.PLUS:
|
|
75
|
+
res = left_val + right_val
|
|
76
|
+
op = "+"
|
|
77
|
+
elif operator.tt == TT.MINUS:
|
|
78
|
+
res = left_val - right_val
|
|
79
|
+
op = "-"
|
|
80
|
+
elif operator.tt == TT.STAR:
|
|
81
|
+
res = left_val * right_val
|
|
82
|
+
op = "*"
|
|
83
|
+
elif operator.tt == TT.DIV:
|
|
84
|
+
res = left_val / right_val
|
|
85
|
+
op = "/"
|
|
86
|
+
elif operator.tt == TT.CARET:
|
|
87
|
+
res = left_val ** right_val
|
|
88
|
+
op = "^"
|
|
89
|
+
return f"{left_repr} {op} {right_repr}", res
|
|
90
|
+
|
|
91
|
+
def visit_Unary(self, node: Unary) -> ReprValue:
|
|
92
|
+
val_repr, val = self.visit(node.value)
|
|
93
|
+
val = self._float(val)
|
|
94
|
+
operator = node.operator
|
|
95
|
+
op = ""
|
|
96
|
+
res = 0
|
|
97
|
+
if operator.tt == TT.PLUS:
|
|
98
|
+
res = abs(val)
|
|
99
|
+
op = "+"
|
|
100
|
+
elif operator.tt == TT.MINUS:
|
|
101
|
+
res = -val
|
|
102
|
+
op = "-"
|
|
103
|
+
return f"{op}{val_repr}", res
|
|
104
|
+
|
|
105
|
+
def visit_Number(self, node: Number) -> ReprValue:
|
|
106
|
+
d = self._float(node.value.data)
|
|
107
|
+
return f"{d:g}", d
|
|
108
|
+
|
|
109
|
+
def visit_Grouping(self, node: Grouping) -> ReprValue:
|
|
110
|
+
rep, res = self.visit(node.value)
|
|
111
|
+
return f"({rep})", res
|
|
112
|
+
|
|
113
|
+
def visit_Predicate(self, node: Predicate) -> DPredicate:
|
|
114
|
+
return DPredicate({
|
|
115
|
+
TT.NEQ: "<>",
|
|
116
|
+
TT.EQ: "=",
|
|
117
|
+
TT.GT: ">",
|
|
118
|
+
TT.LT: "<",
|
|
119
|
+
TT.GE: ">=",
|
|
120
|
+
TT.LE: "<="
|
|
121
|
+
}[node.predicate.tt], self._float(self.visit(node.comp)[1]))
|
|
122
|
+
|
|
123
|
+
def visit_Function(self, node: Function) -> ReprValue:
|
|
124
|
+
func = EvalFunc([param.name.data for param in node.parameters], node.body, node)
|
|
125
|
+
self.environment.assign(node.name.name.data, func)
|
|
126
|
+
return self.visit(node.name)
|
|
127
|
+
|
|
128
|
+
def visit_Variable(self, node: Variable) -> ReprValue:
|
|
129
|
+
return node.name.data, self.environment.get(node.name.data)
|
|
130
|
+
|
|
131
|
+
def visit_FuncCall(self, node: FuncCall) -> ReprValue:
|
|
132
|
+
rep, func = self.visit(node.function)
|
|
133
|
+
rep_args = [self.visit(arg) for arg in node.arguments]
|
|
134
|
+
reps = [rep_arg[0] for rep_arg in rep_args]
|
|
135
|
+
args = [rep_arg[1] for rep_arg in rep_args]
|
|
136
|
+
rep += "(" + ", ".join(reps) + ")"
|
|
137
|
+
if not hasattr(func, "call"):
|
|
138
|
+
n = node.function.name.name.data if isinstance(node.function.name, Variable) else node.function.name.data
|
|
139
|
+
raise ValueError(f"{n} is not a defined function.")
|
|
140
|
+
|
|
141
|
+
if self.function_depth > 100:
|
|
142
|
+
raise ValueError(f"Max function depth limit reached.")
|
|
143
|
+
self.function_depth += 1
|
|
144
|
+
value = func.call(self, args)
|
|
145
|
+
self.function_depth -= 1
|
|
146
|
+
return rep, value
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _float(value) -> float:
|
|
150
|
+
if isinstance(value, (int, float, str)):
|
|
151
|
+
return float(value)
|
|
152
|
+
raise ValueError(f"{value} cannot be interpreted as a number.")
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _int(value) -> int | None:
|
|
156
|
+
if isinstance(value, (int, float)):
|
|
157
|
+
if value.is_integer():
|
|
158
|
+
return int(value)
|
|
159
|
+
else:
|
|
160
|
+
raise ValueError(f"{value} is not an integer.")
|
|
161
|
+
elif value is None: return None
|
|
162
|
+
raise ValueError("Functions cannot be interpreted as an integer.")
|
|
163
|
+
|
|
164
|
+
def visit_Dice(self, node: Dice) -> ReprValue:
|
|
165
|
+
count = self.visit(node.count)[1] if node.count else None
|
|
166
|
+
sides = self.visit(node.side)[1] if node.side != "%" else 100
|
|
167
|
+
drop_high = self.visit(node.drop_high)[1] if node.drop_high else None
|
|
168
|
+
drop_low = self.visit(node.drop_low)[1] if node.drop_low else None
|
|
169
|
+
keep_high = self.visit(node.keep_high)[1] if node.keep_high else None
|
|
170
|
+
keep_low = self.visit(node.keep_low)[1] if node.keep_low else None
|
|
171
|
+
minimum = self.visit(node.minimum)[1] if node.minimum else None
|
|
172
|
+
maximum = self.visit(node.maximum)[1] if node.maximum else None
|
|
173
|
+
explode = self.visit(node.explode) if isinstance(node.explode, Predicate) else node.explode
|
|
174
|
+
penetrate = self.visit(node.penetrate) if isinstance(node.penetrate, Predicate) else node.penetrate
|
|
175
|
+
reroll = self.visit(node.reroll) if isinstance(node.reroll, Predicate) else node.reroll
|
|
176
|
+
success = self.visit(node.success) if isinstance(node.success, Predicate) else None
|
|
177
|
+
failure = self.visit(node.failure) if isinstance(node.failure, Predicate) else None
|
|
178
|
+
ddice = DDice(
|
|
179
|
+
self._int(count),
|
|
180
|
+
self._int(sides),
|
|
181
|
+
self._int(drop_high),
|
|
182
|
+
self._int(drop_low),
|
|
183
|
+
self._int(keep_high),
|
|
184
|
+
self._int(keep_low),
|
|
185
|
+
self._int(minimum),
|
|
186
|
+
self._int(maximum),
|
|
187
|
+
|
|
188
|
+
explode,
|
|
189
|
+
penetrate,
|
|
190
|
+
reroll,
|
|
191
|
+
node.reroll_once,
|
|
192
|
+
|
|
193
|
+
success,
|
|
194
|
+
failure
|
|
195
|
+
)
|
|
196
|
+
roll_result = DiceRoller(ddice).roll()
|
|
197
|
+
return repr(roll_result), roll_result.value
|
|
198
|
+
|
|
199
|
+
class Printer(IEvaluator):
|
|
200
|
+
def visit_BinOp(self, node: BinOp) -> str:
|
|
201
|
+
left = self.visit(node.left)
|
|
202
|
+
right = self.visit(node.right)
|
|
203
|
+
operator = node.operator
|
|
204
|
+
op = ""
|
|
205
|
+
if operator.tt == TT.PLUS:
|
|
206
|
+
op = "+"
|
|
207
|
+
elif operator.tt == TT.MINUS:
|
|
208
|
+
op = "-"
|
|
209
|
+
elif operator.tt == TT.STAR:
|
|
210
|
+
op = "*"
|
|
211
|
+
elif operator.tt == TT.DIV:
|
|
212
|
+
op = "/"
|
|
213
|
+
elif operator.tt == TT.CARET:
|
|
214
|
+
op = "^"
|
|
215
|
+
return f"{left} {op} {right}"
|
|
216
|
+
|
|
217
|
+
def visit_Unary(self, node: Unary) -> str:
|
|
218
|
+
val = self.visit(node.value)
|
|
219
|
+
operator = node.operator
|
|
220
|
+
op = ""
|
|
221
|
+
if operator.tt == TT.PLUS:
|
|
222
|
+
op = "+"
|
|
223
|
+
elif operator.tt == TT.MINUS:
|
|
224
|
+
op = "-"
|
|
225
|
+
return f"{op}{val}"
|
|
226
|
+
|
|
227
|
+
def visit_Number(self, node: Number) -> str:
|
|
228
|
+
return f"{float(node.value.data):g}"
|
|
229
|
+
|
|
230
|
+
def visit_Grouping(self, node: Grouping) -> str:
|
|
231
|
+
rep = self.visit(node.value)
|
|
232
|
+
return f"({rep})"
|
|
233
|
+
|
|
234
|
+
def visit_Predicate(self, node: Predicate) -> str:
|
|
235
|
+
return {
|
|
236
|
+
TT.NEQ: "<>",
|
|
237
|
+
TT.EQ: "=",
|
|
238
|
+
TT.GT: ">",
|
|
239
|
+
TT.LT: "<",
|
|
240
|
+
TT.GE: ">=",
|
|
241
|
+
TT.LE: "<="
|
|
242
|
+
}[node.predicate.tt] + self.visit(node.comp)
|
|
243
|
+
|
|
244
|
+
def visit_Function(self, node: Function) -> str:
|
|
245
|
+
return f"{node.name.name.data}({', '.join(param.name.data for param in node.parameters)}) = {self.visit(node.body)}"
|
|
246
|
+
|
|
247
|
+
def visit_Variable(self, node: Variable) -> str:
|
|
248
|
+
return node.name.data
|
|
249
|
+
|
|
250
|
+
def visit_FuncCall(self, node: FuncCall) -> str:
|
|
251
|
+
rep = self.visit(node.function)
|
|
252
|
+
if isinstance(node.function, Function):
|
|
253
|
+
rep = f"({rep})"
|
|
254
|
+
reps = [self.visit(arg) for arg in node.arguments]
|
|
255
|
+
rep += "(" + ", ".join(reps) + ")"
|
|
256
|
+
return rep
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def visit_Dice(self, node: Dice) -> str:
|
|
260
|
+
count = self.visit(node.count) if node.count else ""
|
|
261
|
+
sides = self.visit(node.side) if node.side != "%" else "%"
|
|
262
|
+
drop_high = self.visit(node.drop_high) if node.drop_high else None
|
|
263
|
+
drop_low = self.visit(node.drop_low) if node.drop_low else None
|
|
264
|
+
keep_high = self.visit(node.keep_high) if node.keep_high else None
|
|
265
|
+
keep_low = self.visit(node.keep_low) if node.keep_low else None
|
|
266
|
+
minimum = self.visit(node.minimum) if node.minimum else None
|
|
267
|
+
maximum = self.visit(node.maximum) if node.maximum else None
|
|
268
|
+
explode = self.visit(node.explode) if isinstance(node.explode, Predicate) else node.explode
|
|
269
|
+
penetrate = self.visit(node.penetrate) if isinstance(node.penetrate, Predicate) else node.penetrate
|
|
270
|
+
reroll = self.visit(node.reroll) if isinstance(node.reroll, Predicate) else node.reroll
|
|
271
|
+
success = self.visit(node.success) if isinstance(node.success, Predicate) else None
|
|
272
|
+
failure = self.visit(node.failure) if isinstance(node.failure, Predicate) else None
|
|
273
|
+
rep = f"{count}d{sides}"
|
|
274
|
+
if success:
|
|
275
|
+
rep += success
|
|
276
|
+
if failure:
|
|
277
|
+
rep += f"f{failure}"
|
|
278
|
+
if minimum is not None:
|
|
279
|
+
rep += f"min{minimum}"
|
|
280
|
+
if maximum is not None:
|
|
281
|
+
rep += f"max{maximum}"
|
|
282
|
+
if explode is not None:
|
|
283
|
+
rep += "!"
|
|
284
|
+
if explode is not True:
|
|
285
|
+
rep += explode
|
|
286
|
+
if penetrate is not None:
|
|
287
|
+
rep += "!p"
|
|
288
|
+
if penetrate is not True:
|
|
289
|
+
rep += penetrate
|
|
290
|
+
if reroll is not None:
|
|
291
|
+
rep += "r"
|
|
292
|
+
if node.reroll_once:
|
|
293
|
+
rep += "o"
|
|
294
|
+
if reroll is not True:
|
|
295
|
+
rep += reroll
|
|
296
|
+
if keep_high:
|
|
297
|
+
rep += f"kh{keep_high}"
|
|
298
|
+
if keep_low:
|
|
299
|
+
rep += f"kl{keep_low}"
|
|
300
|
+
if drop_high:
|
|
301
|
+
rep += f"dh{drop_high}"
|
|
302
|
+
if drop_low:
|
|
303
|
+
rep += f"dl{drop_low}"
|
|
304
|
+
return rep
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
|
|
4
|
+
class TT(Enum):
|
|
5
|
+
NUMBER = auto() # int or float
|
|
6
|
+
NAME = auto() # any string of characters not recognized by the lexer
|
|
7
|
+
|
|
8
|
+
D = auto() # literal d (dice or drop)
|
|
9
|
+
DL = auto() # literal dl (drop low)
|
|
10
|
+
DH = auto() # literal dh (drop high)
|
|
11
|
+
K = auto() # literal k (keep)
|
|
12
|
+
KL = auto() # literal kl (keep low)
|
|
13
|
+
KH = auto() # literal kh (keep high)
|
|
14
|
+
MIN = auto() # literal min (min)
|
|
15
|
+
MAX = auto() # literal max (max)
|
|
16
|
+
R = auto() # literal r (reroll)
|
|
17
|
+
RO = auto() # literal ro (reroll once)
|
|
18
|
+
F = auto() # literal f (failure)
|
|
19
|
+
|
|
20
|
+
PLUS = auto() # +
|
|
21
|
+
MINUS = auto() # -
|
|
22
|
+
STAR = auto() # *
|
|
23
|
+
DIV = auto() # /
|
|
24
|
+
CARET = auto() # ^
|
|
25
|
+
|
|
26
|
+
PERCENT = auto() # %
|
|
27
|
+
|
|
28
|
+
COMMA = auto() # ,
|
|
29
|
+
|
|
30
|
+
BANG = auto() # literal ! (explode)
|
|
31
|
+
BANG_P = auto() # literal !p (penetrate)
|
|
32
|
+
GT = auto() # >
|
|
33
|
+
GE = auto() # >=
|
|
34
|
+
LT = auto() # <
|
|
35
|
+
LE = auto() # <=
|
|
36
|
+
EQ = auto() # =
|
|
37
|
+
NEQ = auto() # <> or !=
|
|
38
|
+
|
|
39
|
+
LPAREN = auto() # (
|
|
40
|
+
RPAREN = auto() # )
|
|
41
|
+
|
|
42
|
+
ERROR = auto()
|
|
43
|
+
|
|
44
|
+
class Token:
|
|
45
|
+
def __init__(self, tt: TT, pos: int, data: Any):
|
|
46
|
+
self.tt = tt
|
|
47
|
+
self.pos = pos
|
|
48
|
+
self.data = data
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"Token({self.tt!r}, {self.pos!r}, {self.data!r})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
NAME_TTs = TT.NAME, TT.DL, TT.DH, TT.K, TT.KL, TT.KH, TT.MIN, TT.MAX, TT.R, TT.RO, TT.F
|
|
55
|
+
|
|
56
|
+
class Lexer:
|
|
57
|
+
def __init__(self, sequence: str):
|
|
58
|
+
self.sequence = sequence
|
|
59
|
+
self.pos = 0
|
|
60
|
+
self.start_pos = 0
|
|
61
|
+
self.tokens = []
|
|
62
|
+
|
|
63
|
+
def add_token(self, tt: TT, data: Any | None = None):
|
|
64
|
+
self.tokens.append(Token(tt, self.start_pos, data or self.sequence[self.start_pos:self.pos]))
|
|
65
|
+
|
|
66
|
+
def at_end(self) -> bool:
|
|
67
|
+
return self.pos >= len(self.sequence)
|
|
68
|
+
|
|
69
|
+
def peek(self) -> str: return self.sequence[self.pos] if not self.at_end() else "\0"
|
|
70
|
+
|
|
71
|
+
def advance(self) -> str:
|
|
72
|
+
char = self.peek()
|
|
73
|
+
self.pos += 1
|
|
74
|
+
return char
|
|
75
|
+
|
|
76
|
+
def number(self, start: str):
|
|
77
|
+
decimal = start == "."
|
|
78
|
+
tt = TT.NUMBER
|
|
79
|
+
dat = None
|
|
80
|
+
while self.peek() in "1234567890.":
|
|
81
|
+
if decimal and self.peek() == ".":
|
|
82
|
+
tt = TT.ERROR
|
|
83
|
+
dat = "Invalid number literal."
|
|
84
|
+
elif self.peek() == "." and not decimal:
|
|
85
|
+
decimal = True
|
|
86
|
+
self.advance()
|
|
87
|
+
self.add_token(tt, dat)
|
|
88
|
+
|
|
89
|
+
def name(self):
|
|
90
|
+
stop_characters = " \n\t\0()+-*/=,<>^!1234567890.%"
|
|
91
|
+
while self.peek() not in stop_characters:
|
|
92
|
+
self.advance()
|
|
93
|
+
n = self.sequence[self.start_pos:self.pos]
|
|
94
|
+
self.add_token({
|
|
95
|
+
"d": TT.D,
|
|
96
|
+
"dl": TT.DL,
|
|
97
|
+
"dh": TT.DH,
|
|
98
|
+
"k": TT.K,
|
|
99
|
+
"kl": TT.KL,
|
|
100
|
+
"kh": TT.KH,
|
|
101
|
+
"min": TT.MIN,
|
|
102
|
+
"max": TT.MAX,
|
|
103
|
+
"r": TT.R,
|
|
104
|
+
"ro": TT.RO,
|
|
105
|
+
"f": TT.F
|
|
106
|
+
}.get(n, TT.NAME))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def token(self):
|
|
110
|
+
char = self.advance()
|
|
111
|
+
if char == "#":
|
|
112
|
+
while self.advance() not in "\n\0":
|
|
113
|
+
pass
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if char in " \n\t": return
|
|
117
|
+
if char in "1234567890.":
|
|
118
|
+
self.number(char)
|
|
119
|
+
elif char in "+-*/()=,^%":
|
|
120
|
+
mapping = {"+": TT.PLUS, "-": TT.MINUS, "*": TT.STAR, "/": TT.DIV, "(": TT.LPAREN, ")": TT.RPAREN, "=": TT.EQ, ",": TT.COMMA, "^": TT.CARET, "%": TT.PERCENT}
|
|
121
|
+
self.add_token(mapping[char])
|
|
122
|
+
elif char == ">":
|
|
123
|
+
if self.peek() == "=":
|
|
124
|
+
self.advance()
|
|
125
|
+
self.add_token(TT.GE)
|
|
126
|
+
else:
|
|
127
|
+
self.add_token(TT.GT)
|
|
128
|
+
elif char == "<":
|
|
129
|
+
if self.peek() == "=":
|
|
130
|
+
self.advance()
|
|
131
|
+
self.add_token(TT.LE)
|
|
132
|
+
elif self.peek() == ">":
|
|
133
|
+
self.advance()
|
|
134
|
+
self.add_token(TT.NEQ)
|
|
135
|
+
else:
|
|
136
|
+
self.add_token(TT.LT)
|
|
137
|
+
elif char == "!":
|
|
138
|
+
if self.peek() == "=":
|
|
139
|
+
self.advance()
|
|
140
|
+
self.add_token(TT.NEQ)
|
|
141
|
+
elif self.peek() == "p":
|
|
142
|
+
self.advance()
|
|
143
|
+
self.add_token(TT.BANG_P)
|
|
144
|
+
else:
|
|
145
|
+
self.add_token(TT.BANG)
|
|
146
|
+
else:
|
|
147
|
+
self.name()
|
|
148
|
+
|
|
149
|
+
def lex(self) -> list[Token]:
|
|
150
|
+
while not self.at_end():
|
|
151
|
+
self.start_pos = self.pos
|
|
152
|
+
self.token()
|
|
153
|
+
return self.tokens
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from dice_roller import RollResult, Dice
|
|
5
|
+
|
|
6
|
+
MAX_ITERATIONS = 100
|
|
7
|
+
|
|
8
|
+
class Mod:
|
|
9
|
+
def __init__(self, dice: Dice):
|
|
10
|
+
self.dice = dice
|
|
11
|
+
|
|
12
|
+
def run(self, res: RollResult): pass
|
|
13
|
+
|
|
14
|
+
class Min(Mod):
|
|
15
|
+
def run(self, res: RollResult):
|
|
16
|
+
for roll in res.rolls:
|
|
17
|
+
if roll.value < self.dice.minimum:
|
|
18
|
+
roll.value = self.dice.minimum
|
|
19
|
+
roll.rep += "^"
|
|
20
|
+
|
|
21
|
+
class Max(Mod):
|
|
22
|
+
def run(self, res: RollResult):
|
|
23
|
+
for roll in res.rolls:
|
|
24
|
+
if roll.value > self.dice.maximum:
|
|
25
|
+
roll.value = self.dice.maximum
|
|
26
|
+
roll.rep += "v"
|
|
27
|
+
|
|
28
|
+
class Explode(Mod):
|
|
29
|
+
def run(self, res: RollResult):
|
|
30
|
+
new_rolls = []
|
|
31
|
+
pred = self.dice.explode
|
|
32
|
+
for roll in res.rolls:
|
|
33
|
+
subrolls = [roll]
|
|
34
|
+
for i in range(MAX_ITERATIONS):
|
|
35
|
+
if (pred is True and subrolls[i].value == self.dice.side) or (pred is not True and pred.meet(subrolls[i].value)):
|
|
36
|
+
subrolls[i].rep += "!"
|
|
37
|
+
subrolls.append(self.dice.roll_once())
|
|
38
|
+
else:
|
|
39
|
+
break
|
|
40
|
+
new_rolls.extend(subrolls)
|
|
41
|
+
res.rolls = new_rolls
|
|
42
|
+
|
|
43
|
+
class Penetrate(Mod):
|
|
44
|
+
def run(self, res: RollResult):
|
|
45
|
+
new_rolls = []
|
|
46
|
+
for roll in res.rolls:
|
|
47
|
+
subrolls = [roll]
|
|
48
|
+
pred = self.dice.penetrate
|
|
49
|
+
for i in range(MAX_ITERATIONS):
|
|
50
|
+
if (pred is True and subrolls[i].value == self.dice.side) or (pred is not True and pred.meet(subrolls[i].value)):
|
|
51
|
+
subrolls[i].rep += "!p"
|
|
52
|
+
new_roll = self.dice.roll_once()
|
|
53
|
+
new_roll.value -= 1
|
|
54
|
+
subrolls.append(new_roll)
|
|
55
|
+
else:
|
|
56
|
+
break
|
|
57
|
+
new_rolls.extend(subrolls)
|
|
58
|
+
res.rolls = new_rolls
|
|
59
|
+
|
|
60
|
+
class ReRoll(Mod):
|
|
61
|
+
def run(self, res: RollResult):
|
|
62
|
+
pred = self.dice.reroll
|
|
63
|
+
for roll in res.rolls:
|
|
64
|
+
for i in range(MAX_ITERATIONS):
|
|
65
|
+
if (pred is True and roll.value == 1) or pred.meet(roll.value):
|
|
66
|
+
roll.reroll(self.dice.roll_once())
|
|
67
|
+
if i == 1:
|
|
68
|
+
roll.rep += "r"
|
|
69
|
+
if self.dice.reroll_once:
|
|
70
|
+
roll.rep += "o"
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
class Keep(Mod):
|
|
74
|
+
def run(self, res: RollResult):
|
|
75
|
+
sorted_values = sorted(res.rolls, key=lambda r: r.value)
|
|
76
|
+
invalidate_indices = [*range(len(res.rolls))]
|
|
77
|
+
if self.dice.keep_high:
|
|
78
|
+
del invalidate_indices[max(len(res.rolls) - self.dice.keep_high, 0):]
|
|
79
|
+
if self.dice.keep_low:
|
|
80
|
+
del invalidate_indices[:max(self.dice.keep_low, 0)]
|
|
81
|
+
|
|
82
|
+
for index in set(invalidate_indices):
|
|
83
|
+
dice = sorted_values[index]
|
|
84
|
+
dice.value_override = 0
|
|
85
|
+
dice.rep += "d"
|
|
86
|
+
|
|
87
|
+
class Drop(Mod):
|
|
88
|
+
def run(self, res: RollResult):
|
|
89
|
+
sorted_values = sorted(res.rolls, key=lambda r: r.value)
|
|
90
|
+
invalidate_indices = []
|
|
91
|
+
if self.dice.drop_low:
|
|
92
|
+
invalidate_indices = [*range(self.dice.drop_low)]
|
|
93
|
+
if self.dice.drop_high:
|
|
94
|
+
invalidate_indices = [*range(max(len(res.rolls) - self.dice.drop_high, 0), len(res.rolls))]
|
|
95
|
+
if len(invalidate_indices) > len(sorted_values):
|
|
96
|
+
invalidate_indices = [*range(len(sorted_values))]
|
|
97
|
+
|
|
98
|
+
for index in set(invalidate_indices):
|
|
99
|
+
dice = sorted_values[index]
|
|
100
|
+
if dice.value_override != 0:
|
|
101
|
+
dice.value_override = 0
|
|
102
|
+
dice.rep += "d"
|
|
103
|
+
|
|
104
|
+
class Success(Mod):
|
|
105
|
+
def run(self, res: RollResult):
|
|
106
|
+
for roll in res.rolls:
|
|
107
|
+
if self.dice.success.meet(roll.value):
|
|
108
|
+
roll.value_override = 1
|
|
109
|
+
roll.rep += "*"
|
|
110
|
+
elif self.dice.failure and self.dice.failure.meet(roll.value):
|
|
111
|
+
roll.value_override = -1
|
|
112
|
+
roll.rep += "_"
|
|
113
|
+
else:
|
|
114
|
+
roll.value_override = 0
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from lexer import TT, Token, NAME_TTs
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
class Expr: pass
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class BinOp(Expr):
|
|
10
|
+
left: Expr
|
|
11
|
+
operator: Token
|
|
12
|
+
right: Expr
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Unary(Expr):
|
|
16
|
+
operator: Token
|
|
17
|
+
value: Expr
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Grouping(Expr):
|
|
21
|
+
value: Expr
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Number(Expr):
|
|
25
|
+
value: Token
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Predicate(Expr):
|
|
29
|
+
predicate: Token
|
|
30
|
+
comp: Expr
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Variable(Expr):
|
|
34
|
+
name: Token
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Function:
|
|
38
|
+
name: Variable
|
|
39
|
+
parameters: list[Variable]
|
|
40
|
+
body: Expr
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class FuncCall(Expr):
|
|
44
|
+
function: Variable | Function
|
|
45
|
+
arguments: list[Expr]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Dice(Expr):
|
|
50
|
+
count: Expr | None
|
|
51
|
+
side: Expr | Literal['%']
|
|
52
|
+
drop_high: Expr | None = None
|
|
53
|
+
drop_low: Expr | None = None
|
|
54
|
+
keep_high: Expr | None = None
|
|
55
|
+
keep_low: Expr | None = None
|
|
56
|
+
minimum: Expr | None = None
|
|
57
|
+
maximum: Expr | None = None
|
|
58
|
+
|
|
59
|
+
explode: Predicate | bool | None = None
|
|
60
|
+
penetrate: Predicate | bool | None = None
|
|
61
|
+
reroll: Predicate | bool | None = None
|
|
62
|
+
reroll_once: bool = False
|
|
63
|
+
|
|
64
|
+
success: Predicate | None = None
|
|
65
|
+
failure: Predicate | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Parser:
|
|
69
|
+
def __init__(self, tokens: list[Token]):
|
|
70
|
+
self.tokens = tokens
|
|
71
|
+
self.position = 0
|
|
72
|
+
|
|
73
|
+
def at_end(self) -> bool: return self.position >= len(self.tokens)
|
|
74
|
+
|
|
75
|
+
def peek(self, lookahead: int = 0) -> Token:
|
|
76
|
+
return self.tokens[self.position + lookahead] if not self.at_end() else Token(TT.ERROR, -1, "End of statement.")
|
|
77
|
+
|
|
78
|
+
def advance(self) -> Token:
|
|
79
|
+
current = self.peek()
|
|
80
|
+
self.position += 1
|
|
81
|
+
return current
|
|
82
|
+
|
|
83
|
+
def match(self, *tt: TT) -> Token | None:
|
|
84
|
+
if self.peek().tt in tt:
|
|
85
|
+
return self.advance()
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def match_name(self) -> Token | None:
|
|
89
|
+
return self.match(
|
|
90
|
+
*NAME_TTs
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def expect(self, *tt: TT) -> Token:
|
|
94
|
+
if match := self.match(*tt):
|
|
95
|
+
return match
|
|
96
|
+
raise ValueError(f"Expected one of the following: {tt}.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def binop(self, next_func, operators: list[TT]) -> Expr:
|
|
100
|
+
value = next_func()
|
|
101
|
+
while operator := self.match(*operators):
|
|
102
|
+
right = next_func()
|
|
103
|
+
value = BinOp(value, operator, right)
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def expression(self) -> Expr:
|
|
108
|
+
return self.add_or_sub()
|
|
109
|
+
|
|
110
|
+
def add_or_sub(self) -> Expr:
|
|
111
|
+
return self.binop(self.mult_or_div, [TT.PLUS, TT.MINUS])
|
|
112
|
+
|
|
113
|
+
def mult_or_div(self) -> Expr:
|
|
114
|
+
return self.binop(self.unary, [TT.STAR, TT.DIV])
|
|
115
|
+
|
|
116
|
+
def unary(self) -> Expr:
|
|
117
|
+
if operator := self.match(TT.MINUS, TT.PLUS):
|
|
118
|
+
return Unary(operator, self.expo())
|
|
119
|
+
return self.expo()
|
|
120
|
+
|
|
121
|
+
def expo(self) -> Expr:
|
|
122
|
+
value = self.primitive()
|
|
123
|
+
while token := self.match(TT.CARET):
|
|
124
|
+
if isinstance(value, BinOp):
|
|
125
|
+
value = BinOp(value.left, value.operator, BinOp(value.right, token, self.primitive()))
|
|
126
|
+
else:
|
|
127
|
+
value = BinOp(value, token, self.primitive())
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
def primitive(self) -> Expr:
|
|
131
|
+
n = self.atom_optional()
|
|
132
|
+
if self.match(TT.D): # dice
|
|
133
|
+
if self.peek().tt == TT.PERCENT:
|
|
134
|
+
self.advance()
|
|
135
|
+
sides = '%'
|
|
136
|
+
else:
|
|
137
|
+
sides = self.atom()
|
|
138
|
+
dice = Dice(n, sides)
|
|
139
|
+
while self.dice_modifier(dice):
|
|
140
|
+
pass
|
|
141
|
+
return dice
|
|
142
|
+
return n
|
|
143
|
+
|
|
144
|
+
def predicate(self) -> Predicate | None:
|
|
145
|
+
if operator := self.match(TT.NEQ, TT.EQ, TT.GT, TT.GE, TT.LT, TT.LE):
|
|
146
|
+
val = self.atom()
|
|
147
|
+
return Predicate(operator, val)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def dice_modifier(self, dice: Dice) -> bool:
|
|
151
|
+
if self.match(TT.D, TT.DL):
|
|
152
|
+
if not dice.drop_low:
|
|
153
|
+
dice.drop_low = self.atom()
|
|
154
|
+
else: raise ValueError(f"Dice have duplicate field: drop low.")
|
|
155
|
+
elif self.match(TT.DH):
|
|
156
|
+
if not dice.drop_high:
|
|
157
|
+
dice.drop_high = self.atom()
|
|
158
|
+
else: raise ValueError(f"Dice have duplicate field: drop high.")
|
|
159
|
+
elif self.match(TT.K, TT.KH):
|
|
160
|
+
if not dice.keep_high:
|
|
161
|
+
dice.keep_high = self.atom()
|
|
162
|
+
else: raise ValueError(f"Dice have duplicate field: keep high.")
|
|
163
|
+
elif self.match(TT.KL):
|
|
164
|
+
if not dice.keep_low:
|
|
165
|
+
dice.keep_low = self.atom()
|
|
166
|
+
else: raise ValueError(f"Dice have duplicate field: keep low.")
|
|
167
|
+
elif self.match(TT.MIN):
|
|
168
|
+
if not dice.minimum:
|
|
169
|
+
dice.minimum = self.atom()
|
|
170
|
+
else: raise ValueError(f"Dice have duplicate field: minimum.")
|
|
171
|
+
elif self.match(TT.MAX):
|
|
172
|
+
if not dice.maximum:
|
|
173
|
+
dice.maximum = self.atom()
|
|
174
|
+
else: raise ValueError(f"Dice have duplicate field: maximum.")
|
|
175
|
+
elif op := self.match(TT.BANG, TT.BANG_P):
|
|
176
|
+
if not dice.explode and not dice.penetrate:
|
|
177
|
+
if op.tt == TT.BANG:
|
|
178
|
+
dice.explode = self.predicate() or True
|
|
179
|
+
else:
|
|
180
|
+
dice.penetrate = self.predicate() or True
|
|
181
|
+
else: raise ValueError(f"Dice have duplicate field: explode/penetrate.")
|
|
182
|
+
elif op := self.match(TT.R, TT.RO):
|
|
183
|
+
if not dice.reroll:
|
|
184
|
+
if op.tt == TT.RO:
|
|
185
|
+
dice.reroll_once = True
|
|
186
|
+
dice.reroll = self.predicate() or True
|
|
187
|
+
else: raise ValueError(f"Dice have duplicate field: reroll.")
|
|
188
|
+
|
|
189
|
+
elif pred := self.predicate():
|
|
190
|
+
if not dice.success:
|
|
191
|
+
dice.success = pred
|
|
192
|
+
if self.match(TT.F):
|
|
193
|
+
dice.failure = self.predicate()
|
|
194
|
+
else: raise ValueError(f"Dice have duplicate field: success.")
|
|
195
|
+
else:
|
|
196
|
+
return False
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
def paramslist(self) -> list[Variable]:
|
|
200
|
+
lst = []
|
|
201
|
+
while n := self.match_name():
|
|
202
|
+
lst.append(Variable(n))
|
|
203
|
+
self.match(TT.COMMA)
|
|
204
|
+
self.expect(TT.RPAREN)
|
|
205
|
+
return lst
|
|
206
|
+
|
|
207
|
+
def argslist(self) -> list[Expr]:
|
|
208
|
+
lst = []
|
|
209
|
+
while self.peek().tt != TT.RPAREN:
|
|
210
|
+
expr = self.expression()
|
|
211
|
+
lst.append(expr)
|
|
212
|
+
self.match(TT.COMMA)
|
|
213
|
+
self.expect(TT.RPAREN)
|
|
214
|
+
|
|
215
|
+
return lst
|
|
216
|
+
|
|
217
|
+
def atom(self) -> Expr:
|
|
218
|
+
r = self.atom_optional()
|
|
219
|
+
if r is None:
|
|
220
|
+
self.expect(TT.NUMBER)
|
|
221
|
+
return r
|
|
222
|
+
|
|
223
|
+
def atom_optional(self) -> Expr | Function | None:
|
|
224
|
+
if name := self.match_name():
|
|
225
|
+
var = Variable(name)
|
|
226
|
+
if self.match(TT.LPAREN):
|
|
227
|
+
position = self.position
|
|
228
|
+
args_list = self.argslist()
|
|
229
|
+
if self.peek().tt == TT.EQ:
|
|
230
|
+
self.position = position
|
|
231
|
+
params_list = self.paramslist()
|
|
232
|
+
self.expect(TT.EQ)
|
|
233
|
+
body = self.expression()
|
|
234
|
+
return Function(var, params_list, body)
|
|
235
|
+
|
|
236
|
+
return FuncCall(var, args_list)
|
|
237
|
+
return var
|
|
238
|
+
|
|
239
|
+
if self.match(TT.LPAREN):
|
|
240
|
+
val = self.expression()
|
|
241
|
+
self.expect(TT.RPAREN)
|
|
242
|
+
if isinstance(val, Function) and self.match(TT.LPAREN):
|
|
243
|
+
position = self.position
|
|
244
|
+
args_list = self.argslist()
|
|
245
|
+
if self.peek().tt == TT.EQ:
|
|
246
|
+
self.position = position
|
|
247
|
+
params_list = self.paramslist()
|
|
248
|
+
self.expect(TT.EQ)
|
|
249
|
+
body = self.expression()
|
|
250
|
+
return Function(val.name, params_list, body)
|
|
251
|
+
return FuncCall(val, args_list)
|
|
252
|
+
return Grouping(val)
|
|
253
|
+
|
|
254
|
+
if n := self.match(TT.NUMBER):
|
|
255
|
+
return Number(n)
|
|
256
|
+
|
|
257
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: expr_dice_roller
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Unofficial port of dice-roller/rpg-dice-roller to Python.
|
|
5
|
+
Author: HellishBro
|
|
6
|
+
Project-URL: Homepage, https://github.com/HellishBro/dice_roller
|
|
7
|
+
Keywords: dice,expressions
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: build>=1.4.0
|
|
12
|
+
Requires-Dist: setuptools>=77.0.3
|
|
13
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/expr_dice_roller/__init__.py
|
|
4
|
+
src/expr_dice_roller/dice_roller.py
|
|
5
|
+
src/expr_dice_roller/evaluator.py
|
|
6
|
+
src/expr_dice_roller/lexer.py
|
|
7
|
+
src/expr_dice_roller/modifiers.py
|
|
8
|
+
src/expr_dice_roller/parser.py
|
|
9
|
+
src/expr_dice_roller.egg-info/PKG-INFO
|
|
10
|
+
src/expr_dice_roller.egg-info/SOURCES.txt
|
|
11
|
+
src/expr_dice_roller.egg-info/dependency_links.txt
|
|
12
|
+
src/expr_dice_roller.egg-info/requires.txt
|
|
13
|
+
src/expr_dice_roller.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
expr_dice_roller
|