-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathezdice.php
301 lines (264 loc) · 9.69 KB
/
ezdice.php
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
293
294
295
296
297
298
299
300
301
<?php
namespace ezdice;
/**
* EZDice, a dice rolling library
*/
class EZDice {
// Magic dice & modifier matching regex
private const REGEX_DICE = '/(?<operator>[\+-]?)\s*(?:(?:(?<number>\d+)*[dD](?<type>(?:\d+|[%fF]))(?:-(?<drop>[LlHh])(?<dropAmount>\d+)?)?)|(?<mod>[0-9]\d*))/';
private const REGEX_DICE_SINGLE = '/(?<number>\d+)*[dD](?<type>(?:[1-9]\d*|[%fF]))/';
// Stores information on last roll
private $total = 0;
private $states = [];
private $modifier = 0;
private $diceGroupNumber = 0;
private $diceModsNumber = 0;
/**
* Get the state of the dice after the last roll.
*
* @return array array that describes the state of the dice after the last roll.
*/
public function getDiceStates(): array
{
return $this->states;
}
/**
* Get the number of dice groups in the last roll.
*
* @return int the number of dice groups in the last roll.
*/
public function getDiceGroupNumber(): int
{
return $this->diceGroupNumber;
}
/**
* Get the number of modifiers in the last roll.
*
* @return int the number of modifiers in the last roll.
*/
public function getDiceModsNumber(): int
{
return $this->diceModsNumber;
}
/**
* Get the combined modifiers of the last roll.
*
* @return string representing the total of all modifiers in the last roll. If there were no modifiers, or they
* cancelled out, an empty string is returned.
*/
public function getModifier(): string
{
if (!$this->modifier) return "";
return sprintf("%+d", $this->modifier);
}
/**
* Get the total of the last roll.
*
* @return bool result of test.
*/
public function getTotal(): int
{
return $this->total;
}
/**
* Parse **$diceStr** as dice notation, then roll those dice.
*
* The parser is very forgiving, ignoring whitespace and anything else it doesn't recognise.
* It is also case-insensitive. Dice notation is documented in README.md
*
* @param string $diceStr the string containing the dice rolls.
*
* @return int|false total of all rolls and modifiers, or false if none were found.
*/
public function roll(string $diceStr): int|false
{
$this->resetResultValues();
// No dice to roll?
if (is_numeric($diceStr)) {
$this->total = (int)$diceStr;
$this->modifier = $this->total;
$this->diceModsNumber = 1;
return $this->total;
}
// Search for dice groups and modifiers
preg_match_all(self::REGEX_DICE, $diceStr, $matches, PREG_SET_ORDER, 0);
// Returning false if no matches found
if (sizeof($matches) == 0) return false;
// Process each match
foreach ($matches as $m) {
$this->processGroup($m);
}
// No dice were rolled and no modifiers were found
if ($this->diceModsNumber == 0 && $this->diceGroupNumber == 0) {
return false;
}
return $this->total;
}
/** Convenience function that ensures **$diceStr** is strictly valid before rolling it.
*
* @param string $diceStr the string containing the dice rolls.
* @param bool $allowWhitespace whether the string can contain whitespace. Default is true.
* @param bool $mustContainDice whether the string must contain at least 1 die. Default is true.
*
* @return int|false total of all rolls and modifiers, or false if none were found, or the string was invalid.
*/
public function rollStrict(string $diceStr, bool $allowWhitespace = true, bool $mustContainDice = true): int|false
{
if (!$this->strIsStrictlyDice($diceStr, $allowWhitespace, $mustContainDice)) {
$this->resetResultValues();
return false;
}
return $this->roll($diceStr);
}
/**
* Parse **$diceStr** and determine if it contains at least one dice roll.
*
* @param string $diceStr the string which may contain dice rolls.
*
* @return bool true if $diceStr contains at least one dice roll, otherwise false.
*/
public function strContainsDice(string $diceStr): bool
{
preg_match_all(self::REGEX_DICE_SINGLE, $diceStr, $matches, PREG_SET_ORDER, 0);
foreach ($matches as $m) {
// Check for valid dice notation
if ((!is_numeric($m['type']) || (int)$m['type'] > 0) && ($m['number'] === "" || (int)$m['number'] > 0)) {
return true;
}
}
return false;
}
/**
* Parse **$diceStr** and determine if it only contains dice and modifiers.
* Whitespace is allowed by default, but strings containing only whitespace will always return false.
*
* @param string $diceStr the string which may contain dice rolls.
* @param bool $allowWhitespace whether the string can contain whitespace. Default is true.
* @param bool $mustContainDice whether the string must contain at least 1 die. Default is true.
*
* @return bool true if $diceStr contains dice, modifiers or whitespace, otherwise false.
*/
public function strIsStrictlyDice(string $diceStr, bool $allowWhitespace = true, bool $mustContainDice = true): bool
{
// Remove whitespace
$diceStr = preg_replace("/\s+/", "", $diceStr, -1, $count);
if ($diceStr == "") return false;
if (!$allowWhitespace && $count > 0) return false;
// Check for invalid dice groups, get get dice count
$diceCount = $this->countAndValidateDiceGroups($diceStr);
if ($diceCount === false || ($mustContainDice && $diceCount == 0)) {
return false;
}
// Remove anything that's a dice or modifier, if there's anything left then it's not strictly dice
$diceStr = preg_replace(self::REGEX_DICE, "", $diceStr);
return $diceStr == "";
}
private function addState(mixed $type, int $value, bool $isNegative, bool $dropped = false): void
{
// Fudge dice have 3 sides
$sides = ($type == 'F' ? 3 : $type);
$this->states[] = [
'sides' => $sides,
'value' => $value,
'dropped' => $dropped,
'negative' => $isNegative,
'type' => "d$type",
'group' => $this->diceGroupNumber,
];
}
private function countAndValidateDiceGroups(string $diceStr): int|false
{
// Search for dice groups and modifiers
preg_match_all(self::REGEX_DICE_SINGLE, $diceStr, $matches, PREG_SET_ORDER, 0);
$groupCount = 0;
// Process each match
foreach ($matches as $m) {
// Check for valid dice notation
if ((!is_numeric($m['type']) || $m['type'] > 0) && ($m['number'] === "" || $m['number'] > 0)) {
$groupCount++;
} else {
return false;
}
}
return $groupCount;
}
private function processGroup(array $group): void
{
// Scaler makes the output positive or negative
$isNegative = ($group['operator'] == '-');
$scaler = ($isNegative ? -1 : 1);
// Modifiers (not dice)
if (isset($group['mod'])) {
$this->total += $group['mod']*$scaler;
$this->modifier += $group['mod']*$scaler;
$this->diceModsNumber++;
return;
}
// Check for zero sized groups, or zero sided dice
if ($group['number'] == 0 || $group['type'] == 0) {
return;
}
$this->diceGroupNumber++;
// Collect information about dice
$number = $group['number'] ? $group['number'] : 1;
$type = $group['type'];
// Collect drop information
$drop = (isset($group['drop']) ? strtoupper($group['drop']) : null);
// 'd%' can be used as shorthand for 'd100'
if ($type == "%") {
$type = 100;
} elseif ($type == "f") { // Fate dice needing capitalisation
$type = "F";
}
// Roll Dice
$results = [];
// Special case for Fudge dice
if ($type == "F") {
for ($i = 0; $i < $number; $i++)
$results[] = $this->getRandomNumber(3) - 2;
} else {
for ($i = 0; $i < $number; $i++)
$results[] = $this->getRandomNumber($type);
}
// Dropping dice
if ($drop) {
// Dropping low, so sort descending
if ($drop == 'L') {
rsort($results, SORT_NUMERIC);
} else { // Dropping high, so sort ascending
sort($results, SORT_NUMERIC);
}
$dropQuantity = min($group['dropAmount'] ?? 1, $number);
for ($i=0; $i < $dropQuantity; $i++) {
$droppedResult = array_pop($results);
$this->addState($type, $droppedResult, $isNegative, true);
}
// Cosmetic re-shuffle of rest of dice
shuffle($results);
}
// Process the rest of the dice
foreach($results as $result) {
$this->total += $result*$scaler;
$this->addState($type, $result, $isNegative);
}
}
private function resetResultValues(): void
{
$this->total = 0;
$this->states = [];
$this->modifier = 0;
$this->diceGroupNumber = 0;
$this->diceModsNumber = 0;
}
/**
* Generates the pseudo-random number for dice rolls.
*
* @param int $max the highest number on the dice. Roll is 1 - $max (inclusive).
*
* @return int result of the roll.
*/
protected function getRandomNumber(int $max): int
{
return mt_rand(1, $max);
}
}