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
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Roll Parser - Dice notation parser for tabletop RPGs.
3
+ *
4
+ * @module roll-parser
5
+ */
6
+
7
+ // * Error hierarchy
8
+ export { RollParserError, isRollParserError } from './errors';
9
+ export type { RollParserErrorCode } from './errors';
10
+
11
+ // * Lexer exports
12
+ export { LexerError } from './lexer/lexer';
13
+
14
+ // * Parser exports
15
+ export { parse, ParseError } from './parser/parser';
16
+ export type {
17
+ ASTNode,
18
+ BinaryOpNode,
19
+ DiceNode,
20
+ ExplodeNode,
21
+ FateDiceNode,
22
+ FunctionCallNode,
23
+ LiteralNode,
24
+ ModifierNode,
25
+ RerollNode,
26
+ SuccessCountNode,
27
+ UnaryOpNode,
28
+ VersusNode,
29
+ } from './parser/ast';
30
+ export {
31
+ isBinaryOp,
32
+ isDice,
33
+ isExplode,
34
+ isFateDice,
35
+ isFunctionCall,
36
+ isLiteral,
37
+ isModifier,
38
+ isReroll,
39
+ isSuccessCount,
40
+ isUnaryOp,
41
+ isVersus,
42
+ } from './parser/ast';
43
+
44
+ // * RNG exports
45
+ export type { RNG } from './rng/types';
46
+ export { SeededRNG } from './rng/seeded';
47
+
48
+ // * Evaluator exports
49
+ export {
50
+ DEFAULT_MAX_DICE,
51
+ DEFAULT_MAX_EXPLODE_ITERATIONS,
52
+ DEFAULT_MAX_REROLL_ITERATIONS,
53
+ evaluate,
54
+ EvaluatorError,
55
+ } from './evaluator/evaluator';
56
+ export type {
57
+ CompareOp,
58
+ ComparePoint,
59
+ DieModifier,
60
+ DieResult,
61
+ EvaluateOptions,
62
+ RollResult,
63
+ } from './types';
64
+ export { DegreeOfSuccess } from './types';
65
+
66
+ // * Public API
67
+ export { roll } from './roll';
68
+ export type { RollOptions } from './roll';
69
+
70
+ import pkg from '../package.json';
71
+
72
+ export const VERSION: string = pkg.version;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Dice notation lexer with character-by-character scanning.
3
+ *
4
+ * @module lexer/lexer
5
+ */
6
+
7
+ import type { RollParserErrorCode } from '../errors';
8
+ import { RollParserError } from '../errors';
9
+ import { type Token, TokenType } from './tokens';
10
+
11
+ /**
12
+ * Error thrown when the lexer encounters an invalid character.
13
+ */
14
+ export class LexerError extends RollParserError {
15
+ readonly position: number;
16
+ readonly character: string;
17
+
18
+ constructor(message: string, code: RollParserErrorCode, position: number, character: string) {
19
+ super(`${message} at position ${position}: '${character}'`, code);
20
+ this.name = 'LexerError';
21
+ this.position = position;
22
+ this.character = character;
23
+ }
24
+ }
25
+
26
+ /** Known identifier keywords mapped to their token types. */
27
+ const IDENTIFIER_KEYWORDS: Record<string, TokenType> = {
28
+ kh: TokenType.KEEP_HIGH,
29
+ kl: TokenType.KEEP_LOW,
30
+ k: TokenType.KEEP_HIGH,
31
+ dh: TokenType.DROP_HIGH,
32
+ dl: TokenType.DROP_LOW,
33
+ d: TokenType.DICE,
34
+ r: TokenType.REROLL,
35
+ ro: TokenType.REROLL_ONCE,
36
+ f: TokenType.FAIL,
37
+ vs: TokenType.VS,
38
+ floor: TokenType.FUNCTION,
39
+ ceil: TokenType.FUNCTION,
40
+ round: TokenType.FUNCTION,
41
+ abs: TokenType.FUNCTION,
42
+ max: TokenType.FUNCTION,
43
+ min: TokenType.FUNCTION,
44
+ };
45
+
46
+ /**
47
+ * Lexer for dice notation.
48
+ *
49
+ * Produces a stream of tokens from an input string using character-by-character
50
+ * scanning with maximal munch for multi-character tokens.
51
+ */
52
+ export class Lexer {
53
+ private pos = 0;
54
+ private readonly input: string;
55
+
56
+ constructor(input: string) {
57
+ this.input = input;
58
+ }
59
+
60
+ /**
61
+ * Tokenize the entire input and return all tokens.
62
+ */
63
+ tokenize(): Token[] {
64
+ const tokens: Token[] = [];
65
+
66
+ while (true) {
67
+ const token = this.nextToken();
68
+ tokens.push(token);
69
+ if (token.type === TokenType.EOF) break;
70
+ }
71
+
72
+ return tokens;
73
+ }
74
+
75
+ /**
76
+ * Get the next token from the input.
77
+ */
78
+ nextToken(): Token {
79
+ this.skipWhitespace();
80
+
81
+ if (this.isAtEnd()) {
82
+ return this.createToken(TokenType.EOF, '');
83
+ }
84
+
85
+ const startPos = this.pos;
86
+ const char = this.peek();
87
+
88
+ // * Numbers
89
+ if (this.isDigit(char)) {
90
+ return this.scanNumber();
91
+ }
92
+
93
+ // * Identifiers (d, kh, kl, dh, dl, r, ro, f, vs, floor, ceil, ...)
94
+ if (this.isAlpha(char)) {
95
+ return this.scanIdentifier();
96
+ }
97
+
98
+ // * Operators and punctuation
99
+ this.advance();
100
+
101
+ switch (char) {
102
+ case '+':
103
+ return this.createTokenAt(TokenType.PLUS, char, startPos);
104
+ case '-':
105
+ return this.createTokenAt(TokenType.MINUS, char, startPos);
106
+ case '*':
107
+ if (this.match('*')) {
108
+ return this.createTokenAt(TokenType.POWER, '**', startPos);
109
+ }
110
+ return this.createTokenAt(TokenType.MULTIPLY, char, startPos);
111
+ case '/':
112
+ return this.createTokenAt(TokenType.DIVIDE, char, startPos);
113
+ case '%':
114
+ return this.createTokenAt(TokenType.MODULO, char, startPos);
115
+ case '^':
116
+ return this.createTokenAt(TokenType.POWER, char, startPos);
117
+ case '(':
118
+ return this.createTokenAt(TokenType.LPAREN, char, startPos);
119
+ case ')':
120
+ return this.createTokenAt(TokenType.RPAREN, char, startPos);
121
+ case ',':
122
+ return this.createTokenAt(TokenType.COMMA, char, startPos);
123
+ case '>':
124
+ if (this.match('=')) {
125
+ return this.createTokenAt(TokenType.GREATER_EQUAL, '>=', startPos);
126
+ }
127
+ return this.createTokenAt(TokenType.GREATER, char, startPos);
128
+ case '<':
129
+ if (this.match('=')) {
130
+ return this.createTokenAt(TokenType.LESS_EQUAL, '<=', startPos);
131
+ }
132
+ return this.createTokenAt(TokenType.LESS, char, startPos);
133
+ case '=':
134
+ return this.createTokenAt(TokenType.EQUAL, char, startPos);
135
+ case '!':
136
+ if (this.match('!')) {
137
+ return this.createTokenAt(TokenType.EXPLODE_COMPOUND, '!!', startPos);
138
+ }
139
+ if (!this.isAtEnd() && this.peek().toLowerCase() === 'p') {
140
+ this.advance();
141
+ return this.createTokenAt(TokenType.EXPLODE_PENETRATING, '!p', startPos);
142
+ }
143
+ return this.createTokenAt(TokenType.EXPLODE, char, startPos);
144
+ default:
145
+ throw new LexerError('Unexpected character', 'UNEXPECTED_CHARACTER', startPos, char);
146
+ }
147
+ }
148
+
149
+ // * Private helpers
150
+
151
+ private skipWhitespace(): void {
152
+ while (!this.isAtEnd() && this.isWhitespace(this.peek())) {
153
+ this.advance();
154
+ }
155
+ }
156
+
157
+ private scanNumber(): Token {
158
+ const startPos = this.pos;
159
+ let value = '';
160
+
161
+ // Integer part
162
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
163
+ value += this.advance();
164
+ }
165
+
166
+ // Decimal part
167
+ if (!this.isAtEnd() && this.peek() === '.' && this.isDigit(this.peekNext())) {
168
+ value += this.advance(); // consume '.'
169
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
170
+ value += this.advance();
171
+ }
172
+ }
173
+
174
+ return this.createTokenAt(TokenType.NUMBER, value, startPos);
175
+ }
176
+
177
+ /**
178
+ * Scans an identifier using full-accumulation: collects all consecutive
179
+ * alpha characters, then classifies the result against known keywords.
180
+ *
181
+ * Special cases run before/after the accumulation loop:
182
+ * - `dF` / `Df` / `dF` / `DF` produces DICE_FATE. Must be handled BEFORE
183
+ * the loop because `F` is alpha and would otherwise be greedily merged
184
+ * into identifiers like `dfkh` (from `4dFkh2`) or `dfdf` (from `dFdF`).
185
+ * Reserves the `d[fF]` prefix namespace for Fate dice.
186
+ * - Bare `d` followed by `%` produces DICE_PERCENT. `%` is not alpha so the
187
+ * accumulation loop stops naturally and the post-loop check handles it.
188
+ */
189
+ private scanIdentifier(): Token {
190
+ const startPos = this.pos;
191
+
192
+ const first = this.peek();
193
+ const second = this.peekNext();
194
+ if ((first === 'd' || first === 'D') && (second === 'f' || second === 'F')) {
195
+ this.advance();
196
+ this.advance();
197
+ return this.createTokenAt(TokenType.DICE_FATE, 'df', startPos);
198
+ }
199
+
200
+ let value = '';
201
+
202
+ while (!this.isAtEnd() && this.isAlpha(this.peek())) {
203
+ value += this.advance();
204
+ }
205
+
206
+ const lower = value.toLowerCase();
207
+
208
+ if (lower === 'd' && !this.isAtEnd() && this.peek() === '%') {
209
+ this.advance();
210
+ return this.createTokenAt(TokenType.DICE_PERCENT, 'd%', startPos);
211
+ }
212
+
213
+ const tokenType = IDENTIFIER_KEYWORDS[lower];
214
+ if (tokenType != null) {
215
+ return this.createTokenAt(tokenType, lower, startPos);
216
+ }
217
+
218
+ throw new LexerError('Unexpected identifier', 'UNEXPECTED_IDENTIFIER', startPos, lower);
219
+ }
220
+
221
+ private peek(): string {
222
+ return this.input[this.pos] ?? '';
223
+ }
224
+
225
+ private peekNext(): string {
226
+ return this.input[this.pos + 1] ?? '';
227
+ }
228
+
229
+ private advance(): string {
230
+ return this.input[this.pos++] ?? '';
231
+ }
232
+
233
+ private match(expected: string): boolean {
234
+ if (this.isAtEnd()) return false;
235
+ if (this.input[this.pos] !== expected) return false;
236
+ this.pos++;
237
+ return true;
238
+ }
239
+
240
+ private isAtEnd(): boolean {
241
+ return this.pos >= this.input.length;
242
+ }
243
+
244
+ private isDigit(char: string): boolean {
245
+ return char >= '0' && char <= '9';
246
+ }
247
+
248
+ private isAlpha(char: string): boolean {
249
+ const c = char.toLowerCase();
250
+ return c >= 'a' && c <= 'z';
251
+ }
252
+
253
+ private isWhitespace(char: string): boolean {
254
+ return char === ' ' || char === '\t' || char === '\n' || char === '\r';
255
+ }
256
+
257
+ private createToken(type: TokenType, value: string): Token {
258
+ return { type, value, position: this.pos };
259
+ }
260
+
261
+ private createTokenAt(type: TokenType, value: string, position: number): Token {
262
+ return { type, value, position };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Tokenize a dice notation string.
268
+ *
269
+ * @param input - The dice notation to tokenize
270
+ * @returns Array of tokens including EOF
271
+ * @throws {LexerError} If an invalid character is encountered
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const tokens = lex('2d20+5');
276
+ * // [NUMBER(2), DICE, NUMBER(20), PLUS, NUMBER(5), EOF]
277
+ * ```
278
+ */
279
+ export function lex(input: string): Token[] {
280
+ return new Lexer(input).tokenize();
281
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Token type definitions for the dice notation lexer.
3
+ *
4
+ * @module lexer/tokens
5
+ */
6
+
7
+ /**
8
+ * Token types for dice notation.
9
+ *
10
+ * Grouped semantically. Numeric values are stable identifiers — the specific
11
+ * numbers don't matter, but they must be unique.
12
+ */
13
+ export enum TokenType {
14
+ //
15
+ // * Literals
16
+ //
17
+
18
+ /** Numeric literal: integer or decimal */
19
+ NUMBER = 0,
20
+
21
+ //
22
+ // * Dice operators
23
+ //
24
+
25
+ /** Dice operator: 'd' or 'D' */
26
+ DICE = 1,
27
+ /** Percentile dice operator: 'd%' (alias for d100) */
28
+ DICE_PERCENT = 2,
29
+ /** Fate/Fudge dice operator: 'dF' */
30
+ DICE_FATE = 3,
31
+
32
+ //
33
+ // * Arithmetic operators
34
+ //
35
+
36
+ /** Addition operator: '+' */
37
+ PLUS = 4,
38
+ /** Subtraction operator: '-' */
39
+ MINUS = 5,
40
+ /** Multiplication operator: '*' */
41
+ MULTIPLY = 6,
42
+ /** Division operator: '/' */
43
+ DIVIDE = 7,
44
+ /** Modulo operator: '%' */
45
+ MODULO = 8,
46
+ /** Power operator: '**' or '^' */
47
+ POWER = 9,
48
+
49
+ //
50
+ // * Comparison operators
51
+ //
52
+
53
+ /** Greater than: '>' */
54
+ GREATER = 10,
55
+ /** Greater than or equal: '>=' */
56
+ GREATER_EQUAL = 11,
57
+ /** Less than: '<' */
58
+ LESS = 12,
59
+ /** Less than or equal: '<=' */
60
+ LESS_EQUAL = 13,
61
+ /** Equal: '=' */
62
+ EQUAL = 14,
63
+
64
+ //
65
+ // * Grouping and punctuation
66
+ //
67
+
68
+ /** Left parenthesis: '(' */
69
+ LPAREN = 15,
70
+ /** Right parenthesis: ')' */
71
+ RPAREN = 16,
72
+ /** Argument separator: ',' */
73
+ COMMA = 17,
74
+
75
+ //
76
+ // * Keep/drop modifiers
77
+ //
78
+
79
+ /** Keep highest modifier: 'kh' or 'k' */
80
+ KEEP_HIGH = 18,
81
+ /** Keep lowest modifier: 'kl' */
82
+ KEEP_LOW = 19,
83
+ /** Drop highest modifier: 'dh' */
84
+ DROP_HIGH = 20,
85
+ /** Drop lowest modifier: 'dl' */
86
+ DROP_LOW = 21,
87
+
88
+ //
89
+ // * Explode modifiers
90
+ //
91
+
92
+ /** Standard explode: '!' */
93
+ EXPLODE = 22,
94
+ /** Compounding explode: '!!' */
95
+ EXPLODE_COMPOUND = 23,
96
+ /** Penetrating explode: '!p' */
97
+ EXPLODE_PENETRATING = 24,
98
+
99
+ //
100
+ // * Reroll modifiers
101
+ //
102
+
103
+ /** Recursive reroll: 'r' */
104
+ REROLL = 25,
105
+ /** Reroll once: 'ro' */
106
+ REROLL_ONCE = 26,
107
+
108
+ //
109
+ // * Success counting
110
+ //
111
+
112
+ /** Fail marker: 'f' */
113
+ FAIL = 27,
114
+
115
+ //
116
+ // * Functions
117
+ //
118
+
119
+ /** Math function: 'floor', 'ceil', 'round', 'abs', 'max', 'min' */
120
+ FUNCTION = 28,
121
+
122
+ //
123
+ // * Keywords
124
+ //
125
+
126
+ /** Versus operator: 'vs' */
127
+ VS = 29,
128
+
129
+ //
130
+ // * End of input
131
+ //
132
+
133
+ /** End of input marker */
134
+ EOF = 30,
135
+ }
136
+
137
+ /**
138
+ * A token produced by the lexer.
139
+ */
140
+ export type Token = {
141
+ /** The type of this token */
142
+ type: TokenType;
143
+ /** The raw string value from input (lowercased for identifiers) */
144
+ value: string;
145
+ /** Zero-based position in the input string */
146
+ position: number;
147
+ };