-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.py
256 lines (202 loc) · 5.19 KB
/
utils.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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
import random, math
from random import randint as rng
from itertools import accumulate
def gen_stats():
stats = [rng(10, 11) for _ in range(6)]
while True:
ind1 = rng(0, 5)
ind2 = rng(0, 5)
if ind1 == ind2:
continue
if not rng(5, 9) <= stats[ind1] <= rng(11, 15):
continue
if not rng(5, 9) <= stats[ind2] <= rng(11, 15):
continue
if not one_in(7):
stats[ind1] -= 1
if not one_in(7):
stats[ind2] += 1
if one_in(50):
return stats
def rng_float(a, b):
if a > b:
a, b = b, a
return random.uniform(a, b)
def triangular_roll(a, b):
#Returns a rand integer between a and b inclusive, biased towards the average result
range = b - a
r1 = range//2
r2 = (range+1)//2
return a + rng(0, r1) + rng(0, r2)
def gauss_roll(mod):
return random.gauss(10 + mod, 5)
def gauss_cdf(mean, stdev, x):
return 0.5 * (1 + math.erf((x - mean)/(stdev*math.sqrt(2))))
def gauss_roll_prob(mod, DC):
cdf = gauss_cdf(10 + mod, 5, DC)
return (1 - cdf) * 100
def clamp(val, lo, hi):
return max(lo, min(val, hi))
def one_in(x):
return x <= 1 or rng(1, x) == 1
def x_in_y(x, y):
return random.uniform(0.0, y) < x
def div_rand(x, y):
"Computes x/y then randomly rounds the result up or down depending on the remainder"
sign = 1
if (x > 0) ^ (y > 0):
sign = -1
x = abs(x)
y = abs(y)
mod = x % y
return sign * (x//y + (rng(1, y) <= mod))
def mult_rand_frac(val, x, y):
return div_rand(val * x, y)
def stat_mod(stat):
return (stat - 10) / 2
def random_weighted(entries):
values, weights = list(zip(*entries))
return random.choices(values, weights=weights)[0]
def dice(num, sides):
if sides == 1:
return num
return sum(rng(1, sides) for _ in range(num))
def gen_stat():
vals = [rng(1, 6) for _ in range(4)]
return sum(vals) - min(vals)
def display_bar(val, max, width):
val = clamp(val, 0, max)
part = width * val / max
num = int(part)
string = []
bars = "|"*num
rem = part - int(part)
if 0 < val < max and (num <= 0 or rem >= 0.5):
bars += "."
bars += " "*(width-len(bars))
return f"[{bars}]"
def apply_armor(damage, armor):
prot = rng(0, 2 * armor)
return max(damage - prot, 0)
def calc_ranged_penalty(r, short, long):
if r <= short:
return 0.0
return 5 * (r - short) / (long - short)
def make_damage_msg(prefix, attacker, damage):
msg = prefix
if damage > 0 and attacker.is_player():
msg += f" for {damage} damage"
elif damage <= 0:
msg += " but does no damage"
msg += "."
return msg
class WeightedList:
def __init__(self):
self.choices = []
self.weights = []
self.cumulative_weights = None
def add(self, value, weight):
if weight > 0:
self.choices.append(value)
self.weights.append(weight)
self.cumulative_weights = None
def clear(self):
self.choices.clear()
self.weights.clear()
self.cumulative_weights = None
def pick(self):
if len(self.choices) == 0:
raise IndexError("cannot pick from an empty weighted list")
if not self.cumulative_weights:
self.cumulative_weights = list(accumulate(self.weights))
return random.choices(self.choices, cum_weights=self.cumulative_weights)[0]
class Dice:
def __init__(self, num, sides, mod=0):
self.num = num
self.sides = sides
self.mod = mod
def roll(self):
return dice(self.num, self.sides) + self.mod
def __str__(self):
s = f"{self.num}d{self.sides}"
if self.mod != 0:
s += f"+{self.mod}" if self.mod > 0 else str(self.mod)
return s
class Point:
def __init__(self, x=0, y=0):
self.set(x, y)
def set(self, x, y):
self.x = x
self.y = y
def set_to(self, other):
self.x = other.x
self.y = other.y
def __repr__(self):
return f"({self.x}, {self.y})"
def __add__(self, other):
if isinstance(other, Point):
return Point(self.x + other.x, self.y + other.y)
return NotImplemented
def __iadd__(self, other):
if isinstance(self, Point):
self.x += other.x
self.y += other.y
return self
return NotImplemented
def __sub__(self, other):
if isinstance(other, Point):
return Point(self.x - other.x, self.y - other.y)
return NotImplemented
def __isub__(self, other):
if isinstance(other, Point):
self.x -= other.x
self.y -= other.y
return self
return NotImplemented
def __neg__(self):
return Point(-self.x, -self.y)
def __abs__(self):
return Point(abs(self.x), abs(self.y))
def as_tuple(self):
return (self.x, self.y)
def copy(self):
return Point(self.x, self.y)
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
if isinstance(other, Point):
return self.x == other.x and self.y == other.y
return False
def distance(self, other):
delta = other - self
return abs(delta.x) + abs(delta.y)
def square_dist(self, other):
abs_d = abs(other - self)
return max(abs_d.x, abs_d.y)
def points_in_line(p1, p2, d=0):
x1 = p1.x
y1 = p1.y
x2 = p2.x
y2 = p2.y
dx = abs(x2 - x1)
sx = 1 if x1 < x2 else -1
dy = -abs(y2 - y1)
sy = 1 if y1 < y2 else -1
error = dx + dy + d
pos = Point(x1, y1)
endpos = Point(x2, y2)
while True:
yield pos.copy()
if pos == endpos:
break
e2 = 2 * error
if e2 >= dy:
if pos.x == x2:
break
error += dy
pos.x += sx
if e2 <= dx:
if pos.y == y2:
break
error += dx
pos.y += sy