├── .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 | }; --------------------------------------------------------------------------------