/dice-notation

A js library for parsing dice notation

Primary LanguageTypeScriptMIT LicenseMIT

@airjp73/dice-notation

A flexible and pluggable js library for parsing dice notation. It has:

  • Fully typed with Typescript
  • No-fuss use for simple use-cases
  • High level of customization
  • Ability to inspect the individual tokens in the notation -- not just calculate the result.

Installation

npm install --save @airjp73/dice-notation

Simple Usage

If all you want to do is parse some dice notation and get the result you can import roll and pass in the notation.

import { roll } from '@airjp73/dice-notation';

const { result } = roll('1d6 + 3');

It is recommended to use an IDE or text editor that can show you the Typescript types for more in-depth documentation.

Inspecting individual rolls

If you want to get the results of individual dice rolls or more info about the notation itself, you can use these functions. The roll function from the Simple Usage section is essentially just a wrapper for these 4 functions.

import {
  tokenize,
  rollDice,
  tallyRolls,
  calculateFinalResult,
} from '@airjp73/dice-notation';

// Gets the tokens from the lexer
// Example: [DiceRollToken, OperatorToken, DiceRollToken]
const tokens = tokenize('2d6 + 3d4');

// Rolls any dice roll tokens and returns all the individual rolls.
// rolls[i] contains all the rolls for the DiceRollToken at tokens[i]
// Example: [[3, 1], null, [1, 3, 2]]
const rolls = rollDice(tokens);

// Takes the rolls and totals them
// Example: [4, null, 6]
const rollTotals = tallyRolls(tokens, rolls);

// Get the final result of the roll
// Example: 10
const result = calculateFinalResult(tokens, rollTotals);

It's broken up this way to allow as much flexibility as possible for how you want to display the information and what kind of custom dice rules you might want.

Custom dice rules

The default roll function only supports basic dice notation. If you want to support something more than that, you need a custom dice rule.

Custom dice rule implementation

Let's say we want to add a rule to allow us to use d% instead of d100. We can create our rule like this:

// A convenience function for rolling a dice. You can use something else if you want
const myRule = {
  // Regex to pass to the parser
  regex: /\d+d%/,

  // A unique string for our token
  typeConstant: 'MyRule',

  // Takes the raw string value of the custom dice roll
  // and returns data about that role -- it can be whatever you want it to be
  // in this case we only care about how many d100s we need to roll
  tokenize: (raw) => ({ numDice: parseInt(raw.split('d')[0]) }),

  // Takes the data returned from `tokenize` and returns an array of rolls
  // this is so we can see what every individual dice roll was if we want
  // Here we're using a helper to auto-generate the rolls (see helper section below)
  roll: ({ numDice }, { generateRolls }) => generateRolls(numDice, 100),

  // Takes the token returned from `tokenize` and the rolls returned from `roll`
  // and returns the total value
  // Here we're just summing all the rolls, but you can do special logic here if you want
  calculateValue: (token, rolls) => rolls.reduce((agg, num) => agg + num, 0),
};

Type definition

The full type definition of a custom rule is:

interface DiceRule<T> {
  regex: RegExp;
  typeConstant: string;
  tokenize: (raw: string, config: RollConfig) => T;
  roll: (token: T, config: RollConfig) => Rolls;
  calculateValue: (token: T, rolls: number[], config: RollConfig) => number;
}

RollConfig helpers

As you can see above, there are some useful helpers given to each part of the dice rule.

export interface RollConfig {
  // Generate a random number between `min` and `max` inclusive
  // `random(1, 6)` would be like rolling 1d6 because it would generate a number between 1 & 6
  random: (min: number, max: number) => number;

  // Generates `numDice` rolls for dice of size `diceSize`
  // `generateRolls(3, 6)` would be like rolling 3d6
  generateRolls: (numDice: number, diceSize: number) => number[];

  // An object that contains any context provided by you
  // See configuration section below
  context: Record<string, any>;
}

Using the custom rule

Once you've created your custom rule, you need to create new roll methods like so:

import { withPlugins, createDiceRoller } from '@airjp73/dice-notation';

// You can use the same roll functions as before, but now your custom rules are injected into it.
const {
  roll,
  tokenize,
  rollDice,
  tallyRolls,
  calculateFinalResult,
} = createDiceRoller(withPlugins(myRule));

Configuration

Configuration options can be provided to createDiceRoller and/or to individual rolling functions.

// Configuration when creating dice roller
createDiceRoller(withPlugins(myRule), config);

// Configuration when rolling
roll('1d6', config);
const tokens = tokenize('1d6', config);
const rolls = rollDice(tokens, config);
const rollTotals = tallyRolls(tokens, rolls, config);

// `calculateFinalResult` does not accept configuration
const result = calculateFinalResult(tokens, rollTotals);

Configuration options

random

Can be used to customize the randomization function used by dice rules.

const config = {
  // Contrived `random` function that returns `min` + `max` instead of a random number
  random: (min, max) => min + max,
};

// The result here will be `7`
roll('1d6', config);

context

Used to provide outside context to custom dice rules.

Example: This could allow you to write a custom rule to allow variables in dice notation. The values for each variable can be provided through context

const config = {
  context: {
    myVariable: 5,
  },
};

// Result will be `1d6 + 5`
roll('1d6 + myVariable', config);

maxRandomRolls

By default, the dice roller will throw an error if it rolls more than 100,000 dice. If you tweak this setting, be aware that rolling too many dice can result in browser or server crashes. This setting is not intended as validation, simply as crash-prevention.

If 100,000 is too few dice, this can be changed with this setting:

const config = {
  maxRandomRolls: 1_000_000,
};

If you really want to disable the limit entirely (this is probably a bad idea), you can turn it off like this:

const config = {
  maxRandomRolls: 'unlimited_rolls_not_recommended',
};