├── .gitignore
├── LICENSE
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── diceRoller.ts
├── diceroll.pegjs
├── discordRollRenderer.ts
├── index.ts
├── parsedRollTypes.ts
├── rollTypes.ts
└── utilityTypes.ts
├── test
├── parser_test.js
├── rendererTest.test.ts
└── rollerTest.test.ts
├── tsconfig.json
├── tslint.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | dist/
40 |
41 | index.js
42 | src/diceroll.js
43 |
44 | .rpt2_cache
45 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Ben Morton
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dice Roller & Parser
2 |
3 | This dice roller is a string parser that returns an object containing the component parts of the dice roll. It supports the full [Roll20 Dice Specification](https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference#DiceReference-Roll20DiceSpecification). It uses a [pegjs](https://github.com/pegjs/pegjs) grammar to create a [representation of the dice roll format](#parsed-roll-output). This can then be converted into a simple number value, or to a [complex object](#roll-result-output) used to display the full roll details.
4 |
5 | ## Credit
6 | This is a fork of Ben Morton's [dice_roller](https://github.com/BTMorton/dice_roller) project. It had a few bugs so I'm republishing with fixes and features needed for my projects.
7 |
8 | ## Quickstart
9 |
10 | Install the library using:
11 |
12 | ```bash
13 | npm install dice-roller-parser
14 | ```
15 |
16 | Once installed, simply load the library, either in the browser:
17 |
18 | ```html
19 |
20 | ```
21 |
22 | Or in node:
23 |
24 | ```javascript
25 | import { DiceRoller } from "dice-roller-parser";
26 | ```
27 |
28 | Then create a new instance of the [`DiceRoller`](#DiceRoller) class, and use it to perform some dice rolls.
29 |
30 | ```javascript
31 | const diceRoller = new DiceRoller();
32 |
33 | // Returns the total rolled value
34 | const roll = diceRoller.rollValue("2d20kh1");
35 | console.log(roll);
36 |
37 | // Returns an object representing the dice roll, use to display the component parts of the roll
38 | const rollObject = diceRoller.roll("2d20kh1");
39 | console.log(rollObject.value);
40 | ```
41 |
42 | ## Usage
43 |
44 | This library exposes two classes, a [`DiceRoller`](#DiceRoller) and a [`DiscordRollRenderer`](#DiscordRollRenderer).
45 |
46 | ### `DiceRoller`
47 |
48 | The `DiceRoller` class manages parsing of a dice string and performing rolls based upon the result.
49 |
50 | ```javascript
51 | // Creates a new instance of the DiceRoller class
52 | const roller = new DiceRoller();
53 | ```
54 |
55 | #### Constructor options
56 |
57 | The default constructor uses `Math.random` and applies a maximum number of rolls per die of 1000. These can be specified using the following constructor overloads.
58 |
59 | ##### `DiceRoller(GeneratorFunction)`
60 |
61 | You can specify a function to be used as the random number generator by the dice roller. This function should be of the type `() => number` and return a number between 0 and 1. By default, it uses the built-in `Math.random` method.
62 |
63 | ```javascript
64 | // Default constructor using Math.random
65 | const roller = new DiceRoller();
66 |
67 | // Uses a custom random generator that always returns 0.5
68 | const roller = new DiceRoller(() => 0.5);
69 | ```
70 |
71 | This can be read or modified using the `randFunction` property.
72 |
73 | ```javascript
74 | roller.randFunction = () => 0.5;
75 | ```
76 |
77 | ##### `DiceRoller(GeneratorFunction, MaxRollsPerDie)`
78 |
79 | To prevent attempting to parse very large numbers of die rolls, a maximum number of rolls for a die can be specified. The default value is set to 1000.
80 |
81 | ```javascript
82 | // Uses the default constructor with a limit of 100 rolls per die
83 | const roller = new DiceRoller(null, 100);
84 |
85 | // Uses a custom random generator that always returns 0.5, and a limit of 10 rolls per die
86 | const roller = new DiceRoller(() => 0.5, 10);
87 | ```
88 |
89 | This can be read or modified using the `maxRollCount` property.
90 |
91 | ```javascript
92 | roller.maxRollCount = 75;
93 | ```
94 |
95 | #### Class Usage
96 |
97 | Once the `DiceRoller` class has been constructed, there are three options for performing a dice roll:
98 |
99 | - Getting a roll result directly
100 | - Generate an object to represent the dice roll
101 | - Just parse the input and add your own rolling logic
102 |
103 | ##### Getting a direct roll result
104 |
105 | The `rollValue` method takes a dice string input, parses it, performs a roll and returns the calculated number value result.
106 |
107 | ```javascript
108 | // Rolls 2 d20 dice and keeps the value of the highest
109 | const roll = roller.rollValue("2d20kh1");
110 |
111 | // Prints out the numeric value result
112 | console.log(roll);
113 | ```
114 |
115 | ##### Generate an object representing the dice roll
116 |
117 | The `roll` method takes a dice string input, parses it, performs a roll and then returns an object that represents the roll. Using the roll objects, you can build your own roll display functionality, rather than just outputting the final value.
118 |
119 | ```javascript
120 | // Rolls 2 d20 dice and keeps the value of the highest
121 | const roll = roller.roll("2d20kh1");
122 |
123 | // Print out the full roll breakdown
124 | printDiceRoll(roll);
125 | // Prints out the numeric value result
126 | console.log(`Final roll value: ${roll.Value}`);
127 | ```
128 |
129 | See the [roll result output](#roll-result-output) in the [output types](#output-types) section below for more details on the returned object.
130 |
131 | ##### Just parse the value
132 |
133 | The `parse` method takes a dice string input, parses it and returns a representation of the parsed input. This can either be used to perform a dice roll or re-construct the original input. The `rollParsed` method takes this parsed result as an input, performs the roll and returns the same output as from the [`roll`](#generate-an-object-representing-the-dice-roll) method.
134 |
135 | ```javascript
136 | // Rolls 2 d20 dice and keeps the value of the highest
137 | const parsedInput = roller.parse("2d20kh1");
138 |
139 | // Print out a re-constructed input string
140 | printParsedInput(parsedInput);
141 |
142 | // Run the roller on the parsed object
143 | const roll = roller.rollParsed(parsedInput);
144 |
145 | // Print out the full roll breakdown
146 | printDiceRoll(roll);
147 | // Print out the numeric value result
148 | console.log(`Final roll value: ${roll.Value}`);
149 | ```
150 |
151 | See the [parsed roll output](#parsed-roll-output) in the [output types](#output-types) section below for more details on the returned object.
152 |
153 | ### `DiscordRollRenderer`
154 |
155 | The `DiscordRollRenderer` class is an example renderer class that takes a rolled input represented by a [`RollBase`](#RollBase) object and renders it to a string in a markdown format, compatible with Discord.
156 |
157 | ```javascript
158 | // Creates a new instance of the DiceRoller class
159 | const renderer = new DiscordRollRenderer();
160 | ```
161 |
162 | #### Class Usage
163 |
164 | The `DiscordRollRenderer` exposes a single `render` method with a single parameter, the [`RollBase`](#RollBase) object to render, and returns the rendered string.
165 |
166 | ```javascript
167 | // Rolls 2 d20 dice and keeps the value of the highest
168 | const roll = roller.rollValue("2d20kh1");
169 |
170 | // Get the formatted string
171 | const render = renderer.render(roll);
172 | console.log(render);
173 | ```
174 |
175 | ## Development
176 |
177 | To develop this library, simply clone the repository, run an install:
178 |
179 | ```bash
180 | npm install
181 | ```
182 |
183 | Then do a build:
184 |
185 | ```bash
186 | npm run build
187 | ```
188 |
189 | This does four things:
190 |
191 | ```bash
192 | # Clean any existing builds
193 | npm run clean
194 |
195 | # Build the dice grammer
196 | npx pegjs src/diceroll.pegjs
197 |
198 | # Run tslint against the project
199 | tslint -c tslint.json --project tsconfig.json
200 |
201 | # Then run webpack to build and package everything up nicely
202 | webpack
203 | ```
204 |
205 | To run the test suite, use:
206 |
207 | ```bash
208 | npm run test
209 | ```
210 |
211 | That's all there is to it!
212 |
213 | ## Output Types
214 |
215 | The following object types are output from the [`DiceRoller`](#DiceRoller) class, and are available as interfaces for typescript users.
216 |
217 | ### Roll Result Output
218 |
219 | The object returned by a roll result is made up of the following types.
220 |
221 | #### `RollBase`
222 |
223 | The base class for all die rolls, extended based upon the type property.
224 |
225 | | Property | Type | Description |
226 | | -------- | ----------------------- | ------------------------------------------------------------------- |
227 | | success | `boolean` | Was the roll a success, for target number rolls. Example: `3d6 > 3` |
228 | | type | [`RollType`](#RollType) | The type of roll that this object represents. |
229 | | valid | `boolean` | Is the roll still valid, and included in calculations. |
230 | | value | `number` | The rolled or calculated value of this roll. |
231 | | label | `string` | The display label for this roll. This property is optional. |
232 | | order | `number` | A property used to maintain ordering of dice rolls within groups. |
233 |
234 | #### `RollType`
235 |
236 | An enum of the valid types of roll. The possible values are:
237 |
238 | - `"number"`
239 | - [`"diceexpressionroll"`](#DiceExpressionRoll)
240 | - [`"expressionroll"`](#ExpressionRoll)
241 | - [`"mathfunction"`](#MathFunctionRoll)
242 | - [`"grouproll"`](#GroupRoll)
243 | - `"fate"`
244 | - [`"die"`](#DiceRollResult)
245 | - [`"roll"`](#DieRoll)
246 | - [`"fateroll"`](#FateDieRoll)
247 |
248 | #### `GroupedRoll`
249 |
250 | An intermediate interface extended for groups of dice. This interface extends [`RollBase`](#RollBase).
251 |
252 | | Property | Type | Description |
253 | | -------- | ---------------------------------- | ----------------------------------------- |
254 | | dice | `Array<`[`RollBase`](#RollBase)`>` | The rolls included as part of this group. |
255 |
256 | #### `DiceExpressionRoll`
257 |
258 | A representation of a dice expression. This interface extends [`GroupedRoll`](#GroupedRoll).
259 |
260 | **Example**
261 |
262 | > `2d20 + 6d6`
263 |
264 | | Property | Type | Description |
265 | | -------- | -------------------------------------------------------------- | --------------------------------------------- |
266 | | type | `"diceexpressionroll"` | The type of roll that this object represents. |
267 | | ops | `Array<`[`DiceGroupMathOperation`](#DiceGroupMathOperation)`>` | The operations to perform on the rolls. |
268 |
269 | #### `ExpressionRoll`
270 |
271 | A representation of a mathematic expression. This interface extends [`GroupedRoll`](#GroupedRoll).
272 |
273 | **Example**
274 |
275 | > `20 * 17`
276 |
277 | | Property | Type | Description |
278 | | -------- | -------------------------------------------- | --------------------------------------------- |
279 | | type | `"expressionroll"` | The type of roll that this object represents. |
280 | | ops | `Array<`[`MathOperation`](#MathOperation)`>` | The operations to perform on the rolls. |
281 |
282 | #### `MathFunctionRoll`
283 |
284 | A representation of a mathematic function. This interface extends [`RollBase`](#RollBase).
285 |
286 | **Example**
287 |
288 | > `floor(20 / 17)`
289 |
290 | | Property | Type | Description |
291 | | -------- | ------------------------------- | ------------------------------------------------- |
292 | | type | `"expressionfunc"` | The type of roll that this object represents. |
293 | | op | [`MathFunction`](#MathFunction) | The operations to perform on the rolls. |
294 | | expr | [`RollBase`](#RollBase) | The expression that the function is applied upon. |
295 |
296 | #### `GroupRoll`
297 |
298 | A representation of a group of rolls
299 |
300 | **Example**
301 |
302 | > {4d6,3d6}. This interface extends [`GroupedRoll`](#GroupedRoll).
303 |
304 | | Property | Type | Description |
305 | | -------- | ------------- | --------------------------------------------- |
306 | | type | `"grouproll"` | The type of roll that this object represents. |
307 |
308 | #### `DiceRollResult`
309 |
310 | The rolled result of a group of dice. This interface extends [`RollBase`](#RollBase).
311 |
312 | **Example**
313 |
314 | > `6d20`
315 |
316 | | Property | Type | Description |
317 | | -------- | ------------------------------- | --------------------------------------------- |
318 | | die | [`RollBase`](#RollBase) | The die this result represents. |
319 | | type | `"die"` | The type of roll that this object represents. |
320 | | rolls | [`DieRollBase`](#DieRollBase)[] | Each roll of the die. |
321 | | count | [`RollBase`](#RollBase) | The number of rolls of the die. |
322 | | matched | `boolean` | Whether this is a match result. |
323 |
324 | #### `DieRollBase`
325 |
326 | An intermediate interface extended for individual die rolls (see below). This interface extends [`RollBase`](#RollBase).
327 |
328 | | Property | Type | Description |
329 | | -------- | --------- | ----------------------------- |
330 | | roll | `number` | The rolled result of the die. |
331 | | matched | `boolean` | Whether this roll is a match. |
332 |
333 | #### `DieRoll`
334 |
335 | A roll on a regular die. This interface extends [`DieRollBase`](#DieRollBase).
336 |
337 | **Example**
338 |
339 | > `d20`
340 |
341 | | Property | Type | Description |
342 | | -------- | ------------------------------- | -------------------------------------------------------------- |
343 | | die | `number` | The die number to be rolled. |
344 | | type | `"roll"` | The type of roll that this object represents. |
345 | | critical | [`CriticalType`](#CriticalType) | If this role is a critical success or failure (for rendering). |
346 |
347 | #### `FateDieRoll`
348 |
349 | A roll on a fate die. This interface extends [`DieRollBase`](#DieRollBase).
350 |
351 | **Example**
352 |
353 | > `dF`
354 |
355 | | Property | Type | Description |
356 | | -------- | ------------ | --------------------------------------------- |
357 | | type | `"fateroll"` | The type of roll that this object represents. |
358 |
359 | ### Parsed Roll Output
360 |
361 | The following interfaces are exposed by the library as a reresentation of the parsed input string. The response from the `parse` method is a `RootType` object and could be any of the interfaces that extend it.
362 |
363 | #### `ParsedObjectType`
364 |
365 | An enum of the valid types of roll. The possible values are:
366 |
367 | - [`"number"`](#NumberType)
368 | - [`"inline"`](#InlineExpression)
369 | - [`"success"`](#SuccessFailureCritModType)
370 | - [`"failure"`](#SuccessFailureCritModType)
371 | - [`"crit"`](#SuccessFailureCritModType)
372 | - [`"critfail"`](#SuccessFailureCritModType)
373 | - [`"match"`](#MatchModType)
374 | - [`"keep"`](#KeepDropModType)
375 | - [`"drop"`](#KeepDropModType)
376 | - [`"group"`](#GroupedRoll)
377 | - [`"diceExpression"`](#RollExpressionType)
378 | - [`"sort"`](#SortRollType)
379 | - [`"explode"`](#ReRollMod)
380 | - [`"compound"`](#ReRollMod)
381 | - [`"penetrate"`](#ReRollMod)
382 | - [`"reroll"`](#ReRollMod)
383 | - [`"rerollOnce"`](#ReRollMod)
384 | - [`"target"`](#TargetMod)
385 | - [`"die"`](#DiceRoll)
386 | - [`"fate"`](#FateExpr)
387 | - [`"expression"`](#MathExpression)
388 | - [`"math"`](#MathType)
389 | - [`"mathfunction"`](#MathFunctionExpression)
390 |
391 | #### `ParsedType`
392 |
393 | This is the base interface for all parsed types.
394 |
395 | | Property | Type | Description |
396 | | -------- | -------- | ----------------------------------------------- |
397 | | type | `string` | The type of parsed item this object represents. |
398 |
399 | #### `RootType`
400 |
401 | This is the base interface for a subset of parsed types, only those that can be the root type. This object extends the [`ParsedType`](#ParsedType) interface.
402 |
403 | | Property | Type | Description |
404 | | -------- | --------- | ----------------------------------------------------------------- |
405 | | label? | `string` | The text label attached to this roll. This property is optional. |
406 | | root | `boolean` | A boolean flag to indicate if this is the root of the parse tree. |
407 |
408 | #### `NumberType`
409 |
410 | This object represents a single number in the input. This object extends the [`RootType`](#RootType) interface.
411 |
412 | | Property | Type | Description |
413 | | -------- | ---------- | ----------------------------------------------- |
414 | | type | `"number"` | The type of parsed item this object represents. |
415 | | value | `number` | The value of the number. |
416 |
417 | #### `InlineExpression`
418 |
419 | This object represents an inline dice expression within a string, wrapped in double square brackets. This object extends the [`RootType`](#RootType) interface.
420 |
421 | **Example**
422 |
423 | > `I want to roll [[2d20]] dice`
424 |
425 | | Property | Type | Description |
426 | | -------- | --------------------------- | ---------------------------------------------------- |
427 | | type | `"inline"` | The type of parsed item this object represents. |
428 | | expr | [`Expression`](#Expression) | The expression that was parsed as the inline string. |
429 |
430 | #### `AnyRoll`
431 |
432 | A combined type representing any valid roll. This is a combination of the following types:
433 |
434 | - [`GroupedRoll`](#GroupedRoll)
435 | - [`FullRoll`](#FullRoll)
436 | - [`NumberType`](#NumberType)
437 |
438 | #### `ModGroupedRoll`
439 |
440 | This object represents a grouped roll with an optional modifier. This object extends the [`RootType`](#RootType) interface.
441 |
442 | **Example**
443 |
444 | > `{4d6+3d8}kh1`
445 |
446 | | Property | Type | Description |
447 | | -------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
448 | | mods | `Array<`[`KeepDropModType`](#KeepDropModType)`,`[`SuccessFailureModType`](#SuccessFailureModType)`>` | The modifiers to be applied to the grouped roll. |
449 |
450 | #### `SuccessFailureCritModType`
451 |
452 | An object representing a success test modifier. This object extends the [`ParsedType`](#ParsedType) interface.
453 | A `"success"` or `"failure"` modifier converts the result into a success type result which returns the number of rolls that meet the target.
454 | A `"crit"` or `"critfail"` modifier tests the roll for whether or not the roll should be displayed as a critical success or critical failure.
455 |
456 | **Example**
457 |
458 | > Success: `3d6>3`
459 | > Failure: `3d6f<3`
460 |
461 | | Property | Type | Description |
462 | | -------- | ------------------------------------------ | ------------------------------------------------- |
463 | | type | `"success", "failure", "crit", "critfail"` | The type of parsed item this object represents. |
464 | | mod | [`CompareOperation`](#CompareOperation) | The check type to use for the condition. |
465 | | expr | [`RollExpression`](#RollExpression) | An expression representing the success condition. |
466 |
467 | #### `SuccessFailureModType`
468 |
469 | Equivalent to the [`SuccessFailureCritModType`](#SuccessFailureCritModType) but only supporting "success" and "failure" modifiers. This object extends the [`SuccessFailureCritModType`](#SuccessFailureCritModType) interface.
470 |
471 | **Example**
472 |
473 | > Success: `3d6>3`
474 | > Failure: `3d6f<3`
475 |
476 | | Property | Type | Description |
477 | | -------- | --------------------------------------- | ------------------------------------------------- |
478 | | type | `"success", "failure"` | The type of parsed item this object represents. |
479 | | mod | [`CompareOperation`](#CompareOperation) | The check type to use for the condition. |
480 | | expr | [`RollExpression`](#RollExpression) | An expression representing the success condition. |
481 |
482 | #### `MatchModType`
483 |
484 | An object representing a match type modifier, used to modify the display of dice output in roll20. This object extends the [`ParsedType`](#ParsedType) interface.
485 |
486 | **Example**
487 |
488 | > `2d6m`
489 |
490 | When used with the `mt` extension, will return the number of matches found.
491 |
492 | **Example**
493 |
494 | > `20d6mt`
495 |
496 | Additional arguments can be specified that increase the required number of matches or to add a constraint to matches.
497 |
498 | **Example**
499 |
500 | > `20d6mt3 counts matches of 3 items`
501 |
502 | **Example**
503 |
504 | > `20d6m>3 Only counts matches where the rolled value is > 3`
505 |
506 | | Property | Type | Description |
507 | | -------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ |
508 | | type | `"match"` | The type of parsed item this object represents. |
509 | | min | [`NumberType`](#NumberType) | The minimum number of matches to accept. This property defaults to 2 as a [`NumberType`](#NumberType). |
510 | | count | `boolean` | Whether or not to count the matches. |
511 | | mod? | [`CompareOperation`](#CompareOperation) | The check type to use for the match condition, if specified. This field is optional. |
512 | | expr? | [`RollExpression`](#RollExpression) | An expression representing the match condition, if specified. This field is optional. |
513 |
514 | #### `KeepDropModType`
515 |
516 | An object representing a keep or drop modifier, specifying a number of dice rolls to keep or drop, either the highest or lowest rolls. This object extends the [`ParsedType`](#ParsedType) interface.
517 |
518 | **Example**
519 |
520 | > Keep: `2d20kh1`
521 | > Drop: `2d20dl1`
522 |
523 | | Property | Type | Description |
524 | | -------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
525 | | type | `"keep", "drop"` | The type of parsed item this object represents. |
526 | | highlow | [`HighLowType`](#HighLowType) | Whether to keep/drop the highest or lowest roll. |
527 | | expr | [`RollExpression`](#RollExpression) | An expression representing the number of rolls to keep/drop. This property defaults to 1 as a [`NumberType`](#NumberType). Example: `2d6` |
528 |
529 | #### `GroupedRoll`
530 |
531 | This object represents a group of rolls combined, with optional modifiers. This object extends the [`ModGroupedRoll`](#ModGroupedRoll) interface.
532 |
533 | **Example**
534 |
535 | > `{2d6,3d6}`
536 |
537 | | Property | Type | Description |
538 | | -------- | ---------------------------------------------- | ----------------------------------------------- |
539 | | type | `"group"` | The type of parsed item this object represents. |
540 | | rolls | `Array<`[`RollExpression`](#RollExpression)`>` | The group of rolls included in this group. |
541 |
542 | #### `RollExpressionType`
543 |
544 | An object representing a roll expression including complex rolls and groups, only allows addition operations. This object extends the [`RootType`](#RootType) interface.
545 |
546 | **Example**
547 |
548 | > `{2d6,3d6}kh1 + {3d6 + 2d6}kh2`
549 |
550 | | Property | Type | Description |
551 | | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
552 | | head | [`RollOrExpression`](#RollOrExpression) | The initial roll or expression for the roll expression. |
553 | | type | `"diceExpression"` | The type of parsed item this object represents. |
554 | | ops | `Array<`[`MathType`](#MathType)`<`[`RollOrExpression`](#RollOrExpression)`,`[`DiceGroupMathOperation`](#DiceGroupMathOperation)`>>` | The operations to apply to the initial roll or expression. |
555 |
556 | #### `RollExpression`
557 |
558 | A helper type combination of a complex roll expression, a roll, or a math expression. Represents the following types:
559 |
560 | - [`RollExpressionType`](#RollExpressionType)
561 | - [`RollOrExpression`](#RollOrExpression)
562 |
563 | #### `RollOrExpression`
564 |
565 | A helper type combination of a roll, or a math expression. Represents the following types:
566 |
567 | - [`FullRoll`](#FullRoll)
568 | - [`Expression`](#Expression)
569 |
570 | #### `FullRoll`
571 |
572 | An object representing a roll including the dice roll, and any modifiers. This object extends the [`DiceRoll`](#DiceRoll) interface.
573 |
574 | **Example**
575 |
576 | > `2d6kh1`
577 |
578 | | Property | Type | Description |
579 | | -------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
580 | | mods? | `Array<`[`ReRollMod`](#ReRollMod)`,`[`KeepDropModType`](#KeepDropModType)`>` | Any modifiers attached to the roll. This property is optional. |
581 | | targets? | `Array<`[`SuccessFailureCritModType`](#SuccessFailureCritModType)`>` | Any success or failure targets for the roll. This property is optional. |
582 | | match? | [`MatchModTyp`](#MatchModTyp) | Any match modifiers for the roll. This property is optional. |
583 | | sort? | [`SortRollType`](#SortRollType) | Any sort operations to apply to the roll. This property is optional. |
584 |
585 | #### `SortRollType`
586 |
587 | A sort operation to apply to a roll. This object extends the [`ParsedType`](#ParsedType) interface.
588 |
589 | **Example**
590 |
591 | > `10d6sa`
592 |
593 | | Property | Type | Description |
594 | | -------- | --------- | ----------------------------------------------- |
595 | | type | `"sort"` | The type of parsed item this object represents. |
596 | | asc | `boolean` | Whether to sort ascending or descending. |
597 |
598 | #### `ReRollMod`
599 |
600 | An object representing a re-roll operation to apply to a roll. Can be one of the following types:
601 |
602 | - `"explode"`: re-rolls any dice that meet the target, continuing if the new roll matches
603 | - `"compound"`: re-rolls any dice that meet the target, continuing if the new roll matches and adding the results into a single roll
604 | - `"penetrate"`: re-rolls any dice that meet the target subtracting 1 from the new value, continuing if the new roll matches
605 | - `"reroll"`: re-rolls a die as long as it meets the target, keeping the final roll
606 | - `"rerollOnce"`: re-rolls a die once if it meets the target, keeping the new roll
607 |
608 | **Example**
609 |
610 | > `2d6!`
611 |
612 | | Property | Type | Description |
613 | | -------- | ------------------------------------------------------------ | ------------------------------------------------------ |
614 | | type | `"explode", "compound", "penetrate", "reroll", "rerollOnce"` | The type of parsed item this object represents. |
615 | | target | [`TargetMod`](#TargetMod) | The target modifier to compare the roll value against. |
616 |
617 | #### `TargetMod`
618 |
619 | An object represting a target modifier to apply to a roll. This object extends the [`ParsedType`](#ParsedType) interface.
620 |
621 | | Property | Type | Description |
622 | | -------- | --------------------------------------- | ------------------------------------------------------ |
623 | | type | `"target"` | The type of parsed item this object represents. |
624 | | mod | [`CompareOperation`](#CompareOperation) | The check type to use for the condition. |
625 | | value | [`RollExpr`](#RollExpr) | An expression representing the target condition value. |
626 |
627 | #### `DiceRoll`
628 |
629 | The representation of a die roll. This object extends the [`RootType`](#RootType) interface.
630 |
631 | **Example**
632 |
633 | > `2d6`
634 |
635 | | Property | Type | Description |
636 | | -------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
637 | | die | [`RollExpr`](#RollExpr)`,`[`FateExpr`](#FateExpr) | The die value to roll against, can be a fate die, a number or a complex roll expression. |
638 | | count | [`RollExpr`](#RollExpr) | The number of time to roll this die. |
639 | | type | `"die"` | The type of parsed item this object represents. |
640 |
641 | #### `FateExpr`
642 |
643 | The representation of a fate die roll. This object extends the [`ParsedType`](#ParsedType) interface.
644 |
645 | **Example**
646 |
647 | > `2dF`
648 |
649 | | Property | Type | Description |
650 | | -------- | -------- | ----------------------------------------------- |
651 | | type | `"fate"` | The type of parsed item this object represents. |
652 |
653 | #### `RollExpr`
654 |
655 | A helper type combination of a number or value that is not an expression. Represents the following types:
656 |
657 | - [`MathExpression`](#MathExpression)
658 | - [`NumberType`](#NumberType)
659 |
660 | #### `Expression`
661 |
662 | A helper type combination of expression types. Represents the following types:
663 |
664 | - [`InlineExpression`](#InlineExpression)
665 | - [`MathExpression`](#MathExpression)
666 |
667 | #### `MathExpression`
668 |
669 | A math type expression between two or more dice rolls. This object extends the [`RootType`](#RootType) interface.
670 |
671 | **Example**
672 |
673 | > `2d6 + 3d6 * 4d6`
674 |
675 | | Property | Type | Description |
676 | | -------- | ----------------------------------------------------------- | ----------------------------------------------- |
677 | | head | [`AnyRoll`](#AnyRoll) | The initial roll to perform operations against. |
678 | | type | `"expression"` | The type of parsed item this object represents. |
679 | | ops | `Array<`[`MathType`](#MathType)`<`[`AnyRoll`](#AnyRoll)`>>` | The operations to apply to the initial roll. |
680 |
681 | #### `MathType`
682 |
683 | An object representating an roll math operation to be applied and the value to apply it to. This object extends the [`ParsedType`](#ParsedType) interface.
684 | The interface for this object takes a templated type `TailType` which specifies the type of the second value used in the operation.
685 | There is a second templated type `OpValues` which specifies the type of operations that can be used. This defaults to `Array<`[`MathOperation`](#MathOperation)>`.
686 |
687 | **Example**
688 |
689 | > `+ 3d6 (as part of 2d6 + 3d6)`
690 |
691 | | Property | Type | Description |
692 | | -------- | ---------- | ----------------------------------------------- |
693 | | type | `"math"` | The type of parsed item this object represents. |
694 | | op | `OpValues` | The math operation to perform. |
695 | | tail | `TailType` | The second value to use in the operation. |
696 |
697 | #### `MathFunctionExpression`
698 |
699 | An object representing a math function to be applied and the expression to apply it to. This object extends the [`RootType`](#RootType) interface.
700 |
701 | **Example**
702 |
703 | > `floor(3d6 / 2d4)`
704 |
705 | | Property | Type | Description |
706 | | -------- | ------------------------------- | ----------------------------------------------- |
707 | | type | `"mathfunction"` | The type of parsed item this object represents. |
708 | | op | [`MathFunction`](#MathFunction) | The function to be applied. |
709 | | expr | [`AnyRoll`](#AnyRoll) | The expression to apply the function on. |
710 |
711 | ### Helper Types
712 |
713 | The following are support types used by the above interfaces.
714 |
715 | #### `DiceGroupMathOperation`
716 |
717 | A helper type representing the valid operations for a math operation on a group of dice.
718 |
719 | > `"+" | "-"`
720 |
721 | #### `MathOperation`
722 |
723 | A helper type representing the valid operations for a math operation.
724 |
725 | > `"+" | "-" | "*" | "/" | "%" | "**"`
726 |
727 | #### `MathFunction`
728 |
729 | A helper type representing the valid operations for a math operation.
730 |
731 | > `"floor" | "ceil" | "round" | "abs"`
732 |
733 | #### `CriticalType`
734 |
735 | A helper type used when marking a roll as a critical success or failure.
736 |
737 | > `"success" | "failure" | null`
738 |
739 | #### `CompareOperation`
740 |
741 | A helper type for the available operations for a comparison point.
742 |
743 | > `">" | "<" | "="`
744 |
745 | #### `HighLowType`
746 |
747 | A helper type used to determine which rolls to keep or drop.
748 |
749 | > `"h" | "l" | null`
750 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after `n` failures
9 | // bail: 0,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "C:\\Users\\Ben\\AppData\\Local\\Temp\\jest",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: null,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: "coverage",
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "\\\\node_modules\\\\"
32 | // ],
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: null,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: null,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // Force coverage collection from ignored files using an array of glob patterns
52 | // forceCoverageMatch: [],
53 |
54 | // A path to a module which exports an async function that is triggered once before all test suites
55 | // globalSetup: null,
56 |
57 | // A path to a module which exports an async function that is triggered once after all test suites
58 | // globalTeardown: null,
59 |
60 | // A set of global variables that need to be available in all test environments
61 | // globals: {},
62 |
63 | // An array of directory names to be searched recursively up from the requiring module's location
64 | // moduleDirectories: [
65 | // "node_modules"
66 | // ],
67 |
68 | // An array of file extensions your modules use
69 | // moduleFileExtensions: [
70 | // "js",
71 | // "json",
72 | // "jsx",
73 | // "ts",
74 | // "tsx",
75 | // "node"
76 | // ],
77 |
78 | // A map from regular expressions to module names that allow to stub out resources with a single module
79 | // moduleNameMapper: {},
80 |
81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
82 | // modulePathIgnorePatterns: [],
83 |
84 | // Activates notifications for test results
85 | // notify: false,
86 |
87 | // An enum that specifies notification mode. Requires { notify: true }
88 | // notifyMode: "failure-change",
89 |
90 | // A preset that is used as a base for Jest's configuration
91 | // preset: null,
92 |
93 | // Run tests from one or more projects
94 | // projects: null,
95 |
96 | // Use this configuration option to add custom reporters to Jest
97 | // reporters: undefined,
98 |
99 | // Automatically reset mock state between every test
100 | // resetMocks: false,
101 |
102 | // Reset the module registry before running each individual test
103 | // resetModules: false,
104 |
105 | // A path to a custom resolver
106 | // resolver: null,
107 |
108 | // Automatically restore mock state between every test
109 | // restoreMocks: false,
110 |
111 | // The root directory that Jest should scan for tests and modules within
112 | // rootDir: null,
113 |
114 | // A list of paths to directories that Jest should use to search for files in
115 | // roots: [
116 | // ""
117 | // ],
118 |
119 | // Allows you to use a custom runner instead of Jest's default test runner
120 | // runner: "jest-runner",
121 |
122 | // The paths to modules that run some code to configure or set up the testing environment before each test
123 | // setupFiles: [],
124 |
125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
126 | // setupFilesAfterEnv: [],
127 |
128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
129 | // snapshotSerializers: [],
130 |
131 | // The test environment that will be used for testing
132 | testEnvironment: "node",
133 |
134 | // Options that will be passed to the testEnvironment
135 | // testEnvironmentOptions: {},
136 |
137 | // Adds a location field to test results
138 | // testLocationInResults: false,
139 |
140 | // The glob patterns Jest uses to detect test files
141 | // testMatch: [
142 | // "**/__tests__/**/*.[jt]s?(x)",
143 | // "**/?(*.)+(spec|test).[tj]s?(x)"
144 | // ],
145 |
146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
147 | // testPathIgnorePatterns: [
148 | // "\\\\node_modules\\\\"
149 | // ],
150 |
151 | // The regexp pattern or array of patterns that Jest uses to detect test files
152 | // testRegex: [],
153 |
154 | // This option allows the use of a custom results processor
155 | // testResultsProcessor: null,
156 |
157 | // This option allows use of a custom test runner
158 | // testRunner: "jasmine2",
159 |
160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
161 | // testURL: "http://localhost",
162 |
163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
164 | // timers: "real",
165 |
166 | // A map from regular expressions to paths to transformers
167 | // transform: null,,
168 | transform: {
169 | "^.+\\.tsx?$": "ts-jest"
170 | }
171 |
172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
173 | // transformIgnorePatterns: [
174 | // "\\\\node_modules\\\\"
175 | // ],
176 |
177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
178 | // unmockedModulePathPatterns: undefined,
179 |
180 | // Indicates whether each individual test should be reported during the run
181 | // verbose: null,
182 |
183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
184 | // watchPathIgnorePatterns: [],
185 |
186 | // Whether to use watchman for file crawling
187 | // watchman: true,
188 | };
189 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@3d-dice/dice-roller-parser",
3 | "version": "0.2.6",
4 | "description": "A javascript dice roller that parses roll20 format strings and generates rolled outputs",
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "types": "dist/index.d.ts",
8 | "typings": "dist/index.d.ts",
9 | "files": [
10 | "dist"
11 | ],
12 | "scripts": {
13 | "build": "npm run clean && pegjs src/diceroll.pegjs && tslint -c tslint.json --project tsconfig.json && webpack",
14 | "clean": "rimraf dist/ src/diceroll.js",
15 | "test": "jest",
16 | "prepublishOnly": "npm run build",
17 | "prepare": "npm run build"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/3d-dice/dice-roller-parser.git"
22 | },
23 | "keywords": [
24 | "dice",
25 | "roller",
26 | "roll",
27 | "roll20",
28 | "rpg",
29 | "trpg",
30 | "tabletop",
31 | "dnd",
32 | "d&d",
33 | "dungeons",
34 | "dragons",
35 | "pathfinder"
36 | ],
37 | "author": "Ben Morton ",
38 | "contributors": [
39 | {
40 | "name": "Frank Ali "
41 | }
42 | ],
43 | "license": "MIT",
44 | "bugs": {
45 | "url": "https://github.com/3d-dice/dice-roller-parser/issues"
46 | },
47 | "homepage": "https://github.com/3d-dice/dice-roller-parser#readme",
48 | "devDependencies": {
49 | "@types/jest": "^27.5.1",
50 | "@types/node": "^16.11.36",
51 | "jest": "^27.5.1",
52 | "pegjs": "^0.10.0",
53 | "rimraf": "^3.0.2",
54 | "ts-jest": "^27.1.5",
55 | "ts-loader": "^9.3.0",
56 | "tslint": "^6.1.3",
57 | "typescript": "^4.7.2",
58 | "webpack": "^5.72.1",
59 | "webpack-cli": "^4.9.2"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/diceRoller.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable-next-line: no-var-requires
2 | const parser = require("./diceroll.js");
3 | import {
4 | RootType, DiceRoll, NumberType, InlineExpression, RollExpressionType, MathType, GroupedRoll, SortRollType, SuccessFailureCritModType,
5 | ReRollMod, FullRoll, ParsedType, MathExpression, KeepDropModType, SuccessFailureModType, MathFunctionExpression
6 | } from "./parsedRollTypes";
7 | import {
8 | RollBase, DiceExpressionRoll, GroupRoll, DiceRollResult, DieRollBase, ExpressionRoll, DieRoll, FateDieRoll, GroupedRollBase, MathFunctionRoll
9 | } from "./rollTypes";
10 |
11 | // TODO: [[ {[[1d6]], 5}kh1 ]] fails due to white space "[[ {" - perhaps add .?* to pegjs file to allow optional spaces
12 |
13 | export class DiceRoller {
14 | public randFunction: () => number = Math.random;
15 | public maxRollCount = 1000;
16 |
17 | /**
18 | * The DiceRoller class that performs parsing and rolls of {@link https://wiki.roll20.net/Dice_Reference roll20 format} input strings
19 | * @constructor
20 | * @param randFunction The random number generator function to use when rolling, default: Math.random
21 | * @param maxRolls The max number of rolls to perform for a single die, default: 1000
22 | */
23 | constructor(randFunction?: () => number, maxRolls = 1000) {
24 | if (randFunction) {
25 | this.randFunction = randFunction;
26 | }
27 | this.maxRollCount = maxRolls;
28 | }
29 |
30 | /**
31 | * Parses and returns an representation of a dice roll input string
32 | * @param input The input string to parse
33 | * @returns A {@link RootType} object representing the parsed input string
34 | */
35 | public parse(input: string): RootType {
36 | return parser.parse(input);
37 | }
38 |
39 | /**
40 | * Parses and rolls a dice roll input string, returning an object representing the roll
41 | * @param input The input string to parse
42 | * @returns A {@link RollBase} object representing the rolled dice input string
43 | */
44 | public roll(input: string): RollBase {
45 | const root = parser.parse(input);
46 | return this.rollType(root);
47 | }
48 |
49 | /**
50 | * Parses and rolls a dice roll input string, returning the result as a number
51 | * @param input The input string to parse
52 | * @returns The final number value of the result
53 | */
54 | public rollValue(input: string): number {
55 | return this.roll(input).value;
56 | }
57 |
58 | /**
59 | * Rolls a previously parsed dice roll input string, returning an object representing the roll
60 | * @param parsed A parsed input as a {@link RootType} string to be rolled
61 | * @returns A {@link RollBase} object representing the rolled dice input string
62 | */
63 | public rollParsed(parsed: RootType): RollBase {
64 | return this.rollType(parsed);
65 | }
66 |
67 | private rollType(input: RootType): RollBase {
68 | let response: RollBase;
69 |
70 | switch (input.type) {
71 | case "diceExpression":
72 | response = this.rollDiceExpr(input as RollExpressionType);
73 | break;
74 | case "group":
75 | response = this.rollGroup(input as GroupedRoll);
76 | break;
77 | case "die":
78 | response = this.rollDie(input as DiceRoll);
79 | break;
80 | case "expression":
81 | response = this.rollExpression(input as MathExpression);
82 | break;
83 | case "mathfunction":
84 | response = this.rollFunction(input as MathFunctionExpression);
85 | break;
86 | case "inline":
87 | response = this.rollType((input as InlineExpression).expr);
88 | break;
89 | case "number":
90 | response = {
91 | ...(input as NumberType),
92 | success: null,
93 | successes: 0,
94 | failures: 0,
95 | valid: true,
96 | order: 0,
97 | }
98 | break;
99 | default:
100 | throw new Error(`Unable to render ${input.type}`);
101 | }
102 |
103 | if (input.label) {
104 | response.label = input.label;
105 | }
106 |
107 | return response;
108 | }
109 |
110 | private rollDiceExpr(input: RollExpressionType): DiceExpressionRoll {
111 | const headRoll = this.rollType(input.head);
112 | const rolls = [headRoll];
113 | const ops: ("+" | "-")[] = [];
114 |
115 | const value = input.ops
116 | .reduce((headValue, math, order: number) => {
117 | const tailRoll = this.rollType(math.tail);
118 | tailRoll.order = order;
119 |
120 | rolls.push(tailRoll);
121 | ops.push(math.op);
122 |
123 | switch (math.op) {
124 | case "+":
125 | return headValue + tailRoll.value;
126 | case "-":
127 | return headValue - tailRoll.value;
128 | default:
129 | return headValue;
130 | }
131 | }, headRoll.value);
132 |
133 | return {
134 | dice: rolls,
135 | ops,
136 | success: null,
137 | successes: 0,
138 | failures: 0,
139 | type: "diceexpressionroll",
140 | valid: true,
141 | value,
142 | order: 0,
143 | }
144 | }
145 |
146 | private rollGroup(input: GroupedRoll): GroupRoll {
147 | let rolls: RollBase[] = input.rolls.map((roll, order) => ({
148 | ...this.rollType(roll),
149 | order,
150 | }));
151 | let successes = 0
152 | let failures = 0
153 | let hasTarget = false
154 |
155 | // TODO: single sub roll vs. multiple sub rolls -- https://wiki.roll20.net/Dice_Reference#Grouped_Roll_Modifiers
156 |
157 | if (input.mods) {
158 | const mods = input.mods;
159 | const applyGroupMods = (dice: RollBase[]) => {
160 | hasTarget = mods.some((mod) => ["failure", "success"].includes(mod.type));
161 | dice = mods
162 | .reduce((arr, mod) => this.applyGroupMod(arr, mod), dice);
163 |
164 | if (hasTarget) {
165 | dice = dice.map((die) => {
166 | successes += die.successes
167 | failures += die.failures
168 | die.value = die.successes - die.failures
169 | die.success = die.value > 0
170 | return die;
171 | });
172 | }
173 |
174 | return dice;
175 | }
176 |
177 | if (rolls.length === 1 && ["die", "diceexpressionroll"].includes(rolls[0].type)) {
178 | const roll = rolls[0];
179 | let dice = roll.type === "die"
180 | ? (roll as DiceRollResult).rolls
181 | : (roll as DiceExpressionRoll).dice
182 | .filter((die) => die.type !== "number")
183 | .reduce((arr: RollBase[], die) => [
184 | ...arr,
185 | ...die.type === "die"
186 | ? (die as DiceRollResult).rolls
187 | : (die as GroupedRollBase).dice,
188 | ], []);
189 |
190 | dice = applyGroupMods(dice);
191 | roll.value = dice.reduce((sum, die) => die.valid ? sum + die.value : sum, 0);
192 | } else {
193 | rolls = applyGroupMods(rolls);
194 | }
195 | }
196 |
197 | const value = rolls.reduce((sum, roll) => !roll.valid ? sum : sum + roll.value, 0)
198 |
199 | return {
200 | dice: rolls,
201 | success: hasTarget ? value > 0 : null,
202 | successes,
203 | failures,
204 | type: "grouproll",
205 | valid: true,
206 | value,
207 | order: 0,
208 | }
209 | }
210 |
211 | private rollDie(input: FullRoll): DiceRollResult {
212 | const count = this.rollType(input.count);
213 |
214 | if (count.value > this.maxRollCount) {
215 | throw new Error("Entered number of dice too large.");
216 | }
217 |
218 | let rolls: DieRollBase[];
219 | let die: RollBase;
220 | if (input.die.type === "fate") {
221 | die = {
222 | type: "fate",
223 | success: null,
224 | successes: 0,
225 | failures: 0,
226 | valid: false,
227 | value: 0,
228 | order: 0,
229 | };
230 | rolls = Array.from({ length: count.value }, (_, i) => this.generateFateRoll(i));
231 | } else {
232 | die = this.rollType(input.die);
233 | rolls = Array.from({ length: count.value }, (_, i) => this.generateDiceRoll(die.value, i));
234 | }
235 |
236 | if (input.mods) {
237 | rolls = input.mods
238 | .reduce((moddedRolls, mod) => this.applyMod(moddedRolls, mod), rolls);
239 | }
240 |
241 | let successes = 0
242 | let failures = 0
243 |
244 | if (input.targets) {
245 | rolls = input.targets
246 | .reduce((moddedRolls, target) => this.applyMod(moddedRolls, target), rolls)
247 | .map((roll) => {
248 | successes += roll.successes
249 | failures += roll.failures
250 | roll.value = roll.successes - roll.failures
251 | roll.success = roll.value > 0
252 | return roll;
253 | });
254 | }
255 |
256 | let matched = false;
257 | let matchCount = 0;
258 | if (input.match) {
259 | const match = input.match;
260 | const counts = rolls.reduce((map: Map, roll) =>
261 | map.set(roll.roll, (map.get(roll.roll) || 0) + 1),
262 | new Map());
263 |
264 | const matches = new Set(Array.from(counts.entries())
265 | .filter(([_, matchedCount]) => matchedCount >= match.min.value)
266 | .filter(([val]) => !(match.mod
267 | && match.expr)
268 | || this.successTest(match.mod, this.rollType(match.expr).value, val))
269 | .map(([val]) => val));
270 |
271 | rolls.filter((roll) => matches.has(roll.roll))
272 | .forEach((roll) => roll.matched = true);
273 |
274 | if (match.count) {
275 | matched = true;
276 | matchCount = matches.size;
277 | }
278 | }
279 |
280 | if (input.sort) {
281 | rolls = this.applySort(rolls, input.sort);
282 | }
283 |
284 | const value = rolls.reduce((sum, roll) => !roll.valid ? sum : sum + roll.value, 0)
285 |
286 | return {
287 | count,
288 | die,
289 | rolls,
290 | success: input.targets ? value > 0 : null,
291 | successes,
292 | failures,
293 | type: "die",
294 | valid: true,
295 | value: matched ? matchCount : value,
296 | order: 0,
297 | matched,
298 | }
299 | }
300 |
301 | private rollExpression(input: RollExpressionType | MathExpression): ExpressionRoll {
302 | const headRoll = this.rollType(input.head);
303 | const rolls = [headRoll];
304 | const ops: ("+" | "-" | "*" | "/" | "%" | "**")[] = [];
305 |
306 | const value = (input.ops as MathType[])
307 | .reduce((headValue: number, math) => {
308 | const tailRoll = this.rollType(math.tail);
309 | rolls.push(tailRoll);
310 | ops.push(math.op);
311 |
312 | switch (math.op) {
313 | case "+":
314 | return headValue + tailRoll.value;
315 | case "-":
316 | return headValue - tailRoll.value;
317 | case "*":
318 | return headValue * tailRoll.value;
319 | case "/":
320 | return headValue / tailRoll.value;
321 | case "%":
322 | return headValue % tailRoll.value;
323 | case "**":
324 | return headValue ** tailRoll.value;
325 | default:
326 | return headValue;
327 | }
328 | }, headRoll.value);
329 |
330 | return {
331 | dice: rolls,
332 | ops,
333 | success: null,
334 | successes: 0,
335 | failures: 0,
336 | type: "expressionroll",
337 | valid: true,
338 | value,
339 | order: 0,
340 | }
341 | }
342 |
343 | private rollFunction(input: MathFunctionExpression): MathFunctionRoll {
344 | const expr = this.rollType(input.expr);
345 |
346 | let value: number;
347 | switch (input.op) {
348 | case "floor":
349 | value = Math.floor(expr.value);
350 | break;
351 | case "ceil":
352 | value = Math.ceil(expr.value);
353 | break;
354 | case "round":
355 | value = Math.round(expr.value);
356 | break;
357 | case "abs":
358 | value = Math.abs(expr.value);
359 | break;
360 | default:
361 | value = expr.value;
362 | break;
363 | }
364 |
365 | return {
366 | expr,
367 | op: input.op,
368 | success: null,
369 | successes: 0,
370 | failures: 0,
371 | type: "mathfunction",
372 | valid: true,
373 | value,
374 | order: 0,
375 | }
376 | }
377 |
378 | private applyGroupMod(rolls: RollBase[], mod: ParsedType): RollBase[] {
379 | return this.getGroupModMethod(mod)(rolls);
380 | }
381 |
382 | private getGroupModMethod(mod: ParsedType): GroupModMethod {
383 | const lookup = (roll: RollBase) => roll.value;
384 | switch (mod.type) {
385 | case "success":
386 | return this.getSuccessMethod(mod as SuccessFailureModType, lookup);
387 | case "failure":
388 | return this.getFailureMethod(mod as SuccessFailureModType, lookup);
389 | case "keep":
390 | return this.getKeepMethod(mod as KeepDropModType, lookup);
391 | case "drop":
392 | return this.getDropMethod(mod as KeepDropModType, lookup);
393 | default:
394 | throw new Error(`Mod ${mod.type} is not recognised`);
395 | }
396 | }
397 |
398 | private applyMod(rolls: DieRollBase[], mod: ParsedType): DieRollBase[] {
399 | return this.getModMethod(mod)(rolls);
400 | }
401 |
402 | private getModMethod(mod: ParsedType): ModMethod {
403 | const lookup = (roll: DieRollBase) => roll.roll;
404 | switch (mod.type) {
405 | case "success":
406 | return this.getSuccessMethod(mod as SuccessFailureCritModType, lookup);
407 | case "failure":
408 | return this.getFailureMethod(mod as SuccessFailureCritModType, lookup);
409 | case "crit":
410 | return this.getCritSuccessMethod(mod as SuccessFailureCritModType, lookup);
411 | case "critfail":
412 | return this.getCritFailureMethod(mod as SuccessFailureCritModType, lookup);
413 | case "keep":
414 | return (rolls) =>
415 | this.getKeepMethod(mod as KeepDropModType, lookup)(rolls)
416 | .sort((a, b) => a.order - b.order);
417 | case "drop":
418 | return (rolls) =>
419 | this.getDropMethod(mod as KeepDropModType, lookup)(rolls)
420 | .sort((a, b) => a.order - b.order);
421 | case "explode":
422 | return this.getExplodeMethod((mod as ReRollMod));
423 | case "compound":
424 | return this.getCompoundMethod((mod as ReRollMod));
425 | case "penetrate":
426 | return this.getPenetrateMethod((mod as ReRollMod));
427 | case "reroll":
428 | return this.getReRollMethod((mod as ReRollMod));
429 | case "rerollOnce":
430 | return this.getReRollOnceMethod((mod as ReRollMod));
431 | default:
432 | throw new Error(`Mod ${mod.type} is not recognised`);
433 | }
434 | }
435 |
436 | private applySort(rolls: DieRollBase[], mod: SortRollType) {
437 | rolls.sort((a, b) => mod.asc ? a.roll - b.roll : b.roll - a.roll);
438 | rolls.forEach((roll, i) => roll.order = i);
439 | return rolls;
440 | }
441 |
442 | private getCritSuccessMethod(mod: SuccessFailureCritModType, lookup: (roll: T) => number) {
443 | const exprResult = this.rollType(mod.expr);
444 |
445 | return (rolls: T[]) => {
446 | return rolls.map((roll) => {
447 | if (!roll.valid) return roll;
448 | if (roll.type !== "roll") return roll;
449 | if (roll.success) return roll;
450 |
451 | const critRoll = (roll as unknown as DieRoll);
452 | if (this.successTest(mod.mod, exprResult.value, lookup(roll))) {
453 | critRoll.critical = "success";
454 | } else if (critRoll.critical === "success") {
455 | critRoll.critical = null;
456 | }
457 |
458 | return roll;
459 | });
460 | }
461 | }
462 |
463 | private getCritFailureMethod(mod: SuccessFailureCritModType, lookup: (roll: T) => number) {
464 | const exprResult = this.rollType(mod.expr);
465 |
466 | return (rolls: T[]) => {
467 | return rolls.map((roll) => {
468 | if (!roll.valid) return roll;
469 | if (roll.type !== "roll") return roll;
470 | if (roll.success) return roll;
471 |
472 | const critRoll = (roll as unknown as DieRoll);
473 | if (this.successTest(mod.mod, exprResult.value, lookup(roll))) {
474 | critRoll.critical = "failure";
475 | } else if (critRoll.critical === "failure") {
476 | critRoll.critical = null;
477 | }
478 |
479 | return roll;
480 | });
481 | }
482 | }
483 |
484 | private getSuccessMethod(mod: SuccessFailureCritModType, lookup: (roll: T) => number) {
485 | const exprResult = this.rollType(mod.expr);
486 |
487 | return (rolls: T[]) => {
488 | return rolls.map((roll) => {
489 | if (!roll.valid) { return roll; }
490 |
491 | if (this.successTest(mod.mod, exprResult.value, lookup(roll))) {
492 | roll.successes += 1
493 | }
494 | return roll;
495 | });
496 | }
497 | }
498 |
499 | private getFailureMethod(mod: SuccessFailureCritModType, lookup: (roll: T) => number) {
500 | const exprResult = this.rollType(mod.expr);
501 |
502 | return (rolls: T[]) => {
503 | return rolls.map((roll) => {
504 | if (!roll.valid) { return roll; }
505 |
506 | if (this.successTest(mod.mod, exprResult.value, lookup(roll))) {
507 | roll.failures += 1
508 | }
509 | return roll;
510 | });
511 | }
512 | }
513 |
514 | private getKeepMethod(mod: KeepDropModType, lookup: (roll: T) => number) {
515 | const exprResult = this.rollType(mod.expr);
516 |
517 | return (rolls: T[]) => {
518 | if (rolls.length === 0) return rolls;
519 |
520 | rolls = rolls
521 | .sort((a, b) => mod.highlow === "l"
522 | ? lookup(b) - lookup(a)
523 | : lookup(a) - lookup(b))
524 | .sort((a, b) => (a.valid ? 1 : 0) - (b.valid ? 1 : 0));
525 |
526 | const toKeep = Math.max(Math.min(exprResult.value, rolls.length), 0);
527 | let dropped = 0;
528 | let i = 0;
529 |
530 | const toDrop = rolls.reduce((value, roll) => (roll.valid ? 1 : 0) + value, 0) - toKeep;
531 |
532 | while (i < rolls.length && dropped < toDrop) {
533 | if (rolls[i].valid) {
534 | rolls[i].valid = false;
535 | rolls[i].drop = true
536 | dropped++;
537 | }
538 |
539 | i++;
540 | }
541 |
542 | return rolls;
543 | }
544 | }
545 |
546 | private getDropMethod(mod: KeepDropModType, lookup: (roll: T) => number) {
547 | const exprResult = this.rollType(mod.expr);
548 |
549 | return (rolls: T[]) => {
550 | rolls = rolls.sort((a, b) => mod.highlow === "h"
551 | ? lookup(b) - lookup(a)
552 | : lookup(a) - lookup(b));
553 |
554 | const toDrop = Math.max(Math.min(exprResult.value, rolls.length), 0);
555 | let dropped = 0;
556 | let i = 0;
557 |
558 | while (i < rolls.length && dropped < toDrop) {
559 | if (rolls[i].valid) {
560 | rolls[i].valid = false;
561 | rolls[i].drop = true
562 | dropped++;
563 | }
564 |
565 | i++;
566 | }
567 |
568 | return rolls;
569 | }
570 | }
571 |
572 | private getExplodeMethod(mod: ReRollMod) {
573 | const targetValue = mod.target
574 | ? this.rollType(mod.target.value)
575 | : null;
576 |
577 | return (rolls: DieRollBase[]) => {
578 | const targetMethod = targetValue
579 | ? (roll: DieRollBase) => this.successTest(mod.target.mod, targetValue.value, roll.roll)
580 | : (roll: DieRollBase) => this.successTest("=", roll.type === "fateroll" ? 1 : (roll as DieRoll).die, roll.roll);
581 |
582 | if (
583 | rolls[0].type === "roll"
584 | && targetMethod({ roll: 1 } as DieRollBase)
585 | && targetMethod({ roll: (rolls[0] as DieRoll).die } as DieRollBase)
586 | ) {
587 | throw new Error("Invalid reroll target");
588 | }
589 |
590 | for (let i = 0; i < rolls.length; i++) {
591 | let roll = rolls[i];
592 | roll.order = i;
593 | let explodeCount = 0;
594 |
595 | while (targetMethod(roll) && explodeCount++ < 1000) {
596 | roll.explode = true
597 | const newRoll = this.reRoll(roll, ++i);
598 | rolls.splice(i, 0, newRoll);
599 | roll = newRoll;
600 | }
601 | }
602 |
603 | return rolls;
604 | }
605 | }
606 |
607 | private getCompoundMethod(mod: ReRollMod) {
608 | const targetValue = mod.target
609 | ? this.rollType(mod.target.value)
610 | : null;
611 |
612 | return (rolls: DieRollBase[]) => {
613 | const targetMethod = targetValue
614 | ? (roll: DieRollBase) => this.successTest(mod.target.mod, targetValue.value, roll.roll)
615 | : (roll: DieRollBase) => this.successTest("=", roll.type === "fateroll" ? 1 : (roll as DieRoll).die, roll.roll);
616 |
617 | if (
618 | rolls[0].type === "roll"
619 | && targetMethod({ roll: 1 } as DieRollBase)
620 | && targetMethod({ roll: (rolls[0] as DieRoll).die } as DieRollBase)
621 | ) {
622 | throw new Error("Invalid reroll target");
623 | }
624 |
625 | for (let i = 0; i < rolls.length; i++) {
626 | let roll = rolls[i];
627 | let rollValue = roll.roll;
628 | let explodeCount = 0;
629 |
630 | while (targetMethod(roll) && explodeCount++ < 1000) {
631 | roll.explode = true
632 | const newRoll = this.reRoll(roll,i+1);
633 | rollValue += newRoll.roll;
634 | roll = newRoll;
635 | }
636 |
637 | rolls[i].value = rollValue;
638 | rolls[i].roll = rollValue;
639 | }
640 |
641 | return rolls;
642 | }
643 | }
644 |
645 | private getPenetrateMethod(mod: ReRollMod) {
646 | const targetValue = mod.target
647 | ? this.rollType(mod.target.value)
648 | : null;
649 |
650 | return (rolls: DieRollBase[]) => {
651 | const targetMethod = targetValue
652 | ? (roll: DieRollBase) => this.successTest(mod.target.mod, targetValue.value, roll.roll)
653 | : (roll: DieRollBase) => this.successTest("=", roll.type === "fateroll" ? 1 : (roll as DieRoll).die, roll.roll);
654 |
655 | if (targetValue
656 | && rolls[0].type === "roll"
657 | && targetMethod(rolls[0])
658 | && this.successTest(mod.target.mod, targetValue.value, 1)
659 | ) {
660 | throw new Error("Invalid reroll target");
661 | }
662 |
663 | for (let i = 0; i < rolls.length; i++) {
664 | let roll = rolls[i];
665 | roll.order = i;
666 | let explodeCount = 0;
667 |
668 | while (targetMethod(roll) && explodeCount++ < 1000) {
669 | roll.explode = true
670 | const newRoll = this.reRoll(roll, ++i);
671 | newRoll.value -= 1;
672 | // newRoll.roll -= 1;
673 | rolls.splice(i, 0, newRoll);
674 | roll = newRoll;
675 | }
676 | }
677 |
678 | return rolls;
679 | }
680 | }
681 |
682 | private getReRollMethod(mod: ReRollMod) {
683 | const targetMethod = mod.target
684 | ? this.successTest.bind(null, mod.target.mod, this.rollType(mod.target.value).value)
685 | : this.successTest.bind(null, "=", 1);
686 |
687 | return (rolls: DieRollBase[]) => {
688 | if (rolls[0].type === "roll" && targetMethod(1) && targetMethod((rolls[0] as DieRoll).die)) {
689 | throw new Error("Invalid reroll target");
690 | }
691 |
692 | for (let i = 0; i < rolls.length; i++) {
693 | while (targetMethod(rolls[i].roll)) {
694 | rolls[i].reroll = true
695 | rolls[i].valid = false;
696 | const newRoll = this.reRoll(rolls[i], i + 1);
697 | rolls.splice(++i, 0, newRoll);
698 | }
699 | }
700 |
701 | return rolls;
702 | }
703 | }
704 |
705 | private getReRollOnceMethod(mod: ReRollMod) {
706 | const targetMethod = mod.target
707 | ? this.successTest.bind(null, mod.target.mod, this.rollType(mod.target.value).value)
708 | : this.successTest.bind(null, "=", 1);
709 |
710 | return (rolls: DieRollBase[]) => {
711 | if (rolls[0].type === "roll" && targetMethod(1) && targetMethod((rolls[0] as DieRoll).die)) {
712 | throw new Error("Invalid reroll target");
713 | }
714 |
715 | for (let i = 0; i < rolls.length; i++) {
716 | if (targetMethod(rolls[i].roll)) {
717 | rolls[i].reroll = true
718 | rolls[i].valid = false;
719 | const newRoll = this.reRoll(rolls[i], i + 1);
720 | rolls.splice(++i, 0, newRoll);
721 | }
722 | }
723 |
724 | return rolls;
725 | }
726 | }
727 |
728 | private successTest(mod: string, target: number, roll: number) {
729 | switch (mod) {
730 | case ">":
731 | return roll >= target;
732 | case "<":
733 | return roll <= target;
734 | case "=":
735 | default:
736 | // tslint:disable-next-line: triple-equals
737 | return roll == target;
738 | }
739 | }
740 |
741 | private reRoll(roll: DieRollBase, order: number): DieRollBase {
742 | switch (roll.type) {
743 | case "roll":
744 | return this.generateDiceRoll((roll as DieRoll).die, order);
745 | case "fateroll":
746 | return this.generateFateRoll(order);
747 | default:
748 | throw new Error(`Cannot do a reroll of a ${roll.type}.`);
749 | }
750 | }
751 |
752 | private generateDiceRoll(die: number, order: number): DieRoll {
753 | // const roll = Math.floor(this.randFunction() * die) + 1;
754 | // avoid floating math errors like .29 * 100 = 28.999999999999996
755 | const roll = parseInt((this.randFunction() * die).toFixed(),10) + 1;
756 |
757 | const critical = roll === die
758 | ? "success"
759 | : roll === 1
760 | ? "failure"
761 | : null;
762 |
763 | return {
764 | critical,
765 | die,
766 | matched: false,
767 | order,
768 | roll,
769 | success: null,
770 | successes: 0,
771 | failures: 0,
772 | type: "roll",
773 | valid: true,
774 | value: roll,
775 | }
776 | }
777 |
778 | private generateFateRoll(order: number): FateDieRoll {
779 | const roll = Math.floor(this.randFunction() * 3) - 1;
780 |
781 | return {
782 | matched: false,
783 | order,
784 | roll,
785 | success: null,
786 | successes: 0,
787 | failures: 0,
788 | type: "fateroll",
789 | valid: true,
790 | value: roll,
791 | }
792 | }
793 | }
794 |
795 | type ModMethod = (rolls: DieRollBase[]) => DieRollBase[]
796 | type GroupModMethod = (rolls: RollBase[]) => RollBase[]
797 |
--------------------------------------------------------------------------------
/src/diceroll.pegjs:
--------------------------------------------------------------------------------
1 | {
2 | const defaultTarget = {
3 | type: "target",
4 | mod: "=",
5 | value: {
6 | type: "number",
7 | value: 1,
8 | },
9 | }
10 |
11 | const defaultExpression = {
12 | type: "number",
13 | value: 1,
14 | }
15 | }
16 |
17 | start = expr:Expression label:(.*) {
18 | expr.root = true;
19 |
20 | if (label) {
21 | expr.label = label.join("");
22 | }
23 |
24 | return expr;
25 | }
26 |
27 | InlineExpression = "[[" expr:Expression "]]" {
28 | return {
29 | type: "inline",
30 | expr,
31 | }
32 | }
33 |
34 | AnyRoll = roll:(ModGroupedRoll / FullRoll / Integer) _ label:Label? {
35 | if (label) {
36 | roll.label = label;
37 | }
38 |
39 | return roll;
40 | }
41 |
42 | ModGroupedRoll = group:GroupedRoll mods:(KeepMod / DropMod / SuccessMod / FailureMod)* _ label:Label? {
43 | if (mods.length > 0) {
44 | group.mods = (group.mods || []).concat(mods);
45 | }
46 |
47 | if (label) {
48 | group.label = label;
49 | }
50 |
51 | return group;
52 | }
53 |
54 | SuccessMod = mod:(">"/"<"/"=") expr:RollExpr {
55 | return {
56 | type: "success",
57 | mod,
58 | expr,
59 | }
60 | }
61 |
62 | FailureMod = "f" mod:(">"/"<"/"=")? expr:RollExpr {
63 | return {
64 | type: "failure",
65 | mod,
66 | expr,
67 | }
68 | }
69 |
70 | CriticalSuccessMod = "cs" mod:(">"/"<"/"=")? expr:RollExpr {
71 | return {
72 | type: "crit",
73 | mod,
74 | expr,
75 | }
76 | }
77 |
78 | CriticalFailureMod = "cf" mod:(">"/"<"/"=")? expr:RollExpr {
79 | return {
80 | type: "critfail",
81 | mod,
82 | expr,
83 | }
84 | }
85 |
86 | MatchTarget = mod:(">"/"<"/"=") expr:RollExpr {
87 | return {
88 | mod,
89 | expr,
90 | }
91 | }
92 |
93 | MatchMod = "m" count:"t"? min:Integer? target: MatchTarget? {
94 | const match = {
95 | type: "match",
96 | min: min || { type: "number", value: 2 },
97 | count: !!count,
98 | }
99 |
100 | if (target) {
101 | match.mod = target.mod;
102 | match.expr = target.expr;
103 | }
104 |
105 | return match;
106 | }
107 |
108 | KeepMod = "k" highlow:("l" / "h")? expr:RollExpr? {
109 | return {
110 | type: "keep",
111 | highlow,
112 | expr: expr || defaultExpression,
113 | }
114 | }
115 |
116 | DropMod = "d" highlow:("l" / "h")? expr:RollExpr? {
117 | return {
118 | type: "drop",
119 | highlow,
120 | expr: expr || defaultExpression,
121 | }
122 | }
123 |
124 | GroupedRoll = "{" _ head:(RollExpression) tail:(_ "," _ (RollExpression))* _ "}" {
125 | return {
126 | rolls: [head, ...tail.map((el) => el[3])],
127 | type: "group",
128 | }
129 | }
130 |
131 | RollExpression = head:RollOrExpression tail:(_ ("+") _ RollOrExpression)* {
132 | if (tail.length == 0) {
133 | return head;
134 | }
135 |
136 | const ops = tail
137 | .map((element) => ({
138 | type: "math",
139 | op: element[1],
140 | tail: element[3]
141 | }));
142 |
143 | return {
144 | head: head,
145 | type: "diceExpression",
146 | ops,
147 | };
148 | }
149 |
150 | RollOrExpression = FullRoll / Expression
151 |
152 | FullRoll = roll:TargetedRoll _ label:Label? {
153 | if (label) {
154 | roll.label = label;
155 | }
156 |
157 | return roll;
158 | }
159 |
160 | TargetedRoll = head:RolledModRoll mods:(DropMod / KeepMod / SuccessMod / FailureMod / CriticalFailureMod / CriticalSuccessMod)* match:MatchMod? sort:(SortMod)? {
161 | const targets = mods.filter((mod) => ["success", "failure"].includes(mod.type));
162 | mods = mods.filter((mod) => !targets.includes(mod));
163 |
164 | head.mods = (head.mods || []).concat(mods);
165 |
166 | if (targets.length > 0) {
167 | head.targets = targets;
168 | }
169 |
170 | if (match) {
171 | head.match = match;
172 | }
173 |
174 | if (sort) {
175 | head.sort = sort;
176 | }
177 |
178 | return head;
179 | }
180 |
181 | SortMod = "s" dir:("a" / "d")? {
182 | if(dir == "d"){
183 | return {
184 | type: "sort",
185 | asc: false
186 | }
187 | }
188 | return {
189 | type: "sort",
190 | asc: true
191 | }
192 | }
193 |
194 | RolledModRoll = head:DiceRoll tail:(CompoundRoll / PenetrateRoll / ExplodeRoll / ReRollOnceMod / ReRollMod)* {
195 | head.mods = (head.mods || []).concat(tail);
196 | return head;
197 | }
198 |
199 | ExplodeRoll = "!" target:TargetMod? {
200 | return {
201 | type: "explode",
202 | target,
203 | }
204 | }
205 |
206 | CompoundRoll = "!!" target:TargetMod? {
207 | return {
208 | type: "compound",
209 | target,
210 | }
211 | }
212 |
213 | PenetrateRoll = "!p" target:TargetMod? {
214 | return {
215 | type: "penetrate",
216 | target,
217 | }
218 | }
219 |
220 | ReRollMod = "r" target:TargetMod? {
221 | target = target || defaultTarget;
222 |
223 | return {
224 | type: "reroll",
225 | target,
226 | }
227 | }
228 |
229 | ReRollOnceMod = "ro" target:TargetMod? {
230 | target = target || defaultTarget;
231 |
232 | return {
233 | type: "rerollOnce",
234 | target,
235 | }
236 | }
237 |
238 | TargetMod = mod:(">"/"<"/"=")? value:RollExpr {
239 | return {
240 | type: "target",
241 | mod,
242 | value,
243 | }
244 | }
245 |
246 | DiceRoll = head:RollExpr? "d" tail:(FateExpr / PercentExpr / RollExpr) {
247 | head = head ? head : { type: "number", value: 1 };
248 |
249 | return {
250 | die: tail,
251 | count: head,
252 | type: "die"
253 | };
254 | }
255 |
256 | FateExpr = ("F" / "f") {
257 | return {
258 | type: "fate",
259 | }
260 | }
261 |
262 | PercentExpr = ("%") {
263 | return {
264 | type: "number",
265 | value: "100",
266 | }
267 | }
268 |
269 | RollExpr = BracketExpression / Integer;
270 |
271 | Expression = InlineExpression / AddSubExpression / BracketExpression;
272 |
273 | BracketExpression = "(" expr:AddSubExpression ")" _ label:Label? {
274 | if (label) {
275 | expr.label = label;
276 | }
277 |
278 | return expr;
279 | }
280 |
281 | AddSubExpression = head:MultDivExpression tail:(_ ("+" / "-") _ MultDivExpression)* {
282 | if (tail.length == 0) {
283 | return head;
284 | }
285 |
286 | const ops = tail
287 | .map((element) => ({
288 | type: "math",
289 | op: element[1],
290 | tail: element[3],
291 | }));
292 |
293 | return {
294 | head,
295 | type: "expression",
296 | ops,
297 | };
298 | }
299 |
300 | MultDivExpression = head:ModExpoExpression tail:(_ ("*" / "/") _ ModExpoExpression)* {
301 | if (tail.length == 0) {
302 | return head;
303 | }
304 |
305 | const ops = tail
306 | .map((element) => ({
307 | type: "math",
308 | op: element[1],
309 | tail: element[3],
310 | }));
311 |
312 | return {
313 | head,
314 | type: "expression",
315 | ops,
316 | };
317 | }
318 |
319 | ModExpoExpression = head:FunctionOrRoll tail:(_ ("**" / "%") _ FunctionOrRoll)* {
320 | if (tail.length == 0) {
321 | return head;
322 | }
323 |
324 | const ops = tail
325 | .map((element) => ({
326 | type: "math",
327 | op: element[1],
328 | tail: element[3],
329 | }));
330 |
331 | return {
332 | head,
333 | type: "expression",
334 | ops,
335 | };
336 | }
337 |
338 | MathFunction = "floor" / "ceil" / "round" / "abs"
339 |
340 | MathFnExpression = op:MathFunction _ "(" _ expr:AddSubExpression _ ")" {
341 | return {
342 | type: "mathfunction",
343 | op,
344 | expr
345 | };
346 | }
347 |
348 | FunctionOrRoll = MathFnExpression / AnyRoll / BracketExpression
349 |
350 | Integer "integer" = "-"? [0-9]+ {
351 | const num = parseInt(text(), 10);
352 | return {
353 | type: "number",
354 | value: num,
355 | }
356 | }
357 |
358 | Label = "[" label:([^\]]+) "]" {
359 | return label.join("")
360 | }
361 |
362 | _ "whitespace"
363 | = [ \t\n\r]*
--------------------------------------------------------------------------------
/src/discordRollRenderer.ts:
--------------------------------------------------------------------------------
1 | import { RollBase, DiceExpressionRoll, GroupRoll, DiceRollResult, ExpressionRoll, DieRoll, FateDieRoll, MathFunctionRoll } from "./rollTypes";
2 |
3 | /** An example renderer class that renders a roll to a string in a markdown format, compatible with Discord */
4 | export class DiscordRollRenderer {
5 | /**
6 | * Renders a dice roll in a format compatible with Discord
7 | * @param roll a {@link RollBase} object that has been generated by the {@link DiceRoller}
8 | * @returns a string representing the roll that can be used on Discord
9 | */
10 | public render(roll: RollBase) {
11 | return this.doRender(roll, true);
12 | }
13 |
14 | private doRender(roll: RollBase, root = false) {
15 | let render = "";
16 |
17 | const type = roll.type;
18 |
19 | switch (type) {
20 | case "diceexpressionroll":
21 | render = this.renderGroupExpr(roll as DiceExpressionRoll);
22 | break;
23 | case "grouproll":
24 | render = this.renderGroup(roll as GroupRoll);
25 | break;
26 | case "die":
27 | render = this.renderDie(roll as DiceRollResult);
28 | break;
29 | case "expressionroll":
30 | render = this.renderExpression(roll as ExpressionRoll);
31 | break;
32 | case "mathfunction":
33 | render = this.renderFunction(roll as MathFunctionRoll);
34 | break;
35 | case "roll":
36 | return this.renderRoll(roll as DieRoll);
37 | case "fateroll":
38 | return this.renderFateRoll(roll as FateDieRoll);
39 | case "number":
40 | const label = roll.label
41 | ? ` (${roll.label})`
42 | : "";
43 | return `${roll.value}${label}`;
44 | case "fate":
45 | return `F`;
46 | default:
47 | throw new Error("Unable to render");
48 | }
49 |
50 | if (!roll.valid) {
51 | render = "~~" + render.replace(/~~/g, "") + "~~";
52 | }
53 |
54 | if (root) {
55 | return this.stripBrackets(render);
56 | }
57 |
58 | return roll.label ? `(${roll.label}: ${render})` : render;
59 | }
60 |
61 | private renderGroup(group: GroupRoll) {
62 | const replies: string[] = [];
63 |
64 | for (const die of group.dice) {
65 | replies.push(this.doRender(die));
66 | }
67 |
68 | if (replies.length > 1) {
69 | return `{ ${replies.join(" + ")} } = ${group.value}`;
70 | }
71 |
72 | const reply = this.stripBrackets(replies[0]);
73 | return `{ ${reply} } = ${group.value}`;
74 | }
75 |
76 | private renderGroupExpr(group: DiceExpressionRoll) {
77 | const replies: string[] = [];
78 |
79 | for (const die of group.dice) {
80 | replies.push(this.doRender(die));
81 | }
82 |
83 | return replies.length > 1 ? `(${replies.join(" + ")} = ${group.value})` : replies[0];
84 | }
85 |
86 | private renderDie(die: DiceRollResult) {
87 | const replies: string[] = [];
88 |
89 | for (const roll of die.rolls) {
90 | replies.push(this.doRender(roll));
91 | }
92 |
93 | let reply = `${replies.join(", ")}`;
94 |
95 | if (!["number", "fate"].includes(die.die.type) || die.count.type !== "number") {
96 | reply += `[*Rolling: ${this.doRender(die.count)}d${this.doRender(die.die)}*]`;
97 | }
98 |
99 | const matches = die.matched
100 | ? ` Match${die.value === 1 ? "" : "es"}`
101 | : "";
102 | reply += ` = ${die.value}${matches}`;
103 | return `(${reply})`;
104 | }
105 |
106 | private renderExpression(expr: ExpressionRoll) {
107 | if (expr.dice.length > 1) {
108 | const expressions: string[] = [];
109 |
110 | for (let i = 0; i < expr.dice.length - 1; i++) {
111 | expressions.push(this.doRender(expr.dice[i]));
112 | expressions.push(expr.ops[i]);
113 | }
114 |
115 | expressions.push(this.doRender(expr.dice.slice(-1)[0]));
116 | expressions.push("=");
117 | expressions.push(expr.value + "");
118 |
119 | return `(${expressions.join(" ")})`;
120 | } else if (expr.dice[0].type === "number") {
121 | return expr.value + "";
122 | } else {
123 | return this.doRender(expr.dice[0]);
124 | }
125 | }
126 |
127 | private renderFunction(roll: MathFunctionRoll) {
128 | const render = this.doRender(roll.expr);
129 | return `(${roll.op}${this.addBrackets(render)} = ${roll.value})`;
130 | }
131 |
132 | private addBrackets(render: string) {
133 | if (!render.startsWith("(")) {
134 | render = `(${render}`;
135 | }
136 | if (!render.endsWith(")")) {
137 | render = `${render})`;
138 | }
139 | return render;
140 | }
141 |
142 | private stripBrackets(render: string) {
143 | if (render.startsWith("(")) {
144 | render = render.substring(1);
145 | }
146 | if (render.endsWith(")")) {
147 | render = render.substring(0, render.length - 1);
148 | }
149 | return render;
150 | }
151 |
152 | private renderRoll(roll: DieRoll) {
153 | let rollDisplay = `${roll.roll}`;
154 | if (!roll.valid) {
155 | rollDisplay = `~~${roll.roll}~~`;
156 | } else if (roll.success && roll.value === 1) {
157 | rollDisplay = `**${roll.roll}**`;
158 | } else if (roll.success && roll.value === -1) {
159 | rollDisplay = `*${roll.roll}*`;
160 | } else if (!roll.success && roll.critical === "success") {
161 | rollDisplay = `**${roll.roll}**`;
162 | } else if (!roll.success && roll.critical === "failure") {
163 | rollDisplay = `*${roll.roll}*`;
164 | }
165 |
166 | if (roll.matched) {
167 | rollDisplay = `__${rollDisplay}__`;
168 | }
169 |
170 | return rollDisplay;
171 | }
172 |
173 | private renderFateRoll(roll: FateDieRoll) {
174 | const rollValue: string = roll.roll === 0
175 | ? "0"
176 | : roll.roll > 0
177 | ? "+"
178 | : "-";
179 |
180 | let rollDisplay = `${roll.roll}`;
181 | if (!roll.valid) {
182 | rollDisplay = `~~${rollValue}~~`;
183 | } else if (roll.success && roll.value === 1) {
184 | rollDisplay = `**${rollValue}**`;
185 | } else if (roll.success && roll.value === -1) {
186 | rollDisplay = `*${rollValue}*`;
187 | }
188 |
189 | if (roll.matched) {
190 | rollDisplay = `__${rollDisplay}__`;
191 | }
192 |
193 | return rollDisplay;
194 | }
195 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./diceRoller";
2 | export * from "./parsedRollTypes";
3 | export * from "./rollTypes";
4 | export * from "./discordRollRenderer";
5 | export * from "./utilityTypes";
--------------------------------------------------------------------------------
/src/parsedRollTypes.ts:
--------------------------------------------------------------------------------
1 | import { MathFunction, DiceGroupMathOperation, MathOperation, CompareOperation, HighLowType } from "./utilityTypes";
2 |
3 | /** The type of the parsed object */
4 | export type ParsedObjectType = "number"
5 | | "inline"
6 | | "success"
7 | | "failure"
8 | | "match"
9 | | "keep"
10 | | "drop"
11 | | "group"
12 | | "diceExpression"
13 | | "sort"
14 | | "explode"
15 | | "compound"
16 | | "penetrate"
17 | | "reroll"
18 | | "rerollOnce"
19 | | "target"
20 | | "die"
21 | | "fate"
22 | | "expression"
23 | | "expression"
24 | | "math"
25 | | "crit"
26 | | "critfail"
27 | | "mathfunction";
28 |
29 | /** The base interface for all parsed types */
30 | export interface ParsedType {
31 | /** The type of parsed item this object represents */
32 | type: ParsedObjectType;
33 | }
34 |
35 | /** The base interface for a subset of parsed types */
36 | export interface RootType extends ParsedType {
37 | /** The text label attached to this roll */
38 | label?: string;
39 | /** A boolean flag to indicate if this is the root of the parse tree */
40 | root: boolean;
41 | }
42 |
43 | /**
44 | * A single number in the input
45 | * @example 17
46 | */
47 | export interface NumberType extends RootType {
48 | type: "number";
49 | /** The value of the number */
50 | value: number;
51 | }
52 |
53 | /**
54 | * An inline dice expression contained in a string
55 | * @example I want to roll [[2d20]] dice
56 | */
57 | export interface InlineExpression extends RootType {
58 | type: "inline";
59 | /** The expression that was parsed as the inline string */
60 | expr: Expression;
61 | }
62 |
63 | /** A combined type representing any roll */
64 | export type AnyRoll = GroupedRoll | FullRoll | NumberType;
65 |
66 | /**
67 | * A grouped roll with a modifier
68 | * @example {4d6+3d8}kh1
69 | */
70 | export interface ModGroupedRoll extends RootType {
71 | /** The modifiers to be applied to the grouped roll */
72 | mods?: (KeepDropModType | SuccessFailureModType)[];
73 | }
74 |
75 | /**
76 | * A success test modifier.
77 | * A `"success"` or `"failure"` modifier converts the result into a success type result which returns the number of rolls that meet the target.
78 | * A `"crit"` or `"critfail"` modifier tests the roll for whether or not the roll should be displayed as a critical success or critical failure.
79 | * @example 3d6>3
80 | * @example 3d6f<3
81 | */
82 | export interface SuccessFailureCritModType extends ParsedType {
83 | type: "success" | "failure" | "crit" | "critfail";
84 | /** The operation to use for the comparison */
85 | mod: CompareOperation;
86 | /** An expression representing the success or failure comparison */
87 | expr: RollExpression;
88 | }
89 |
90 | /** Equivalent to the `SuccessFailureCritModType` but only supporting "success" and "failure" */
91 | export interface SuccessFailureModType extends SuccessFailureCritModType {
92 | type: "success" | "failure";
93 | }
94 |
95 | /**
96 | * A match type modifier, used to modify the display of dice output in roll20
97 | * @example 2d6m
98 | *
99 | * When used with the `mt` extension, will return the number of matches found
100 | * @example 20d6mt
101 | *
102 | * Additional arguments can be specified that increase the required number of matches or to add a constraint to matches
103 | * @example 20d6mt3 counts matches of 3 items
104 | * @example 20d6m>3 Only counts matches where the rolled value is > 3
105 | */
106 | export interface MatchModType extends ParsedType {
107 | type: "match";
108 | /**
109 | * The minimum number of matches to accept
110 | * @default 2 as a `NumberType`
111 | */
112 | min: NumberType;
113 | /** Whether or not to count the matches */
114 | count: boolean;
115 | /**
116 | * The check type to use for the match condition, if specified
117 | * @optional
118 | */
119 | mod?: CompareOperation;
120 | /**
121 | * An expression representing the match condition, if specified
122 | * @optional
123 | */
124 | expr?: RollExpression;
125 | }
126 |
127 | /**
128 | * A keep or drop modifier specifies a number of dice rolls to keep or drop, either the highest or lowest rolls
129 | * @example 2d20kh1
130 | */
131 | export interface KeepDropModType extends ParsedType {
132 | type: "keep" | "drop";
133 | /** Whether to keep/drop the highest or lowest roll */
134 | highlow: HighLowType;
135 | /**
136 | * An expression representing the number of rolls to keep
137 | * @example 2d6
138 | * @default 1 as a `NumberType`
139 | */
140 | expr: RollExpression;
141 | }
142 |
143 | /**
144 | * Represents a group of rolls combined, with optional modifiers
145 | * @example {2d6,3d6}
146 | */
147 | export interface GroupedRoll extends ModGroupedRoll {
148 | type: "group";
149 | rolls: RollExpression[];
150 | }
151 |
152 | /**
153 | * A roll expression including complex rolls and groups, only allows addition operations
154 | * @example {2d6,3d6}kh1 + {3d6 + 2d6}kh2
155 | */
156 | export interface RollExpressionType extends RootType {
157 | /** The initial roll or expression for the roll expression */
158 | head: RollOrExpression,
159 | type: "diceExpression",
160 | /** The operations to apply to the initial roll or expression */
161 | ops: MathType[],
162 | }
163 |
164 | /** A combination of a complex roll expression, a roll, or a math expression. Used as a helper for type combinations */
165 | export type RollExpression = RollExpressionType | RollOrExpression;
166 |
167 | /** A combination of a roll, or a math expression. Used as a helper for type combinations */
168 | export type RollOrExpression = FullRoll | Expression;
169 |
170 | /**
171 | * A roll object including the dice roll, and any modifiers
172 | * @example 2d6kh1
173 | */
174 | export interface FullRoll extends DiceRoll {
175 | /** Any modifiers attached to the roll */
176 | mods?: (ReRollMod | KeepDropModType)[];
177 | /** Any success or failure targets for the roll */
178 | targets?: (SuccessFailureCritModType)[]
179 | /** Any match modifiers for the roll */
180 | match?: MatchModType
181 | /** Any sort operations to apply to the roll */
182 | sort?: SortRollType;
183 | }
184 |
185 | /**
186 | * A sort operation to apply to a roll
187 | * @example 10d6sa
188 | */
189 | export interface SortRollType extends ParsedType {
190 | type: "sort";
191 | /** Whether to sort ascending or descending */
192 | asc: boolean;
193 | }
194 |
195 | /**
196 | * A re-roll operation to apply to a roll. Can be one of the following types:
197 | * - `explode`: re-rolls any dice that meet the target, continuing if the new roll matches
198 | * - `compound`: re-rolls any dice that meet the target, continuing if the new roll matches and adding the results into a single roll
199 | * - `penetrate`: re-rolls any dice that meet the target subtracting 1 from the new value, continuing if the new roll matches
200 | * - `reroll`: re-rolls a die as long as it meets the target, keeping the final roll
201 | * - `rerollOnce`: re-rolls a die once if it meets the target, keeping the new roll
202 | * @example 2d6!
203 | */
204 | export interface ReRollMod extends ParsedType {
205 | type: "explode" | "compound" | "penetrate" | "reroll" | "rerollOnce",
206 | /** The target modifier to compare the roll value against */
207 | target: TargetMod
208 | }
209 |
210 | /**
211 | * A target modifier to apply to a roll
212 | */
213 | export interface TargetMod extends ParsedType {
214 | type: "target";
215 | /** The check type to use for the condition */
216 | mod: CompareOperation;
217 | /** An expression representing the target condition value */
218 | value: RollExpr;
219 | }
220 |
221 | /**
222 | * The representation of a die roll
223 | * @example 2d6
224 | */
225 | export interface DiceRoll extends RootType {
226 | /** The die value to roll against, can be a fate die, a number or a complex roll expression */
227 | die: RollExpr | FateExpr;
228 | /** The number of time to roll this die */
229 | count: RollExpr;
230 | type: "die";
231 | }
232 |
233 | /**
234 | * The representation of a fate die roll
235 | * @example 2dF
236 | */
237 | export interface FateExpr extends ParsedType {
238 | type: "fate";
239 | }
240 |
241 | /** A combination of a number or value that is not an expression. Used as a helper for type combinations */
242 | export type RollExpr = MathExpression | NumberType;
243 |
244 | /** A combination of expression types. Used as a helper for type combinations */
245 | export type Expression = InlineExpression | MathExpression;
246 |
247 | /**
248 | * A math type expression between two or more dice rolls
249 | * @example 2d6 + 3d6 * 4d6
250 | */
251 | export interface MathExpression extends RootType {
252 | /** The initial roll to perform operations against */
253 | head: AnyRoll;
254 | type: "expression";
255 | /** The operations to apply to the initial roll */
256 | ops: MathType[];
257 | }
258 |
259 | /**
260 | * A representation of an operation to be applied and the value to apply it to
261 | * @param TailType The type of the second value used in the operation
262 | * @param OpValues The possible operations that can be used
263 | * @example + 3d6 (as part of 2d6 + 3d6)
264 | */
265 | export interface MathType extends ParsedType {
266 | type: "math";
267 | /** The math operation to perform */
268 | op: OpValues;
269 | /** The second value to use in the operation */
270 | tail: TailType;
271 | }
272 |
273 | /**
274 | * A representation of a math function to be applied and the expression to apply it to
275 | * @example floor(3d6 / 2d4)
276 | */
277 | export interface MathFunctionExpression extends RootType {
278 | type: "mathfunction";
279 | /** The function to be applied */
280 | op: MathFunction;
281 | /** The expression to apply the function on */
282 | expr: AnyRoll;
283 | }
--------------------------------------------------------------------------------
/src/rollTypes.ts:
--------------------------------------------------------------------------------
1 | import { MathFunction, MathOperation, DiceGroupMathOperation, CriticalType } from "./utilityTypes";
2 |
3 | /** The following types of roll can be used */
4 | export type RollType = "number"
5 | | "diceexpressionroll"
6 | | "expressionroll"
7 | | "grouproll"
8 | | "fate"
9 | | "die"
10 | | "roll"
11 | | "fateroll"
12 | | "mathfunction";
13 |
14 | /** The base class for all die rolls, extended based upon the type property */
15 | export interface RollBase {
16 | /**
17 | * Was the roll a success, for target number rolls
18 | * @example 3d6 > 3
19 | * null if no target was set
20 | */
21 | success: boolean | null;
22 | /** The total target number roll successes */
23 | successes: number;
24 | /** The total target number roll failures */
25 | failures: number;
26 | /** The type of roll that this object represents */
27 | type: RollType;
28 | /** Is the roll still valid, and included in calculations */
29 | valid: boolean;
30 | /** The rolled or calculated value of this roll */
31 | value: number;
32 | /** The display label for this roll */
33 | label?: string;
34 | /** A property used to maintain ordering of dice rolls within groups */
35 | order: number;
36 | /** Has this die been dropped */
37 | drop?: boolean;
38 | /** Has this die exploded */
39 | explode?: boolean;
40 | /** Has this die been rerolled */
41 | reroll?: boolean;
42 | }
43 |
44 | /** An intermediate interface extended for groups of dice */
45 | export interface GroupedRollBase extends RollBase {
46 | /** The rolls included as part of this group */
47 | dice: RollBase[];
48 | }
49 |
50 | /**
51 | * A representation of a dice expression
52 | * @example 2d20 + 6d6
53 | */
54 | export interface DiceExpressionRoll extends GroupedRollBase {
55 | type: "diceexpressionroll";
56 | /** The operations to perform on the rolls */
57 | ops: DiceGroupMathOperation[];
58 | }
59 |
60 | /**
61 | * A representation of a mathematic expression
62 | * @example 20 * 17
63 | */
64 | export interface ExpressionRoll extends GroupedRollBase {
65 | type: "expressionroll";
66 | /** The operations to perform on the rolls */
67 | ops: MathOperation[];
68 | }
69 |
70 | /**
71 | * A representation of a mathematic function
72 | * @example floor(20 / 17)
73 | */
74 | export interface MathFunctionRoll extends RollBase {
75 | type: "mathfunction";
76 | /** The operations to perform on the rolls */
77 | op: MathFunction;
78 | /** The expression that the function is applied upon */
79 | expr: RollBase;
80 | }
81 |
82 | /**
83 | * A representation of a group of rolls
84 | * @example 4d6,3d6
85 | */
86 | export interface GroupRoll extends GroupedRollBase {
87 | type: "grouproll";
88 | }
89 |
90 | /**
91 | * The rolled result of a group of dice
92 | * @example 6d20
93 | */
94 | export interface DiceRollResult extends RollBase {
95 | /** The die this result represents */
96 | die: RollBase;
97 | type: "die";
98 | /** Each roll of the die */
99 | rolls: DieRollBase[];
100 | /** The number of rolls of the die */
101 | count: RollBase;
102 | /** Whether this is a match result */
103 | matched: boolean;
104 | }
105 |
106 | /** An intermediate interface extended for individual die rolls (see below) */
107 | export interface DieRollBase extends RollBase {
108 | /** The rolled result of the die */
109 | roll: number;
110 | /** Whether this roll is a match */
111 | matched: boolean;
112 | }
113 |
114 | /**
115 | * A roll on a regular die
116 | * @example d20
117 | */
118 | export interface DieRoll extends DieRollBase {
119 | /** The die number to be rolled */
120 | die: number;
121 | type: "roll";
122 | critical: CriticalType;
123 | }
124 |
125 | /**
126 | * A roll on a fate die
127 | * @example dF
128 | */
129 | export interface FateDieRoll extends DieRollBase {
130 | type: "fateroll";
131 | }
132 |
--------------------------------------------------------------------------------
/src/utilityTypes.ts:
--------------------------------------------------------------------------------
1 | /** A helper type representing the valid operations for a math operation on a group of dice. */
2 | export type DiceGroupMathOperation = "+" | "-";
3 |
4 | /** A helper type representing the valid operations for a math operation. */
5 | export type MathOperation = "+" | "-" | "*" | "/" | "%" | "**";
6 |
7 | /** A helper type representing the valid operations for a math operation. */
8 | export type MathFunction = "floor" | "ceil" | "round" | "abs";
9 |
10 | /** A helper type used when marking a roll as a critical success or failure */
11 | export type CriticalType = "success" | "failure" | null;
12 |
13 | /** A helper type for the available operations for a comparison point */
14 | export type CompareOperation = ">" | "<" | "=";
15 |
16 | /** A helper type used to determine which rolls to keep or drop */
17 | export type HighLowType = "h" | "l" | null;
--------------------------------------------------------------------------------
/test/parser_test.js:
--------------------------------------------------------------------------------
1 | const dice_roller = require("../dist/index");
2 |
3 | const roller = new dice_roller.DiceRoller(() => 0);
4 | console.log(JSON.stringify(roller.parse("4d6mt3"), null, " "));
5 | console.log(JSON.stringify(roller.roll("4d6mt3"), null, " "));
6 | console.log(roller.rollValue("4d6mt3"));
7 | // const parser = require("./diceroll.js");
8 |
9 | // const testRolls = [
10 | // "d20+5",
11 | // "3d6+2",
12 | // "2d6+5 + d8",
13 | // "1d20+5",
14 | // "1d20+5 Roll for Initiative",
15 | // "1d20+5 Roll for Initiative",
16 | // "1d20+5 \ +5 Roll for Initiative",
17 | // "2d20+5[Fire Damage] + 3d6+5[Ice Damage]",
18 | // "2d10+2d6[crit]+5 Critical Hit!",
19 | // "[[2d6]]d5",
20 | // "5+3",
21 | // "3d6!",
22 | // "3d6!>4",
23 | // "3d6!3",
24 | // "10d6!",
25 | // "5d6!!",
26 | // "{5d6!!}>8",
27 | // "{5d6!!}>8",
28 | // "5d6!p",
29 | // "5d6!p",
30 | // "8d100k4",
31 | // "8d100d3",
32 | // "8d100d3",
33 | // "8d100dh3",
34 | // "8d100kl3",
35 | // "3d6>3",
36 | // "10d6<4",
37 | // "{3d6+1}<3",
38 | // "3d6>3",
39 | // "1d20cs>10",
40 | // "1d20cf<3",
41 | // "1d20cs20cs10",
42 | // "1d20!>18cs>18",
43 | // "1d20cs>10",
44 | // "2d8r<2",
45 | // "2d8r8",
46 | // "2d8r1r3r5r7",
47 | // "2d8r<2",
48 | // "4dF",
49 | // "4dF+1",
50 | // "4dF",
51 | // "{4d6+3d8}kh1",
52 | // "{4d6,3d8}kh1",
53 | // ];
54 |
55 | // testRolls.forEach((roll) => console.log(roll, JSON.stringify(parser.parse(roll), null, " ")));
--------------------------------------------------------------------------------
/test/rendererTest.test.ts:
--------------------------------------------------------------------------------
1 | import * as dist from "../dist/index";
2 |
3 | const testRolls: [string, string][] = [
4 | ["d20+5", "(*1* = 1) + 5 = 6"],
5 | ["3d6+2", "(*1*, *1*, *1* = 3) + 2 = 5"],
6 | ["2d6+5 + d8", "(*1*, *1* = 2) + 5 + (*1* = 1) = 8"],
7 | ["1d20+5", "(*1* = 1) + 5 = 6"],
8 | ["1d20+5 Roll for Initiative", "(*1* = 1) + 5 = 6"],
9 | ["1d20+5 \\ +5 Roll for Initiative", "(*1* = 1) + 5 = 6"],
10 | ["2d20+5[Fire Damage] + 3d6+5[Ice Damage]", "(*1*, *1* = 2) + 5 (Fire Damage) + (*1*, *1*, *1* = 3) + 5 (Ice Damage) = 15"],
11 | ["2d10+2d6[crit]+5 Critical Hit!", "(*1*, *1* = 2) + (crit: (*1*, *1* = 2)) + 5 = 9"],
12 | ["[[2d6]]d5", "*1*, *1* = 2"],
13 | ["5+3", "5 + 3 = 8"],
14 | ["3d6!", "*1*, *1*, *1* = 3"],
15 | ["3d6!>4", "*1*, *1*, *1* = 3"],
16 | ["3d6!3", "*1*, *1*, *1* = 3"],
17 | ["10d6!", "*1*, *1*, *1*, *1*, *1*, *1*, *1*, *1*, *1*, *1* = 10"],
18 | ["5d6!!", "*1*, *1*, *1*, *1*, *1* = 5"],
19 | ["{5d6!!}>8", "{ 1, 1, 1, 1, 1 = 0 } = 0"],
20 | ["5d6!p", "*1*, *1*, *1*, *1*, *1* = 5"],
21 | ["5d6!p", "*1*, *1*, *1*, *1*, *1* = 5"],
22 | ["8d100k4", "~~1~~, ~~1~~, ~~1~~, ~~1~~, *1*, *1*, *1*, *1* = 4"],
23 | ["8d100d3", "~~1~~, ~~1~~, ~~1~~, *1*, *1*, *1*, *1*, *1* = 5"],
24 | ["8d100dh3", "~~1~~, ~~1~~, ~~1~~, *1*, *1*, *1*, *1*, *1* = 5"],
25 | ["8d100kl3", "~~1~~, ~~1~~, ~~1~~, ~~1~~, ~~1~~, *1*, *1*, *1* = 3"],
26 | ["3d6>3", "1, 1, 1 = 0"],
27 | ["10d6<4", "**1**, **1**, **1**, **1**, **1**, **1**, **1**, **1**, **1**, **1** = 10"],
28 | ["3d6>3f1", "*1*, *1*, *1* = -3"],
29 | ["3d6<3f1", "1, 1, 1 = 0"],
30 | ["10d6<4f>5", "**1**, **1**, **1**, **1**, **1**, **1**, **1**, **1**, **1**, **1** = 10"],
31 | ["{3d6+1}<3", "{ (**1**, **1**, **1** = 3) + 1 = 3 } = 3"],
32 | ["2d8r8", "*1*, *1* = 2"],
33 | ["2d8ro1r3r5r7", "~~1~~, *1*, ~~1~~, *1* = 2"],
34 | ["2d8ro<2", "~~1~~, *1*, ~~1~~, *1* = 2"],
35 | ["4dF", "-1, -1, -1, -1 = -4"],
36 | ["4dF+1", "(-1, -1, -1, -1 = -4) + 1 = -3"],
37 | ["{4d6+3d8}kh1", "{ (~~1~~, ~~1~~, ~~1~~, ~~1~~ = 4) + (~~1~~, ~~1~~, *1* = 3) = 1 } = 1"],
38 | ["{4d6,3d8}kh1", "{ ~~(*1*, *1*, *1* = 3)~~ + (*1*, *1*, *1*, *1* = 4) } = 4"],
39 | ["4d6kh1<4", "~~1~~, ~~1~~, ~~1~~, **1** = 1"],
40 | ["4d6kh3>4", "~~1~~, 1, 1, 1 = 0"],
41 | ["4d6>4kh3", "~~1~~, 1, 1, 1 = 0"],
42 | ["4d6<4kh3", "~~1~~, **1**, **1**, **1** = 3"],
43 | ["4d6mt", "__*1*__, __*1*__, __*1*__, __*1*__ = 1 Match"],
44 | ["4d6mt3", "__*1*__, __*1*__, __*1*__, __*1*__ = 1 Match"],
45 | ["4d6mt5", "*1*, *1*, *1*, *1* = 0 Matches"],
46 | ["4d6mt3>2", "*1*, *1*, *1*, *1* = 0 Matches"],
47 | ["4d6mt4<2", "__*1*__, __*1*__, __*1*__, __*1*__ = 1 Match"],
48 | ["floor(7/2)", "floor(7 / 2 = 3.5) = 3"],
49 | ["ceil(7/2)", "ceil(7 / 2 = 3.5) = 4"],
50 | ["round(7/3)", "round(7 / 3 = 2.3333333333333335) = 2"],
51 | ["round(8/3)", "round(8 / 3 = 2.6666666666666665) = 3"],
52 | ["abs(7)", "abs(7) = 7"],
53 | ["abs(-7)", "abs(-7) = 7"],
54 | ["floor( 5 / 2d6 ) + ceil( (3d6 + 7d2) / 4 ) - 2d6", "(floor(5 / (*1*, *1* = 2) = 2.5) = 2) + (ceil(((*1*, *1*, *1* = 3) + (*1*, *1*, *1*, *1*, *1*, *1*, *1* = 7) = 10) / 4 = 2.5) = 3) - (*1*, *1* = 2) = 3"],
55 | ["16 % 3", "16 % 3 = 1"],
56 | ["3 ** 2", "3 ** 2 = 9"],
57 | ];
58 |
59 | const roller = new dist.DiceRoller(() => 0);
60 | const renderer = new dist.DiscordRollRenderer();
61 | testRolls.forEach(([input, expectedValue]) => {
62 | test(input, () => {
63 | const roll = roller.roll(input);
64 | expect(renderer.render(roll)).toBe(expectedValue)
65 | });
66 | });
--------------------------------------------------------------------------------
/test/rollerTest.test.ts:
--------------------------------------------------------------------------------
1 | import * as dist from "../dist/index";
2 | import {
3 | RollBase
4 | } from "../src/rollTypes";
5 |
6 | const testRolls: [string, number][] = [
7 | ["d20+5", 1 + 5],
8 | ["3d6+2", 3 + 2],
9 | ["2d6+5 + d8", 2 + 5 + 1],
10 | ["1d20+5", 1 + 5],
11 | ["1d20+5 Roll for Initiative", 1 + 5],
12 | ["1d20+5 \\ +5 Roll for Initiative", 1 + 5],
13 | ["2d20+5[Fire Damage] + 3d6+5[Ice Damage]", 2 + 5 + 3 + 5],
14 | ["2d10+2d6[crit]+5 Critical Hit!", 2 + 2 + 5],
15 | ["[[2d6]]d5", 2],
16 | ["5+3", 5 + 3],
17 | ["3d6!", 3],
18 | ["3d6!>4", 3],
19 | ["3d6!3", 3],
20 | ["10d6!", 10],
21 | ["5d6!!", 5],
22 | ["{5d6!!}>8", 0],
23 | ["{5d6!!}>8", 0],
24 | ["5d6!p", 5],
25 | ["5d6!p", 5],
26 | ["8d100k4", 4],
27 | ["8d100d3", 5],
28 | ["8d100dh3", 5],
29 | ["8d100kl3", 3],
30 | ["3d6>3", 0],
31 | ["10d6<4", 10],
32 | ["3d6>3f1", -3],
33 | ["3d6<3f1", 0],
34 | ["10d6<4f>5", 10],
35 | ["{3d6+1}<3", 3],
36 | ["2d8r8", 2],
37 | ["2d8ro1r3r5r7", 2],
38 | ["2d8ro<2", 2],
39 | ["4dF", -4],
40 | ["4dF+1", -3],
41 | ["{4d6+3d8}kh1", 1],
42 | ["{4d6,3d8}kh1", 4],
43 | ["4d6kh1<4", 1],
44 | ["4d6kh3>4", 0],
45 | ["4d6>4kh3", 0],
46 | ["4d6<4kh3", 3],
47 | ["4d6mt", 1],
48 | ["4d6mt3", 1],
49 | ["4d6mt5", 0],
50 | ["4d6mt3>2", 0],
51 | ["4d6mt4<2", 1],
52 | ["floor(7/2)", 3],
53 | ["ceil(7/2)", 4],
54 | ["round(7/3)", 2],
55 | ["round(8/3)", 3],
56 | ["abs(7)", 7],
57 | ["abs(-7)", 7],
58 | ["floor( 5 / 2d6 ) + ceil( (3d6 + 7d2) / 4 ) - 2d6", 3],
59 | ["16 % 3", 1],
60 | ["3 ** 2", 9],
61 | ];
62 |
63 | const roller = new dist.DiceRoller(() => 0);
64 | testRolls.forEach(([roll, expectedValue]) => {
65 | test(roll, () => {
66 | expect(roller.rollValue(roll)).toBe(expectedValue)
67 | });
68 | });
69 |
70 | const testFixedRolls: [string, number, number[]][] = [
71 | ['1d6!!', 14, [.84, .84, .17]], // value = [6,6,2]
72 | ['4d6!!', 24, [.84, .67, .5, .17, .84, 0]], // value = [6,5,4,2,6,1]
73 | ['4d6dl1', 15, [.84, .67, .5, .17]], // value = [6,5,4,2]
74 | ['4d6>4', 2, [.84, .17, .5, .34]], // [6,2,4,3]
75 | ['4d6<2', 1, [.84, .17, .5, .34]], // [6,2,4,3]
76 | ['3d6>3f1', -3, [0, 0, 0]], // [1,1,1]
77 | ['3d6>3f1', 2, [.84, .17, .5]], // [6,2,4]
78 | ['4d6<2f>4', -1, [.84, .17, .5, .34]], // [6,2,4,3]
79 | ['4d6', 15, [.84, .17, .5, .34]], // [6,2,4,3]
80 | ['{3d6>4 + 3d4>3, 2d8>7}', 4 , [.84, .17, .5, .76, 0, .49, .5, .86]], // [6,2,4,3,1,2,3,5,7]
81 | ['{2d6!}>5', 3, [.17, .84, .84, .67]] // [2,6!,6!,5]
82 | // TODO: +3 modifier is not being added before eval with target
83 | // ['{3d6+3}<3',0,[0,0,0]], //[1,1,1]
84 | // ['{3d6+1}<3',2,[0,.17,.34]], //[1,2,3]
85 | // ['{3d6+1}<3',1,[.84,0,.34]], //[6,1,3]
86 | ]
87 |
88 | let externalCount: number = 0
89 | let rollsAsFloats: Array = []
90 | const fixedRoller = new dist.DiceRoller((rolls: Array = rollsAsFloats) =>{
91 | if(rolls.length > 0) {
92 | return rolls[externalCount++]
93 | } else {
94 | console.warn("No results passed to the dice-roller-parser. Using fallback Math.random")
95 | return Math.random()
96 | }
97 | })
98 |
99 | testFixedRolls.forEach(([roll, expectedValue, values]) => {
100 | test(roll, () => {
101 | externalCount = 0
102 | rollsAsFloats = values
103 | expect(fixedRoller.rollValue(roll)).toBe(expectedValue)
104 | });
105 | });
106 |
107 | const testSortRolls: [string, number[], number[]][] = [
108 | ['5d6sd', [6,5,4,2,1], [.67, .5, .17, .84, 0]], // value = [5,4,2,6,1]
109 | ['5d6sa', [1,2,4,5,6], [.67, .5, .17, .84, 0]], // value = [5,4,2,6,1]
110 | ['4d6s', [1,3,4,6], [.84, 0, .5, .34]], // value = [6,1,4,3]
111 | ]
112 |
113 | testSortRolls.forEach(([roll, expectedValue, values]) => {
114 | externalCount = 0
115 | rollsAsFloats = values
116 | const result:any = fixedRoller.roll(roll)
117 | const diceOrder = result.rolls.map((roll:RollBase) => roll.value)
118 | expect(diceOrder).toStrictEqual(expectedValue)
119 | });
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "outDir": ".",
5 | "module": "commonjs",
6 | "target": "es2017",
7 | "noImplicitAny": true,
8 | "declaration": true,
9 | "declarationDir": "./dist",
10 | "strict": true,
11 | "sourceMap": true
12 | },
13 | "exclude": [
14 | "node_modules"
15 | ],
16 | "include": [
17 | "src/**/*"
18 | ]
19 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:recommended"
4 | ],
5 | "rules": {
6 | "curly": {
7 | "options": "ignore-same-line"
8 | },
9 | "indent": {
10 | "options": "tabs"
11 | },
12 | "max-line-length": false,
13 | "member-ordering": {
14 | "options": {
15 | "order": "fields-first"
16 | }
17 | },
18 | "no-duplicate-variable": true,
19 | "no-inferrable-types": true,
20 | "no-console": [
21 | true,
22 | "log",
23 | "info",
24 | "trace",
25 | "debug",
26 | "time",
27 | "timeEnd"
28 | ],
29 | "variable-name": {
30 | "options": [
31 | "ban-keywords",
32 | "check-format",
33 | "allow-leading-underscore",
34 | "allow-pascal-case"
35 | ]
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | mode: 'production',
5 | entry: './src/index.ts',
6 | devtool: 'inline-source-map',
7 | module: {
8 | rules: [
9 | {
10 | test: /\.ts$/,
11 | use: 'ts-loader',
12 | exclude: /node_modules/,
13 | }
14 | ]
15 | },
16 | resolve: {
17 | extensions: ['.ts', '.js'],
18 | },
19 | output: {
20 | filename: 'index.js',
21 | path: path.resolve(__dirname, 'dist'),
22 | globalObject: 'this',
23 | libraryTarget: "umd",
24 | },
25 | };
--------------------------------------------------------------------------------