roll-parser 2.3.2 → 3.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +47 -178
  2. package/dist/cli/args.d.ts +35 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/format.d.ts +20 -0
  5. package/dist/cli/format.d.ts.map +1 -0
  6. package/dist/cli/index.d.ts +8 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli.js +1775 -0
  9. package/dist/errors.d.ts +39 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/evaluator/evaluator.d.ts +65 -0
  12. package/dist/evaluator/evaluator.d.ts.map +1 -0
  13. package/dist/evaluator/index.d.ts +8 -0
  14. package/dist/evaluator/index.d.ts.map +1 -0
  15. package/dist/evaluator/modifiers/compare.d.ts +15 -0
  16. package/dist/evaluator/modifiers/compare.d.ts.map +1 -0
  17. package/dist/evaluator/modifiers/explode.d.ts +44 -0
  18. package/dist/evaluator/modifiers/explode.d.ts.map +1 -0
  19. package/dist/evaluator/modifiers/keep-drop.d.ts +44 -0
  20. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -0
  21. package/dist/evaluator/modifiers/reroll.d.ts +33 -0
  22. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -0
  23. package/dist/evaluator/modifiers/success-count.d.ts +28 -0
  24. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -0
  25. package/dist/index.d.ts +20 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1723 -0
  28. package/dist/index.mjs +1724 -0
  29. package/dist/lexer/lexer.d.ts +75 -0
  30. package/dist/lexer/lexer.d.ts.map +1 -0
  31. package/dist/lexer/tokens.d.ts +87 -0
  32. package/dist/lexer/tokens.d.ts.map +1 -0
  33. package/dist/parser/ast.d.ts +183 -0
  34. package/dist/parser/ast.d.ts.map +1 -0
  35. package/dist/parser/parser.d.ts +102 -0
  36. package/dist/parser/parser.d.ts.map +1 -0
  37. package/dist/rng/index.d.ts +8 -0
  38. package/dist/rng/index.d.ts.map +1 -0
  39. package/dist/rng/mock.d.ts +36 -0
  40. package/dist/rng/mock.d.ts.map +1 -0
  41. package/dist/rng/seeded.d.ts +33 -0
  42. package/dist/rng/seeded.d.ts.map +1 -0
  43. package/dist/rng/types.d.ts +24 -0
  44. package/dist/rng/types.d.ts.map +1 -0
  45. package/dist/roll.d.ts +47 -0
  46. package/dist/roll.d.ts.map +1 -0
  47. package/dist/testing.d.ts +11 -0
  48. package/dist/testing.d.ts.map +1 -0
  49. package/dist/testing.js +38 -0
  50. package/dist/testing.mjs +39 -0
  51. package/dist/types.d.ts +105 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/package.json +62 -43
  54. package/src/cli/args.ts +72 -0
  55. package/src/cli/format.ts +45 -0
  56. package/src/cli/index.ts +75 -0
  57. package/src/errors.ts +78 -0
  58. package/src/evaluator/evaluator.ts +837 -0
  59. package/src/evaluator/index.ts +14 -0
  60. package/src/evaluator/modifiers/compare.ts +29 -0
  61. package/src/evaluator/modifiers/explode.ts +215 -0
  62. package/src/evaluator/modifiers/keep-drop.ts +151 -0
  63. package/src/evaluator/modifiers/reroll.ts +172 -0
  64. package/src/evaluator/modifiers/success-count.ts +59 -0
  65. package/src/index.ts +72 -0
  66. package/src/lexer/lexer.ts +281 -0
  67. package/src/lexer/tokens.ts +147 -0
  68. package/src/parser/ast.ts +261 -0
  69. package/src/parser/parser.ts +716 -0
  70. package/src/rng/index.ts +8 -0
  71. package/src/rng/mock.ts +65 -0
  72. package/src/rng/seeded.ts +127 -0
  73. package/src/rng/types.ts +25 -0
  74. package/src/roll.ts +66 -0
  75. package/src/testing.ts +16 -0
  76. package/src/types.ts +112 -0
  77. package/bin/help.txt +0 -19
  78. package/bin/roll-parser.js +0 -23
  79. package/dist/roll-parser.js +0 -697
  80. package/dist/roll-parser.min.js +0 -1
  81. package/index.js +0 -24
  82. package/src/complex/parse.js +0 -37
  83. package/src/complex/parseAndRoll.js +0 -21
  84. package/src/complex/parseAndRollClassic.js +0 -20
  85. package/src/complex/parseAndRollSimple.js +0 -19
  86. package/src/complex/parseAndRollWod.js +0 -20
  87. package/src/complex/parseClassicRoll.js +0 -22
  88. package/src/complex/parseSimpleRoll.js +0 -21
  89. package/src/complex/parseWodRoll.js +0 -21
  90. package/src/converter.js +0 -45
  91. package/src/grammar.js +0 -14
  92. package/src/mapper.js +0 -50
  93. package/src/normalizer.js +0 -51
  94. package/src/object/Result.js +0 -23
  95. package/src/object/Roll.js +0 -26
  96. package/src/object/Type.js +0 -5
  97. package/src/object/WodRoll.js +0 -29
  98. package/src/parser.js +0 -43
  99. package/src/random.js +0 -22
  100. package/src/roller.js +0 -106
  101. package/src/stringifier.js +0 -33
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Mock RNG for deterministic testing.
3
+ *
4
+ * @module rng/mock
5
+ */
6
+
7
+ import type { RNG } from './types';
8
+
9
+ /**
10
+ * Error thrown when MockRNG exhausts its predefined values.
11
+ *
12
+ * This is intentional behavior to catch incorrect roll counts in tests.
13
+ * If you see this error, your test is consuming more random values than expected.
14
+ */
15
+ export class MockRNGExhaustedError extends Error {
16
+ readonly consumed: number;
17
+
18
+ constructor(consumed: number) {
19
+ super(`MockRNG exhausted: consumed ${consumed} values, no more available`);
20
+ this.name = 'MockRNGExhaustedError';
21
+ this.consumed = consumed;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Creates a mock RNG that returns predefined values in sequence.
27
+ *
28
+ * IMPORTANT: Throws MockRNGExhaustedError when all values are consumed.
29
+ * This behavior catches incorrect roll counts in tests - it never wraps around.
30
+ *
31
+ * @param values - Array of values to return (dice results for nextInt, floats for next)
32
+ * @returns RNG instance returning predefined values
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const rng = createMockRng([4, 2, 6]);
37
+ * rng.nextInt(1, 6); // Returns 4
38
+ * rng.nextInt(1, 6); // Returns 2
39
+ * rng.nextInt(1, 6); // Returns 6
40
+ * rng.nextInt(1, 6); // Throws MockRNGExhaustedError
41
+ * ```
42
+ */
43
+ export function createMockRng(values: number[]): RNG {
44
+ let index = 0;
45
+
46
+ const getNext = (): number => {
47
+ const value = values[index];
48
+ if (value === undefined) {
49
+ throw new MockRNGExhaustedError(index);
50
+ }
51
+ index++;
52
+ return value;
53
+ };
54
+
55
+ return {
56
+ next: getNext,
57
+ nextInt: (min: number, max: number): number => {
58
+ const value = getNext();
59
+ if (value < min || value > max) {
60
+ throw new RangeError(`MockRNG value ${value} is out of bounds [${min}, ${max}]`);
61
+ }
62
+ return value;
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Seedable RNG using xorshift128 algorithm.
3
+ *
4
+ * @module rng/seeded
5
+ */
6
+
7
+ import type { RNG } from './types';
8
+
9
+ /**
10
+ * Seedable pseudo-random number generator using xorshift128.
11
+ *
12
+ * Produces reproducible sequences from identical seeds.
13
+ * Period: 2^128 - 1
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Same seed = same sequence
18
+ * const rng1 = new SeededRNG('test-seed');
19
+ * const rng2 = new SeededRNG('test-seed');
20
+ * rng1.nextInt(1, 6) === rng2.nextInt(1, 6); // true
21
+ * ```
22
+ */
23
+ export class SeededRNG implements RNG {
24
+ private s0: number;
25
+ private s1: number;
26
+ private s2: number;
27
+ private s3: number;
28
+
29
+ constructor(seed?: string | number) {
30
+ // Initialize state to zero, will be set by initState
31
+ this.s0 = 0;
32
+ this.s1 = 0;
33
+ this.s2 = 0;
34
+ this.s3 = 0;
35
+
36
+ this.initState(seed);
37
+
38
+ // Warm-up: discard first 20 values for better initial distribution
39
+ for (let i = 0; i < 20; i++) {
40
+ this.nextUint32();
41
+ }
42
+ }
43
+
44
+ private initState(seed?: string | number): void {
45
+ const numSeed =
46
+ seed == null
47
+ ? (Date.now() ^ (Math.random() * 0xffffffff)) >>> 0
48
+ : typeof seed === 'string'
49
+ ? this.hashString(seed)
50
+ : seed >>> 0;
51
+
52
+ // Split seed into 4 state values using splitmix32
53
+ let s = numSeed;
54
+ const state: number[] = [];
55
+
56
+ for (let i = 0; i < 4; i++) {
57
+ s = (s + 0x9e3779b9) >>> 0;
58
+ let z = s;
59
+ z = Math.imul(z ^ (z >>> 16), 0x85ebca6b) >>> 0;
60
+ z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35) >>> 0;
61
+ state.push((z ^ (z >>> 16)) >>> 0);
62
+ }
63
+
64
+ this.s0 = state[0] ?? 0;
65
+ this.s1 = state[1] ?? 0;
66
+ this.s2 = state[2] ?? 0;
67
+ this.s3 = state[3] ?? 0;
68
+
69
+ // Ensure non-zero state (xorshift requires at least one non-zero)
70
+ if (this.s0 === 0 && this.s1 === 0 && this.s2 === 0 && this.s3 === 0) {
71
+ this.s0 = 1;
72
+ }
73
+ }
74
+
75
+ private hashString(str: string): number {
76
+ // djb2 hash algorithm
77
+ let hash = 5381;
78
+ for (let i = 0; i < str.length; i++) {
79
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
80
+ }
81
+ return hash;
82
+ }
83
+
84
+ private nextUint32(): number {
85
+ // xorshift128 algorithm
86
+ let t = this.s3;
87
+ const s = this.s0;
88
+
89
+ this.s3 = this.s2;
90
+ this.s2 = this.s1;
91
+ this.s1 = s;
92
+
93
+ t ^= t << 11;
94
+ t ^= t >>> 8;
95
+ this.s0 = (t ^ s ^ (s >>> 19)) >>> 0;
96
+
97
+ return this.s0;
98
+ }
99
+
100
+ next(): number {
101
+ // Convert uint32 to [0, 1) float
102
+ return this.nextUint32() / 0x100000000;
103
+ }
104
+
105
+ nextInt(min: number, max: number): number {
106
+ // Handle inverted bounds
107
+ const lo = min > max ? max : min;
108
+ const hi = min > max ? min : max;
109
+
110
+ const range = hi - lo + 1;
111
+
112
+ // Single value case
113
+ if (range <= 1) {
114
+ return lo;
115
+ }
116
+
117
+ // Rejection sampling for unbiased distribution
118
+ // Avoids modulo bias by rejecting values that would cause uneven distribution
119
+ const threshold = (0x100000000 - range) % range;
120
+ let value: number;
121
+ do {
122
+ value = this.nextUint32();
123
+ } while (value < threshold);
124
+
125
+ return lo + (value % range);
126
+ }
127
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Random Number Generator interface.
3
+ *
4
+ * All dice rolling MUST use this interface - never use Math.random() directly.
5
+ *
6
+ * @module rng/types
7
+ */
8
+
9
+ /**
10
+ * Random Number Generator interface for dice rolling.
11
+ */
12
+ export type RNG = {
13
+ /**
14
+ * Returns a random floating-point number in the range [0, 1).
15
+ */
16
+ next(): number;
17
+
18
+ /**
19
+ * Returns a random integer in the inclusive range [min, max].
20
+ *
21
+ * @param min - Minimum value (inclusive)
22
+ * @param max - Maximum value (inclusive)
23
+ */
24
+ nextInt(min: number, max: number): number;
25
+ };
package/src/roll.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Main public API for rolling dice expressions.
3
+ *
4
+ * @module roll
5
+ */
6
+
7
+ import type { RNG } from './rng/types';
8
+ import type { EvaluateOptions, RollResult } from './types';
9
+ import { evaluate } from './evaluator/evaluator';
10
+ import { lex } from './lexer/lexer';
11
+ import { Parser } from './parser/parser';
12
+ import { SeededRNG } from './rng/seeded';
13
+
14
+ /**
15
+ * Options for the roll function.
16
+ */
17
+ export type RollOptions = {
18
+ /** Custom RNG instance (takes precedence over seed) */
19
+ rng?: RNG;
20
+ /** Seed for deterministic rolls (ignored if rng provided) */
21
+ seed?: string | number;
22
+ /** Maximum total dice allowed per evaluation (default: 10,000) */
23
+ maxDice?: number;
24
+ /** Maximum explosion iterations allowed per die (default: 1,000) */
25
+ maxExplodeIterations?: number;
26
+ /** Maximum reroll iterations allowed per die (default: 1,000) */
27
+ maxRerollIterations?: number;
28
+ };
29
+
30
+ /**
31
+ * Parses and evaluates a dice notation string.
32
+ *
33
+ * @param notation - Dice notation (e.g., "2d6+3", "4d6kh3")
34
+ * @param options - Optional configuration (RNG or seed)
35
+ * @returns Complete roll result with total and metadata
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Random roll
40
+ * const result = roll('2d6+3');
41
+ * console.log(result.total); // 5-15
42
+ *
43
+ * // Seeded for reproducibility
44
+ * const r1 = roll('4d6', { seed: 'test' });
45
+ * const r2 = roll('4d6', { seed: 'test' });
46
+ * r1.total === r2.total; // true
47
+ *
48
+ * // Custom RNG for testing
49
+ * const result = roll('1d20', { rng: createMockRng([15]) });
50
+ * result.total; // 15
51
+ * ```
52
+ */
53
+ export function roll(notation: string, options: RollOptions = {}): RollResult {
54
+ const rng = options.rng ?? new SeededRNG(options.seed);
55
+ const tokens = lex(notation);
56
+ const ast = new Parser(tokens).parse();
57
+ const evalOptions: EvaluateOptions = { notation };
58
+ if (options.maxDice != null) evalOptions.maxDice = options.maxDice;
59
+ if (options.maxExplodeIterations != null) {
60
+ evalOptions.maxExplodeIterations = options.maxExplodeIterations;
61
+ }
62
+ if (options.maxRerollIterations != null) {
63
+ evalOptions.maxRerollIterations = options.maxRerollIterations;
64
+ }
65
+ return evaluate(ast, rng, evalOptions);
66
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Test utilities for roll-parser consumers.
3
+ *
4
+ * Import from `roll-parser/testing` for deterministic dice testing.
5
+ *
6
+ * @module testing
7
+ */
8
+
9
+ // Direct value exports force the bundler to inline the code
10
+ import {
11
+ MockRNGExhaustedError as _MockRNGExhaustedError,
12
+ createMockRng as _createMockRng,
13
+ } from './rng/mock';
14
+
15
+ export const createMockRng = _createMockRng;
16
+ export const MockRNGExhaustedError = _MockRNGExhaustedError;
package/src/types.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Shared type definitions for roll results and comparison primitives.
3
+ *
4
+ * @module types
5
+ */
6
+
7
+ import type { ASTNode } from './parser/ast';
8
+
9
+ /**
10
+ * Comparison operator for compare points.
11
+ */
12
+ export type CompareOp = '>' | '>=' | '<' | '<=' | '=';
13
+
14
+ /**
15
+ * A comparison threshold used by exploding dice, reroll, and success counting.
16
+ *
17
+ * The value is an ASTNode to support computed thresholds (e.g., `>=ceil(5)`),
18
+ * matching the pattern used by DiceNode.count and DiceNode.sides.
19
+ */
20
+ export type ComparePoint = {
21
+ operator: CompareOp;
22
+ value: ASTNode;
23
+ };
24
+
25
+ /**
26
+ * Modifier flags applied to individual die results.
27
+ */
28
+ export type DieModifier = 'dropped' | 'kept' | 'exploded' | 'rerolled' | 'success' | 'failure';
29
+
30
+ /**
31
+ * PF2e Degree of Success. Produced by the `vs` operator when comparing a
32
+ * roll against a Difficulty Class. Ordering is significant — natural 20
33
+ * upgrades one step and natural 1 downgrades one step.
34
+ */
35
+ export enum DegreeOfSuccess {
36
+ CriticalFailure = 0,
37
+ Failure = 1,
38
+ Success = 2,
39
+ CriticalSuccess = 3,
40
+ }
41
+
42
+ /**
43
+ * Individual die roll result with metadata.
44
+ */
45
+ export type DieResult = {
46
+ /**
47
+ * Number of sides on the die. Normal dice use `sides >= 1`. Fate/Fudge
48
+ * dice use `sides = 0` as a sentinel — they have no configurable sides
49
+ * and always produce results in {-1, 0, +1}.
50
+ */
51
+ sides: number;
52
+ /** The rolled value */
53
+ result: number;
54
+ /** Modifiers applied to this die */
55
+ modifiers: DieModifier[];
56
+ /** True if rolled the maximum value (always false for Fate dice) */
57
+ critical: boolean;
58
+ /** True if rolled 1 (always false for Fate dice) */
59
+ fumble: boolean;
60
+ };
61
+
62
+ /**
63
+ * Complete roll result with all metadata.
64
+ */
65
+ export type RollResult = {
66
+ /** Final computed total */
67
+ total: number;
68
+ /** Original input notation */
69
+ notation: string;
70
+ /** Normalized expression */
71
+ expression: string;
72
+ /** Rendered result with individual rolls shown */
73
+ rendered: string;
74
+ /** All individual die results */
75
+ rolls: DieResult[];
76
+ /**
77
+ * Number of dice tagged as success across the whole expression. Present
78
+ * only when a success-counting modifier was used. Independent of `total` —
79
+ * arithmetic on top of a success count (e.g. `5d6>=5 * 2`) affects `total`
80
+ * but not `successes`.
81
+ */
82
+ successes?: number;
83
+ /**
84
+ * Number of dice tagged as failure across the whole expression. Present
85
+ * only when a success-counting modifier with a fail threshold was used.
86
+ */
87
+ failures?: number;
88
+ /**
89
+ * PF2e Degree of Success. Present only when the expression used the `vs`
90
+ * operator at the top level (e.g. `1d20+10 vs 25`).
91
+ */
92
+ degree?: DegreeOfSuccess;
93
+ /**
94
+ * Natural d20 value used for PF2e upgrade/downgrade — present only when
95
+ * exactly one kept d20 was rolled on the roll side of a `vs` expression.
96
+ */
97
+ natural?: number;
98
+ };
99
+
100
+ /**
101
+ * Options for the evaluate function.
102
+ */
103
+ export type EvaluateOptions = {
104
+ /** Original notation string (for result metadata) */
105
+ notation?: string;
106
+ /** Maximum total dice allowed per evaluation (default: 10,000) */
107
+ maxDice?: number;
108
+ /** Maximum explosion iterations allowed per die (default: 1,000) */
109
+ maxExplodeIterations?: number;
110
+ /** Maximum reroll iterations allowed per die (default: 1,000) */
111
+ maxRerollIterations?: number;
112
+ };
package/bin/help.txt DELETED
@@ -1,19 +0,0 @@
1
- Usage:
2
- roll-parser [options] [<rolls>]
3
-
4
- Options:
5
- --help # Print the module options and usage
6
-
7
- Arguments:
8
- rolls # Whitespace separated list of roll (classic or WoD) notations.
9
- If omitted, classic d20 roll will be used by default.
10
- Use quotes on arguments if WoD notation is passed, to prevent problems
11
- with special characters like '>'.
12
- Classic: 2d20+1
13
- World of Darkness: 6d10!>6f1
14
-
15
- Examples:
16
- roll-parser 2d20+1
17
- roll-parser 4d10 5d6-1 d100
18
- roll-parser '6d10!>6f1'
19
- roll-parser --help
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- process.title = 'roll-parser';
4
-
5
- /* eslint-disable max-len, no-console, no-var, vars-on-top, prefer-spread, global-require, import/no-dynamic-require, wrap-iife, no-void */
6
-
7
- const argv = require( 'minimist' )( process.argv.slice( 2 ));
8
- const path = require( 'path' );
9
- const fs = require( 'fs' );
10
-
11
- const rollParser = require( '../index' );
12
-
13
- const format = roll => `${ rollParser.parseAndRoll( roll ) || 'Invalid roll notation.' }`;
14
-
15
- void function cmd() {
16
- // --help
17
- if ( argv.help ) {
18
- fs.createReadStream( path.resolve( __dirname, './help.txt' )).pipe( process.stdout );
19
- } else {
20
- const rolls = argv._.length > 0 ? argv._ : [ 'd20' ];
21
- rolls.forEach( roll => console.log( format( roll )));
22
- }
23
- }();