forked from rubellyte/Red_Star_Plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdiceroll.py
188 lines (153 loc) · 6.14 KB
/
diceroll.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
from red_star.plugin_manager import BasePlugin
from red_star.rs_utils import respond
from red_star.command_dispatcher import Command
from red_star.rs_errors import CommandSyntaxError
from random import randint
from collections import defaultdict
from enum import Enum
import re
roll_tokens = re.compile(r"\d+(?:\.\d+)?|d(?:\d+|f)[da]?|[+\-/*()]")
class Adv(Enum):
n = 0
a = 1
d = 2
def parse_roll(roll_string: str):
rolls = []
def rpn(string: str):
# tokenize by only finding the valid operators
tokens = roll_tokens.findall(string.lower())
# turn into reverse polish notation for easier parsing
prec = defaultdict(lambda: 3, {
'(': 0,
'-': 1,
'+': 1,
'*': 2,
'/': 2
})
polish = []
ops = []
unary_minus_flag = True # ugh
new_expr_flag = False # because people may not write right
for token in tokens:
try:
try:
val = int(token)
except ValueError:
val = float(token)
if new_expr_flag:
polish.extend(ops[::-1])
ops = []
polish.append(val)
unary_minus_flag = False
new_expr_flag = True
except ValueError:
new_expr_flag = False
if token == '(':
ops.append(token)
unary_minus_flag = True
elif token == ')':
while ops and ops[-1] != '(':
polish.append(ops.pop())
if ops:
ops.pop()
unary_minus_flag = False
elif token[0] == 'd':
polish.append(token)
unary_minus_flag = False
else:
# unary minuses are hard.
if unary_minus_flag and token == '-':
token = '_'
unary_minus_flag = token in '*/+-'
# as cool as it would be to have strings of data then strings of operators, this works better
while ops and prec[ops[-1]] >= prec[token]:
polish.append(ops.pop())
ops.append(token)
polish.extend(ops[::-1])
return polish
def roll(dice: str, stack: list):
num = min(max(round(stack.pop()), 1), 10000) if stack else 1
if dice[-1] in 'ad':
reroll = Adv.a if dice[-1] == 'a' else Adv.d
side = 'f' if dice[1:-1] == 'f' else int(dice[1:-1])
else:
reroll = Adv.n
side = 'f' if dice[1:] == 'f' else int(dice[1:])
if side == 'f':
def diceroll():
return randint(-1, 1)
else:
side = min(max(side, 2), 10000)
def diceroll():
return randint(1, side)
roll_a = [diceroll() for _ in range(num)]
roll_b = [diceroll() for _ in range(num)]
if reroll == Adv.a:
roll_a = roll_a if sum(roll_a) > sum(roll_b) else roll_b
elif reroll == Adv.d:
roll_a = roll_a if sum(roll_a) < sum(roll_b) else roll_b
rolls.append(f"{num}d{side}{'a' if reroll == Adv.a else ('d' if reroll == Adv.d else '')} - {sum(roll_a):2d}"
f" {roll_a}")
stack.append(sum(roll_a))
# evaluate using reverse polish notation rules
polish = rpn(roll_string)
stack = []
u_ops = {
'_': lambda: stack.append(-stack.pop())
}
b_ops = {
'+': lambda x, y: stack.append(y + x),
'-': lambda x, y: stack.append(y - x),
'/': lambda x, y: stack.append(y / x),
'*': lambda x, y: stack.append(y * x)
}
while polish:
token = polish.pop(0)
if isinstance(token, (int, float)):
stack.append(token)
elif len(stack) > 1 and token in b_ops:
b_ops[token](stack.pop(), stack.pop())
elif len(stack) > 0 and token in u_ops:
u_ops[token]()
elif token[0] == 'd':
roll(token, stack)
return stack, rolls
class DiceRoll(BasePlugin):
name = "diceroll"
description = "A plugin for rolling dice and dice accessories."
version = "1.0"
author = "GTG3000"
@Command("Roll",
doc="Rolls a specified amount of specified dice with specified bonus and advantage/disadvantage.\n\n"
"The parser can evaluate full dice expressions, including basic calculations such as addition, "
"subtraction, multiplication and division.\n"
"It is also capable of doing multiple rolls per same expression, '4df 4df' for example.\n"
"'dn' is treated as an unary operator, so for example '2d6 d6' will roll 2d6 and then roll that "
"amount of d6.",
syntax="[number]D(die/F)[A/D][+/-bonus]",
category="role_play",
run_anywhere=True)
async def _roll(self, msg):
args = msg.clean_content.split(None, 1)
if len(args) < 2:
raise CommandSyntaxError("Requires a roll expression.")
results, rolls = parse_roll(args[1])
roll_args = ' '.join(roll_tokens.findall(args[1])).upper()
results = (str(x) for x in results)
if rolls:
t_string = f"**ANALYSIS: {msg.author.display_name} has attempted a " \
f"{roll_args} roll, getting {', '.join(results)}.\n" \
f"ANALYSIS: Rolled dice:** ```\n"
if len(rolls[0]) + len(t_string) <= 1996:
for r in rolls:
if len(t_string) + len(r) > 1996:
t_string += r[:1993 - len(t_string)] + '...'
break
else:
t_string += r + '\n'
t_string += '```'
else:
t_string += rolls[0][:1990 - len(t_string)] + '...\n```'
await respond(msg, t_string)
else:
await respond(msg, f"**ANALYSIS: expression {roll_args} evaluated. Result: {', '.join(results)}**")