roll-parser 2.3.2 → 3.0.0-beta.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.
- package/CHANGELOG.md +91 -0
- package/README.md +175 -170
- package/dist/cli/args.d.ts +35 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/format.d.ts +20 -0
- package/dist/cli/format.d.ts.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli.js +2608 -0
- package/dist/cli.js.map +28 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/evaluator/evaluator.d.ts +121 -0
- package/dist/evaluator/evaluator.d.ts.map +1 -0
- package/dist/evaluator/index.d.ts +8 -0
- package/dist/evaluator/index.d.ts.map +1 -0
- package/dist/evaluator/modifiers/compare.d.ts +15 -0
- package/dist/evaluator/modifiers/compare.d.ts.map +1 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +48 -0
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -0
- package/dist/evaluator/modifiers/keep-drop.d.ts +44 -0
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -0
- package/dist/evaluator/modifiers/reroll.d.ts +33 -0
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -0
- package/dist/evaluator/modifiers/sort.d.ts +23 -0
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
- package/dist/evaluator/modifiers/success-count.d.ts +28 -0
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2560 -0
- package/dist/index.js.map +26 -0
- package/dist/lexer/lexer.d.ts +90 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/tokens.d.ts +107 -0
- package/dist/lexer/tokens.d.ts.map +1 -0
- package/dist/parser/ast.d.ts +368 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/parser.d.ts +145 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/rng/index.d.ts +8 -0
- package/dist/rng/index.d.ts.map +1 -0
- package/dist/rng/mock.d.ts +36 -0
- package/dist/rng/mock.d.ts.map +1 -0
- package/dist/rng/seeded.d.ts +40 -0
- package/dist/rng/seeded.d.ts.map +1 -0
- package/dist/rng/types.d.ts +24 -0
- package/dist/rng/types.d.ts.map +1 -0
- package/dist/roll.d.ts +51 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/testing.d.ts +11 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +41 -0
- package/dist/testing.js.map +11 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +66 -43
- package/src/cli/args.ts +73 -0
- package/src/cli/format.ts +49 -0
- package/src/cli/index.ts +92 -0
- package/src/errors.ts +90 -0
- package/src/evaluator/evaluator.ts +1552 -0
- package/src/evaluator/index.ts +14 -0
- package/src/evaluator/modifiers/compare.ts +29 -0
- package/src/evaluator/modifiers/crit-threshold.ts +59 -0
- package/src/evaluator/modifiers/explode.ts +219 -0
- package/src/evaluator/modifiers/keep-drop.ts +151 -0
- package/src/evaluator/modifiers/reroll.ts +165 -0
- package/src/evaluator/modifiers/sort.ts +30 -0
- package/src/evaluator/modifiers/success-count.ts +59 -0
- package/src/index.ts +90 -0
- package/src/lexer/lexer.ts +374 -0
- package/src/lexer/tokens.ts +187 -0
- package/src/parser/ast.ts +628 -0
- package/src/parser/parser.ts +1239 -0
- package/src/rng/index.ts +8 -0
- package/src/rng/mock.ts +65 -0
- package/src/rng/seeded.ts +157 -0
- package/src/rng/types.ts +25 -0
- package/src/roll.ts +74 -0
- package/src/testing.ts +16 -0
- package/src/types.ts +237 -0
- package/bin/help.txt +0 -19
- package/bin/roll-parser.js +0 -23
- package/dist/roll-parser.js +0 -697
- package/dist/roll-parser.min.js +0 -1
- package/index.js +0 -24
- package/src/complex/parse.js +0 -37
- package/src/complex/parseAndRoll.js +0 -21
- package/src/complex/parseAndRollClassic.js +0 -20
- package/src/complex/parseAndRollSimple.js +0 -19
- package/src/complex/parseAndRollWod.js +0 -20
- package/src/complex/parseClassicRoll.js +0 -22
- package/src/complex/parseSimpleRoll.js +0 -21
- package/src/complex/parseWodRoll.js +0 -21
- package/src/converter.js +0 -45
- package/src/grammar.js +0 -14
- package/src/mapper.js +0 -50
- package/src/normalizer.js +0 -51
- package/src/object/Result.js +0 -23
- package/src/object/Roll.js +0 -26
- package/src/object/Type.js +0 -5
- package/src/object/WodRoll.js +0 -29
- package/src/parser.js +0 -43
- package/src/random.js +0 -22
- package/src/roller.js +0 -106
- package/src/stringifier.js +0 -33
package/src/rng/index.ts
ADDED
package/src/rng/mock.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock RNG for deterministic testing.
|
|
3
|
+
*
|
|
4
|
+
* @module rng/mock
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RNG } from './types.js';
|
|
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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seedable RNG using xorshift128 algorithm.
|
|
3
|
+
*
|
|
4
|
+
* @module rng/seeded
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RNG } from './types.js';
|
|
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
|
+
// Ranges wider than 2^32 need two draws — a single uint32 can never
|
|
118
|
+
// produce the upper part of the range and would silently truncate it.
|
|
119
|
+
if (range > 0x100000000) {
|
|
120
|
+
return lo + this.nextBoundedWide(range);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Rejection sampling for unbiased distribution
|
|
124
|
+
// Avoids modulo bias by rejecting values that would cause uneven distribution
|
|
125
|
+
const threshold = (0x100000000 - range) % range;
|
|
126
|
+
let value: number;
|
|
127
|
+
do {
|
|
128
|
+
value = this.nextUint32();
|
|
129
|
+
} while (value < threshold);
|
|
130
|
+
|
|
131
|
+
return lo + (value % range);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unbiased sampling in `[0, range)` for ranges above 2^32, built from two
|
|
136
|
+
* uint32 draws combined into a 53-bit integer (the largest width JS numbers
|
|
137
|
+
* represent exactly). Ranges beyond 2^53 cannot be sampled without bias —
|
|
138
|
+
* throw instead of silently degrading.
|
|
139
|
+
*/
|
|
140
|
+
private nextBoundedWide(range: number): number {
|
|
141
|
+
const MAX_53 = 2 ** 53;
|
|
142
|
+
if (range > MAX_53) {
|
|
143
|
+
throw new RangeError(`nextInt range ${range} exceeds 2^53 and cannot be sampled exactly`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Largest multiple of `range` below 2^53 — values at or above it are
|
|
147
|
+
// rejected to avoid modulo bias.
|
|
148
|
+
const limit = Math.floor(MAX_53 / range) * range;
|
|
149
|
+
let value: number;
|
|
150
|
+
do {
|
|
151
|
+
// 32 high bits shifted up by 21 + top 21 bits of a second draw = 53 bits.
|
|
152
|
+
value = this.nextUint32() * 0x200000 + (this.nextUint32() >>> 11);
|
|
153
|
+
} while (value >= limit);
|
|
154
|
+
|
|
155
|
+
return value % range;
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/rng/types.ts
ADDED
|
@@ -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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main public API for rolling dice expressions.
|
|
3
|
+
*
|
|
4
|
+
* @module roll
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RNG } from './rng/types.js';
|
|
8
|
+
import type { EvaluateOptions, RollResult } from './types.js';
|
|
9
|
+
import { evaluate } from './evaluator/evaluator.js';
|
|
10
|
+
import { lex } from './lexer/lexer.js';
|
|
11
|
+
import { Parser } from './parser/parser.js';
|
|
12
|
+
import { SeededRNG } from './rng/seeded.js';
|
|
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
|
+
/** Variable context for `@name` / `@{name}` references (default: empty) */
|
|
29
|
+
context?: Record<string, number>;
|
|
30
|
+
/** Behavior when a referenced variable is missing from context (default: 'throw') */
|
|
31
|
+
onMissingVariable?: 'throw' | 'zero';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses and evaluates a dice notation string.
|
|
36
|
+
*
|
|
37
|
+
* @param notation - Dice notation (e.g., "2d6+3", "4d6kh3")
|
|
38
|
+
* @param options - Optional configuration (RNG or seed)
|
|
39
|
+
* @returns Complete roll result with total and metadata
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Random roll
|
|
44
|
+
* const result = roll('2d6+3');
|
|
45
|
+
* console.log(result.total); // 5-15
|
|
46
|
+
*
|
|
47
|
+
* // Seeded for reproducibility
|
|
48
|
+
* const r1 = roll('4d6', { seed: 'test' });
|
|
49
|
+
* const r2 = roll('4d6', { seed: 'test' });
|
|
50
|
+
* r1.total === r2.total; // true
|
|
51
|
+
*
|
|
52
|
+
* // Custom RNG for testing
|
|
53
|
+
* const result = roll('1d20', { rng: createMockRng([15]) });
|
|
54
|
+
* result.total; // 15
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function roll(notation: string, options: RollOptions = {}): RollResult {
|
|
58
|
+
const rng = options.rng ?? new SeededRNG(options.seed);
|
|
59
|
+
const tokens = lex(notation);
|
|
60
|
+
const ast = new Parser(tokens).parse();
|
|
61
|
+
const evalOptions: EvaluateOptions = { notation };
|
|
62
|
+
if (options.maxDice != null) evalOptions.maxDice = options.maxDice;
|
|
63
|
+
if (options.maxExplodeIterations != null) {
|
|
64
|
+
evalOptions.maxExplodeIterations = options.maxExplodeIterations;
|
|
65
|
+
}
|
|
66
|
+
if (options.maxRerollIterations != null) {
|
|
67
|
+
evalOptions.maxRerollIterations = options.maxRerollIterations;
|
|
68
|
+
}
|
|
69
|
+
if (options.context != null) evalOptions.context = options.context;
|
|
70
|
+
if (options.onMissingVariable != null) {
|
|
71
|
+
evalOptions.onMissingVariable = options.onMissingVariable;
|
|
72
|
+
}
|
|
73
|
+
return evaluate(ast, rng, evalOptions);
|
|
74
|
+
}
|
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.js';
|
|
14
|
+
|
|
15
|
+
export const createMockRng = _createMockRng;
|
|
16
|
+
export const MockRNGExhaustedError = _MockRNGExhaustedError;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for roll results and comparison primitives.
|
|
3
|
+
*
|
|
4
|
+
* @module types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ASTNode } from './parser/ast.js';
|
|
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
|
+
* A ComparePoint whose value has been evaluated to a number. Used in
|
|
27
|
+
* `RollPart` where meta-expressions are already resolved.
|
|
28
|
+
*/
|
|
29
|
+
export type ResolvedComparePoint = {
|
|
30
|
+
operator: CompareOp;
|
|
31
|
+
value: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A resolved crit threshold — `'default'` means the per-die default rule
|
|
36
|
+
* (`result === sides` for critical, `result === 1` for fumble).
|
|
37
|
+
*/
|
|
38
|
+
export type ResolvedCritThreshold = ResolvedComparePoint | 'default';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Modifier flags applied to individual die results.
|
|
42
|
+
*/
|
|
43
|
+
export type DieModifier =
|
|
44
|
+
| 'dropped'
|
|
45
|
+
| 'kept'
|
|
46
|
+
| 'exploded'
|
|
47
|
+
| 'rerolled'
|
|
48
|
+
| 'success'
|
|
49
|
+
| 'failure'
|
|
50
|
+
| 'meta';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* PF2e Degree of Success. Produced by the `vs` operator when comparing a
|
|
54
|
+
* roll against a Difficulty Class. Ordering is significant — natural 20
|
|
55
|
+
* upgrades one step and natural 1 downgrades one step.
|
|
56
|
+
*/
|
|
57
|
+
export enum DegreeOfSuccess {
|
|
58
|
+
CriticalFailure = 0,
|
|
59
|
+
Failure = 1,
|
|
60
|
+
Success = 2,
|
|
61
|
+
CriticalSuccess = 3,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Individual die roll result with metadata.
|
|
66
|
+
*/
|
|
67
|
+
export type DieResult = {
|
|
68
|
+
/**
|
|
69
|
+
* Number of sides on the die. Normal dice use `sides >= 1`. Fate/Fudge
|
|
70
|
+
* dice use `sides = 0` as a sentinel — they have no configurable sides
|
|
71
|
+
* and always produce results in {-1, 0, +1}.
|
|
72
|
+
*/
|
|
73
|
+
sides: number;
|
|
74
|
+
/** The rolled value */
|
|
75
|
+
result: number;
|
|
76
|
+
/**
|
|
77
|
+
* Raw first roll before any mutation (e.g., compound-explode accumulation).
|
|
78
|
+
* Only populated when `result` has been overwritten with a computed value.
|
|
79
|
+
* Consumers that need the original face (nat-20 / nat-1 detection) should
|
|
80
|
+
* read `initialResult ?? result`.
|
|
81
|
+
*/
|
|
82
|
+
initialResult?: number;
|
|
83
|
+
/** Modifiers applied to this die */
|
|
84
|
+
modifiers: DieModifier[];
|
|
85
|
+
/** True if rolled the maximum value (always false for Fate dice) */
|
|
86
|
+
critical: boolean;
|
|
87
|
+
/** True if rolled 1 (always false for Fate dice) */
|
|
88
|
+
fumble: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Per-spec keep/drop entry inside a flattened modifier chain. Counts are
|
|
93
|
+
* resolved at evaluation time (meta-expressions like `kh(1d2)` become the
|
|
94
|
+
* rolled number).
|
|
95
|
+
*/
|
|
96
|
+
export type ModifierSpec = {
|
|
97
|
+
kind: 'keep' | 'drop';
|
|
98
|
+
selector: 'highest' | 'lowest';
|
|
99
|
+
count: number;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fields shared by every RollPart variant. `start`/`end` mirror the source
|
|
104
|
+
* span of the AST node the part was evaluated from — present whenever the
|
|
105
|
+
* AST came from `parse()`, absent on hand-built ASTs.
|
|
106
|
+
*/
|
|
107
|
+
type RollPartBase = {
|
|
108
|
+
/** Sub-total this part contributed to its parent. */
|
|
109
|
+
total: number;
|
|
110
|
+
start?: number;
|
|
111
|
+
end?: number;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Structured breakdown of an evaluated expression, mirroring the AST 1:1 —
|
|
116
|
+
* every ASTNode produces exactly one RollPart. Discriminants are lowercase
|
|
117
|
+
* camelCase to distinguish evaluation-tree types from `ASTNode.type`
|
|
118
|
+
* (PascalCase) at a glance.
|
|
119
|
+
*
|
|
120
|
+
* Invariants:
|
|
121
|
+
* - `RollResult.parts.total === RollResult.total`.
|
|
122
|
+
* - `successCount.total === successes - failures`.
|
|
123
|
+
* - `literal.total === value` and `variable.total === value`.
|
|
124
|
+
* - Each part's `rolls[]` shares `DieResult` references with
|
|
125
|
+
* `RollResult.rolls[]`; both reflect post-evaluation state (explode
|
|
126
|
+
* accumulation, reroll flags, keep/drop flags). No deep clone.
|
|
127
|
+
*
|
|
128
|
+
* Meta-expression sub-trees (`4d6kh(1d2)`, `(1+1)d6` counts/sides, computed
|
|
129
|
+
* thresholds) are not surfaced as nested parts — their resolved numbers
|
|
130
|
+
* appear in the owning part, and their dice are inspectable in
|
|
131
|
+
* `RollResult.rolls` via the `'meta'` modifier tag.
|
|
132
|
+
*/
|
|
133
|
+
export type RollPart =
|
|
134
|
+
| (RollPartBase & { type: 'literal'; value: number })
|
|
135
|
+
| (RollPartBase & { type: 'variable'; name: string; value: number })
|
|
136
|
+
| (RollPartBase & { type: 'dice'; count: number; sides: number; rolls: DieResult[] })
|
|
137
|
+
| (RollPartBase & { type: 'fateDice'; count: number; rolls: DieResult[] })
|
|
138
|
+
| (RollPartBase & { type: 'grouped'; inner: RollPart })
|
|
139
|
+
| (RollPartBase & {
|
|
140
|
+
type: 'binaryOp';
|
|
141
|
+
operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
142
|
+
left: RollPart;
|
|
143
|
+
right: RollPart;
|
|
144
|
+
})
|
|
145
|
+
| (RollPartBase & { type: 'unaryOp'; operator: '-'; operand: RollPart })
|
|
146
|
+
| (RollPartBase & { type: 'modifier'; specs: ModifierSpec[]; target: RollPart })
|
|
147
|
+
| (RollPartBase & {
|
|
148
|
+
type: 'explode';
|
|
149
|
+
variant: 'standard' | 'compound' | 'penetrating';
|
|
150
|
+
threshold?: ResolvedComparePoint;
|
|
151
|
+
target: RollPart;
|
|
152
|
+
})
|
|
153
|
+
| (RollPartBase & {
|
|
154
|
+
type: 'reroll';
|
|
155
|
+
once: boolean;
|
|
156
|
+
condition: ResolvedComparePoint;
|
|
157
|
+
target: RollPart;
|
|
158
|
+
})
|
|
159
|
+
| (RollPartBase & {
|
|
160
|
+
type: 'successCount';
|
|
161
|
+
threshold: ResolvedComparePoint;
|
|
162
|
+
failThreshold?: ResolvedComparePoint;
|
|
163
|
+
target: RollPart;
|
|
164
|
+
successes: number;
|
|
165
|
+
failures: number;
|
|
166
|
+
})
|
|
167
|
+
| (RollPartBase & { type: 'versus'; roll: RollPart; dc: RollPart; degree: DegreeOfSuccess })
|
|
168
|
+
| (RollPartBase & { type: 'functionCall'; name: string; args: RollPart[] })
|
|
169
|
+
| (RollPartBase & { type: 'group'; parts: RollPart[]; keptIndices?: number[] })
|
|
170
|
+
| (RollPartBase & { type: 'sort'; order: 'ascending' | 'descending'; target: RollPart })
|
|
171
|
+
| (RollPartBase & {
|
|
172
|
+
type: 'critThreshold';
|
|
173
|
+
successThresholds: ResolvedCritThreshold[];
|
|
174
|
+
failThresholds: ResolvedCritThreshold[];
|
|
175
|
+
target: RollPart;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** Convenience alias for consumers writing exhaustive switches. */
|
|
179
|
+
export type RollPartType = RollPart['type'];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Complete roll result with all metadata.
|
|
183
|
+
*/
|
|
184
|
+
export type RollResult = {
|
|
185
|
+
/** Final computed total */
|
|
186
|
+
total: number;
|
|
187
|
+
/** Original input notation */
|
|
188
|
+
notation: string;
|
|
189
|
+
/** Normalized expression */
|
|
190
|
+
expression: string;
|
|
191
|
+
/** Rendered result with individual rolls shown */
|
|
192
|
+
rendered: string;
|
|
193
|
+
/** All individual die results */
|
|
194
|
+
rolls: DieResult[];
|
|
195
|
+
/** Structured breakdown of the evaluated expression, mirroring the AST 1:1. */
|
|
196
|
+
parts: RollPart;
|
|
197
|
+
/**
|
|
198
|
+
* Number of dice tagged as success across the whole expression. Present
|
|
199
|
+
* only when a success-counting modifier was used. Independent of `total` —
|
|
200
|
+
* arithmetic on top of a success count (e.g. `5d6>=5 * 2`) affects `total`
|
|
201
|
+
* but not `successes`.
|
|
202
|
+
*/
|
|
203
|
+
successes?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Number of dice tagged as failure across the whole expression. Present
|
|
206
|
+
* only when a success-counting modifier with a fail threshold was used.
|
|
207
|
+
*/
|
|
208
|
+
failures?: number;
|
|
209
|
+
/**
|
|
210
|
+
* PF2e Degree of Success. Present only when the expression used the `vs`
|
|
211
|
+
* operator at the top level (e.g. `1d20+10 vs 25`).
|
|
212
|
+
*/
|
|
213
|
+
degree?: DegreeOfSuccess;
|
|
214
|
+
/**
|
|
215
|
+
* Natural d20 value used for PF2e upgrade/downgrade — present only when
|
|
216
|
+
* exactly one kept d20 was rolled on the roll side of a `vs` expression.
|
|
217
|
+
*/
|
|
218
|
+
natural?: number;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Options for the evaluate function.
|
|
223
|
+
*/
|
|
224
|
+
export type EvaluateOptions = {
|
|
225
|
+
/** Original notation string (for result metadata) */
|
|
226
|
+
notation?: string;
|
|
227
|
+
/** Maximum total dice allowed per evaluation (default: 10,000) */
|
|
228
|
+
maxDice?: number;
|
|
229
|
+
/** Maximum explosion iterations allowed per die (default: 1,000) */
|
|
230
|
+
maxExplodeIterations?: number;
|
|
231
|
+
/** Maximum reroll iterations allowed per die (default: 1,000) */
|
|
232
|
+
maxRerollIterations?: number;
|
|
233
|
+
/** Variable context for `@name` / `@{name}` references (default: empty) */
|
|
234
|
+
context?: Record<string, number>;
|
|
235
|
+
/** Behavior when a referenced variable is missing from context (default: 'throw') */
|
|
236
|
+
onMissingVariable?: 'throw' | 'zero';
|
|
237
|
+
};
|
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
|
package/bin/roll-parser.js
DELETED
|
@@ -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
|
-
}();
|