-
Notifications
You must be signed in to change notification settings - Fork 1
/
dice.lua
292 lines (226 loc) · 12.2 KB
/
dice.lua
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
--- A library used to roll and manipulate roguelike based dice
-- @classmod dice
-- @author Timothy Torres
-- @copyright 2013
-- @license MIT/X11
local dice = {
_AUTHOR = 'Timothy Torres',
_URL = 'https://github.com/timothymtorres/RL-Dice',
_DESCRIPTION = 'A robust module to roll and manipulate roguelike dice.',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Timothy Torres
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:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
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.
]],
class_minimum = 1, -- class default lowest possible roll is 1 (can set to nil to allow negative rolls)
_cache = {},
}
dice.__index = dice
local random, max, abs, sort, match, format = math.random, math.max, math.abs, table.sort, string.match, string.format
--- Creates a new dice instance
-- @tparam ?int|string dice_notation Can be either a dice string, or int
-- @tparam[opt] int minimum Sets dice instance roll's minimum result boundaries
-- @treturn dice
function dice:new(dice_notation, minimum)
-- If dice_notation is a number, we must convert it into the proper dice string format
if type(dice_notation) == 'number' then dice_notation = '1d'..dice_notation end
local dice_pattern = '[(]?[-]?%d+[d][-]?%d+[+-]?[+-]?%d*[%^]?[+-]?[+-]?%d*[)]?[x]?[-]?%d*'
assert(dice_notation == match(dice_notation, dice_pattern), "Dice string incorrectly formatted.")
local bonus_notation = match(dice_notation, '[d][-]?%d+([+-]?[+-]%d+)')
if bonus_notation then assert(bonus_notation == match(bonus_notation, '[+-]+%d+'), "Dice string bonus portion incorrectly formatted.") end
local reroll_notation = match(dice_notation, '[%^][+-]?[+-]?%d*')
if reroll_notation then assert(reroll_notation == match(reroll_notation, '[%^][+-]+%d+'), "Dice string reroll portion incorrectly formatted.") end
local merged_notation = dice_notation .. (minimum and '['..minimum..']' or '')
if not dice._cache[merged_notation] then
local dice_INST = {}
dice_INST.num = tonumber(match(dice_notation, '[-]?%d+'))
dice_INST.faces = tonumber(match(dice_notation, '[d]([-]?%d+)'))
local double_bonus = match(dice_notation, '[d][-]?%d+([+-]?[+-])%d+')
local bonus = match(dice_notation, '[d][-]?%d+[+-]?([+-]%d+)')
dice_INST.is_bonus_plural = double_bonus == '++' or double_bonus == '--'
dice_INST.bonus = tonumber(bonus) or 0
local double_reroll = match(dice_notation, '[%^]([+-]?[+-])%d+')
local reroll = match(dice_notation, '[%^][+-]?([+-]%d+)')
dice_INST.is_reroll_plural = double_reroll == '++' or double_reroll == '--'
dice_INST.rerolls = tonumber(reroll) or 0
dice_INST.sets = tonumber(match(dice_notation, '[x]([-]?%d+)')) or 1
dice_INST.minimum = minimum
dice_INST.notation = dice_notation
dice._cache[merged_notation] = setmetatable(dice_INST, self)
end
return dice._cache[merged_notation]
end
--- Sets class minimum for dice module
--@tparam[opt] int min Sets dice class minimum result boundaries (if nil, no minimum result)
function dice.setClassMin(min) dice.class_minimum = min end
--- Resets the cache for dice instances
function dice.resetCache() dice._cache = {} end
--- The raw notation including all negatives
-- @treturn string
function dice:getNotation() return self.notation end
--- Number of total dice
-- @treturn int
function dice:getNum() return self.num end
--- Number of total faces on a dice
-- @treturn int
function dice:getFaces() return self.faces end
--- Bonus to be added to the dice total
-- @treturn int
function dice:getBonus() return self.bonus end
--- Rerolls to be added to the dice
-- @treturn int
function dice:getRerolls() return self.rerolls end
--- Number of total dice sets
-- @treturn int
function dice:getSets() return self.sets end
--- Bonus to be added to all dice (if double bonus enabled) otherwise regular bonus
-- @treturn int
function dice:getTotalBonus() return (self.is_bonus_plural and self.bonus*self.num) or self.bonus end
--- Rerolls to be added to all dice (if double reroll enabled) otherwise regular reroll
-- @treturn int
function dice:getTotalRerolls() return (self.is_reroll_plural and self.rerolls*self.num) or self.rerolls end
--- Determines if all dice are to be rerolled together or individually
-- @treturn bool
function dice:isDoubleReroll() return self.is_reroll_plural end
--- Determines if all dice are to apply a bonus together or individually
-- @treturn bool
function dice:isDoubleBonus() return self.is_bonus_plural end
-- local function used with our metamethods
local function modifyNotation(dice_INST, field, value)
-- For the field that is about to be modfied the old value must be preserved
-- so the variables declared for the new dice_INST don't overwrite the old dice_INST
local original_value = dice_INST[field] -- save this so we can restore it later
dice_INST[field] = dice_INST[field] + value -- modfiy the field with added value
-- apply all fields to variables that will be used to generate our notation string
local num_dice, dice_faces, bonus, is_bonus_plural, rerolls, is_reroll_plural, sets, minimum = dice_INST.num, dice_INST.faces, dice_INST.bonus, dice_INST.is_bonus_plural, dice_INST.rerolls, dice_INST.is_reroll_plural, dice_INST.sets, dice_INST.minimum
dice_INST[field] = original_value -- restore the original value so that the supplied instance table is not modified
local double_bonus = is_bonus_plural and (bonus >= 0 and '+' or '-') or ''
bonus = (bonus ~= 0 and double_bonus..format('%+d', bonus)) or ''
local double_reroll = is_reroll_plural and (rerolls >= 0 and '+' or '-') or ''
rerolls = (rerolls ~= 0 and '^'..double_reroll..format('%+d', rerolls)) or ''
if sets == 1 then return num_dice..'d'..dice_faces..bonus..rerolls
else return '('..num_dice..'d'..dice_faces..bonus..rerolls..')x'..sets
end
end
--- Sets dice minimum for instance
--@tparam[opt] int min Sets dice instance minimum result boundaries (if nil, no minimum result)
function dice:setMin(min) return dice:new(self.notation, min) end
--- Modifies bonus
-- @tparam int value
-- @treturn dice
function dice:__add(value) return dice:new(modifyNotation(self, 'bonus', value), self.minimum) end
--- Modifies bonus
-- @tparam int value
-- @treturn dice
function dice:__sub(value) return dice:new(modifyNotation(self, 'bonus', -value), self.minimum) end
--- Modifies number of dice
-- @tparam int value
-- @treturn dice
function dice:__mul(value) return dice:new(modifyNotation(self, 'num', value), self.minimum) end
--- Modifies amount of dice faces
-- @tparam int value
-- @treturn dice
function dice:__div(value) return dice:new(modifyNotation(self, 'faces', value), self.minimum) end
--- Modifies rerolls
-- @tparam int value
-- @treturn dice
function dice:__pow(value) return dice:new(modifyNotation(self, 'rerolls', value), self.minimum) end
--- Modifies dice sets
-- @tparam int value
-- @treturn dice
function dice:__mod(value) return dice:new(modifyNotation(self, 'sets', value), self.minimum) end
--- Gets a formatted dice string in roguelike notation
-- @treturn string
function dice:__tostring()
if self.faces > 0 and self.sets > 0 and self.num > 0 then
return self.notation
else
local num_dice, dice_faces, bonus, is_bonus_plural, rerolls, is_reroll_plural, sets, minimum = self.num, self.faces, self.bonus, self.is_bonus_plural, self.rerolls, self.is_reroll_plural, self.sets, self.minimum
--num_dice & dice_faces default to 1 if negative or 0!
sets, num_dice, dice_faces = max(sets, 1), max(num_dice, 1), max(dice_faces, 1)
local double_bonus = is_bonus_plural and (bonus >= 0 and '+' or '-') or ''
bonus = (bonus ~= 0 and double_bonus..format('%+d', bonus)) or ''
local double_reroll = is_reroll_plural and (rerolls >= 0 and '+' or '-') or ''
rerolls = (rerolls ~= 0 and '^'..double_reroll..format('%+d', rerolls)) or ''
if sets > 1 then return '('..num_dice..'d'..dice_faces..bonus..rerolls..')x'..sets
else return num_dice..'d'..dice_faces..bonus..rerolls
end
end
end
--- Modifies whether reroll or bonus applies to individual dice or all of them
-- @tparam str pluralism_notation String must be one of the following operators `- + ^` The operator may be double signed to indicate pluralism.
-- @treturn dice
function dice:__concat(pluralism_notation)
local str_b = match(pluralism_notation, '[+-][+-]?') or ''
local bonus_indicator = ((str_b == '++' or str_b == '--') and 'double') or ((str_b == '+' or str_b == '-') and 'single') or nil
local str_r = match(pluralism_notation, '[%^][%^]?') or ''
local reroll_indicator = (str_r == '^^' and 'double') or (str_r == '^' and 'single') or nil
local num_dice, dice_faces, bonus, is_bonus_plural, rerolls, is_reroll_plural, sets, minimum = self.num, self.faces, self.bonus, self.is_bonus_plural, self.rerolls, self.is_reroll_plural, self.sets, self.minimum
if bonus_indicator == 'double' then is_bonus_plural = true
elseif bonus_indicator == 'single' then is_bonus_plural = false end
if reroll_indicator == 'double' then is_reroll_plural = true
elseif reroll_indicator == 'single' then is_reroll_plural = false end
local double_bonus = is_bonus_plural and (bonus >= 0 and '+' or '-') or ''
bonus = (bonus ~= 0 and double_bonus..format('%+d', bonus)) or ''
local double_reroll = is_reroll_plural and (rerolls >= 0 and '+' or '-') or ''
rerolls = (rerolls ~= 0 and '^'..double_reroll..format('%+d', rerolls)) or ''
local new_dice_notation
if sets == 1 then new_dice_notation = num_dice..'d'..dice_faces..bonus..rerolls
else new_dice_notation = '('..num_dice..'d'..dice_faces..bonus..rerolls..')x'..sets
end
return dice:new(new_dice_notation, minimum)
end
--- Rolls the dice
-- @tparam ?int|dice|str self
-- @tparam[opt] int minimum
function dice.roll(self, minimum)
if type(self) ~= 'table' then self = dice:new(self, minimum) end
local num_dice, dice_faces = self.num, self.faces
local bonus, rerolls = self.bonus, self.rerolls
local is_bonus_plural, is_reroll_plural = self.is_bonus_plural, self.is_reroll_plural
local sets, minimum = self.sets, self.minimum or dice.class_minimum
sets = max(sets, 1) -- Minimum of 1 needed
local set_rolls = {}
local bonus_all = is_bonus_plural and bonus or 0
rerolls = is_reroll_plural and rerolls*num_dice or rerolls
-- num_dice & dice_faces CANNOT be negative!
num_dice, dice_faces = max(num_dice, 1), max(dice_faces, 1)
for i=1, sets do
local rolls = {}
for ii=1, num_dice + abs(rerolls) do
rolls[ii] = random(1, dice_faces) + bonus_all -- if is_bonus_plural then bonus_all gets added to every roll, otherwise bonus_all = 0
end
if rerolls ~= 0 then
-- sort and if reroll is + then remove lowest rolls, if reroll is - then remove highest rolls
if rerolls > 0 then sort(rolls, function(a,b) return a>b end) else sort(rolls) end
for index=num_dice + 1, #rolls do rolls[index] = nil end
end
-- bonus gets added to the last roll if it is not plural
if not is_bonus_plural then rolls[#rolls] = rolls[#rolls] + bonus end
local total = 0
for _, number in ipairs(rolls) do total = total + number end
set_rolls[i] = total
end
if minimum then
for i=1, sets do
set_rolls[i] = max(set_rolls[i], minimum)
end
end
return unpack(set_rolls)
end
return dice