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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ build>=1.4.0
2
+ setuptools>=77.0.3
@@ -0,0 +1 @@
1
+ expr_dice_roller