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,38 @@
1
+ // src/rng/mock.ts
2
+ class MockRNGExhaustedError extends Error {
3
+ consumed;
4
+ constructor(consumed) {
5
+ super(`MockRNG exhausted: consumed ${consumed} values, no more available`);
6
+ this.name = "MockRNGExhaustedError";
7
+ this.consumed = consumed;
8
+ }
9
+ }
10
+ function createMockRng(values) {
11
+ let index = 0;
12
+ const getNext = () => {
13
+ const value = values[index];
14
+ if (value === undefined) {
15
+ throw new MockRNGExhaustedError(index);
16
+ }
17
+ index++;
18
+ return value;
19
+ };
20
+ return {
21
+ next: getNext,
22
+ nextInt: (min, max) => {
23
+ const value = getNext();
24
+ if (value < min || value > max) {
25
+ throw new RangeError(`MockRNG value ${value} is out of bounds [${min}, ${max}]`);
26
+ }
27
+ return value;
28
+ }
29
+ };
30
+ }
31
+
32
+ // src/testing.ts
33
+ var createMockRng2 = createMockRng;
34
+ var MockRNGExhaustedError2 = MockRNGExhaustedError;
35
+ export {
36
+ createMockRng2 as createMockRng,
37
+ MockRNGExhaustedError2 as MockRNGExhaustedError
38
+ };
@@ -0,0 +1,39 @@
1
+ // @bun
2
+ // src/rng/mock.ts
3
+ class MockRNGExhaustedError extends Error {
4
+ consumed;
5
+ constructor(consumed) {
6
+ super(`MockRNG exhausted: consumed ${consumed} values, no more available`);
7
+ this.name = "MockRNGExhaustedError";
8
+ this.consumed = consumed;
9
+ }
10
+ }
11
+ function createMockRng(values) {
12
+ let index = 0;
13
+ const getNext = () => {
14
+ const value = values[index];
15
+ if (value === undefined) {
16
+ throw new MockRNGExhaustedError(index);
17
+ }
18
+ index++;
19
+ return value;
20
+ };
21
+ return {
22
+ next: getNext,
23
+ nextInt: (min, max) => {
24
+ const value = getNext();
25
+ if (value < min || value > max) {
26
+ throw new RangeError(`MockRNG value ${value} is out of bounds [${min}, ${max}]`);
27
+ }
28
+ return value;
29
+ }
30
+ };
31
+ }
32
+
33
+ // src/testing.ts
34
+ var createMockRng2 = createMockRng;
35
+ var MockRNGExhaustedError2 = MockRNGExhaustedError;
36
+ export {
37
+ createMockRng2 as createMockRng,
38
+ MockRNGExhaustedError2 as MockRNGExhaustedError
39
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared type definitions for roll results and comparison primitives.
3
+ *
4
+ * @module types
5
+ */
6
+ import type { ASTNode } from './parser/ast';
7
+ /**
8
+ * Comparison operator for compare points.
9
+ */
10
+ export type CompareOp = '>' | '>=' | '<' | '<=' | '=';
11
+ /**
12
+ * A comparison threshold used by exploding dice, reroll, and success counting.
13
+ *
14
+ * The value is an ASTNode to support computed thresholds (e.g., `>=ceil(5)`),
15
+ * matching the pattern used by DiceNode.count and DiceNode.sides.
16
+ */
17
+ export type ComparePoint = {
18
+ operator: CompareOp;
19
+ value: ASTNode;
20
+ };
21
+ /**
22
+ * Modifier flags applied to individual die results.
23
+ */
24
+ export type DieModifier = 'dropped' | 'kept' | 'exploded' | 'rerolled' | 'success' | 'failure';
25
+ /**
26
+ * PF2e Degree of Success. Produced by the `vs` operator when comparing a
27
+ * roll against a Difficulty Class. Ordering is significant — natural 20
28
+ * upgrades one step and natural 1 downgrades one step.
29
+ */
30
+ export declare enum DegreeOfSuccess {
31
+ CriticalFailure = 0,
32
+ Failure = 1,
33
+ Success = 2,
34
+ CriticalSuccess = 3
35
+ }
36
+ /**
37
+ * Individual die roll result with metadata.
38
+ */
39
+ export type DieResult = {
40
+ /**
41
+ * Number of sides on the die. Normal dice use `sides >= 1`. Fate/Fudge
42
+ * dice use `sides = 0` as a sentinel — they have no configurable sides
43
+ * and always produce results in {-1, 0, +1}.
44
+ */
45
+ sides: number;
46
+ /** The rolled value */
47
+ result: number;
48
+ /** Modifiers applied to this die */
49
+ modifiers: DieModifier[];
50
+ /** True if rolled the maximum value (always false for Fate dice) */
51
+ critical: boolean;
52
+ /** True if rolled 1 (always false for Fate dice) */
53
+ fumble: boolean;
54
+ };
55
+ /**
56
+ * Complete roll result with all metadata.
57
+ */
58
+ export type RollResult = {
59
+ /** Final computed total */
60
+ total: number;
61
+ /** Original input notation */
62
+ notation: string;
63
+ /** Normalized expression */
64
+ expression: string;
65
+ /** Rendered result with individual rolls shown */
66
+ rendered: string;
67
+ /** All individual die results */
68
+ rolls: DieResult[];
69
+ /**
70
+ * Number of dice tagged as success across the whole expression. Present
71
+ * only when a success-counting modifier was used. Independent of `total` —
72
+ * arithmetic on top of a success count (e.g. `5d6>=5 * 2`) affects `total`
73
+ * but not `successes`.
74
+ */
75
+ successes?: number;
76
+ /**
77
+ * Number of dice tagged as failure across the whole expression. Present
78
+ * only when a success-counting modifier with a fail threshold was used.
79
+ */
80
+ failures?: number;
81
+ /**
82
+ * PF2e Degree of Success. Present only when the expression used the `vs`
83
+ * operator at the top level (e.g. `1d20+10 vs 25`).
84
+ */
85
+ degree?: DegreeOfSuccess;
86
+ /**
87
+ * Natural d20 value used for PF2e upgrade/downgrade — present only when
88
+ * exactly one kept d20 was rolled on the roll side of a `vs` expression.
89
+ */
90
+ natural?: number;
91
+ };
92
+ /**
93
+ * Options for the evaluate function.
94
+ */
95
+ export type EvaluateOptions = {
96
+ /** Original notation string (for result metadata) */
97
+ notation?: string;
98
+ /** Maximum total dice allowed per evaluation (default: 10,000) */
99
+ maxDice?: number;
100
+ /** Maximum explosion iterations allowed per die (default: 1,000) */
101
+ maxExplodeIterations?: number;
102
+ /** Maximum reroll iterations allowed per die (default: 1,000) */
103
+ maxRerollIterations?: number;
104
+ };
105
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/F;;;;GAIG;AACH,oBAAY,eAAe;IACzB,eAAe,IAAI;IACnB,OAAO,IAAI;IACX,OAAO,IAAI;IACX,eAAe,IAAI;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,oDAAoD;IACpD,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC"}
package/package.json CHANGED
@@ -1,18 +1,40 @@
1
1
  {
2
2
  "name": "roll-parser",
3
- "version": "2.3.2",
4
- "description": "Parser for classic (2d6+1), simple (2 6 1), and WoD (4d10!>6f1) dice rolls.",
5
- "main": "index.js",
6
- "bin": "bin/roll-parser.js",
3
+ "version": "3.0.0-alpha.0",
4
+ "description": "High-performance dice notation parser for tabletop RPGs. TypeScript-first, Bun-optimized.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": { "roll-parser": "./dist/cli.js" },
10
+ "sideEffects": false,
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ },
17
+ "./testing": {
18
+ "types": "./dist/testing.d.ts",
19
+ "import": "./dist/testing.mjs",
20
+ "require": "./dist/testing.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src",
26
+ "!src/**/*.test.ts"
27
+ ],
7
28
  "keywords": [
8
29
  "parser",
9
30
  "dice",
10
31
  "roll",
11
32
  "rpg",
12
33
  "dnd",
13
- "wod",
14
- "random",
15
- "d20"
34
+ "d20",
35
+ "pratt-parser",
36
+ "typescript",
37
+ "bun"
16
38
  ],
17
39
  "homepage": "https://github.com/edloidas/roll-parser#readme",
18
40
  "bugs": {
@@ -20,50 +42,47 @@
20
42
  },
21
43
  "license": "MIT",
22
44
  "author": "Mikita Taukachou <edloidas@gmail.com> (https://edloidas.com)",
23
- "files": [
24
- "index.js",
25
- "bin",
26
- "dist",
27
- "src"
28
- ],
29
45
  "repository": {
30
46
  "type": "git",
31
47
  "url": "git+https://github.com/edloidas/roll-parser.git"
32
48
  },
33
- "scripts": {
34
- "test": "jest",
35
- "cover": "jest --coverage",
36
- "report": "node_modules/.bin/jest --coverage && cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js",
37
- "fix": "eslint --fix .",
38
- "api": "rimraf api/ && jsdoc -c .jsdocrc",
39
- "clean": "rimraf dist/*",
40
- "build:bundle": "browserify index.js --debug --standalone rollParser -o dist/roll-parser.js",
41
- "build:minify": "babili dist/roll-parser.js --out-file dist/roll-parser.min.js",
42
- "build": "npm run build:bundle && npm run build:minify",
43
- "prepush": "npm test",
44
- "prepublishOnly": "npm run fix && npm test && npm run clean && npm run build"
49
+ "publishConfig": {
50
+ "access": "public"
45
51
  },
46
- "dependencies": {
47
- "minimist": "^1.2.0"
52
+ "scripts": {
53
+ "typecheck": "tsc --noEmit",
54
+ "lint": "biome lint .",
55
+ "lint:fix": "biome lint --write .",
56
+ "format": "biome format --write .",
57
+ "format:check": "biome format .",
58
+ "check": "bun typecheck && bun lint && bun format:check",
59
+ "check:fix": "bun typecheck && bun lint:fix && bun format",
60
+ "check:version": "bun scripts/check-version.ts",
61
+ "check:changelog": "bun scripts/check-changelog.ts",
62
+ "clean": "rm -rf dist coverage",
63
+ "build": "bun run clean && bun run build:esm && bun run build:cjs && bun run build:esm:testing && bun run build:cjs:testing && bun run build:cli && bun run build:types",
64
+ "build:esm": "bun build src/index.ts --outfile dist/index.mjs --target bun",
65
+ "build:cjs": "bun build src/index.ts --outfile dist/index.js --target node",
66
+ "build:esm:testing": "bun build src/testing.ts --outfile dist/testing.mjs --target bun",
67
+ "build:cjs:testing": "bun build src/testing.ts --outfile dist/testing.js --target node",
68
+ "build:cli": "bun build src/cli/index.ts --outfile dist/cli.js --target node",
69
+ "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
70
+ "test": "bun test",
71
+ "test:node": "node -e \"import('./dist/index.mjs').then(m => { const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('ESM smoke failed: total=' + r.total); console.log('Node.js ESM smoke: ok'); })\" && node -e \"const m = require('./dist/index.js'); const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('CJS smoke failed: total=' + r.total); console.log('Node.js CJS smoke: ok');\"",
72
+ "test:watch": "bun test --watch",
73
+ "test:ci": "bun test --bail",
74
+ "validate": "bun run check && bun run build && bun test:ci",
75
+ "coverage": "bun test --coverage",
76
+ "release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun test",
77
+ "prepublishOnly": "bun run release:dry"
48
78
  },
49
79
  "devDependencies": {
50
- "babel-core": "^6.24.1",
51
- "babel-preset-babili": "^0.1.2",
52
- "babili": "^0.1.2",
53
- "browserify": "^14.4.0",
54
- "coveralls": "^2.13.1",
55
- "docdash": "^0.4.0",
56
- "eslint": "^3.19.0",
57
- "eslint-config-airbnb-base": "^11.2.0",
58
- "eslint-plugin-import": "^2.3.0",
59
- "husky": "^0.13.4",
60
- "istanbul": "^0.4.5",
61
- "jest": "^20.0.4",
62
- "jsdoc": "^3.4.3",
63
- "rimraf": "^2.6.1"
80
+ "@biomejs/biome": "^2.4.9",
81
+ "@types/bun": "^1.3.0",
82
+ "fast-check": "^4.6.0",
83
+ "typescript": "^6.0.2"
64
84
  },
65
85
  "engines": {
66
- "node": ">= 6.0.0",
67
- "npm": ">= 3.8.6"
86
+ "node": ">= 22.0.0"
68
87
  }
69
88
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CLI argument parser.
3
+ *
4
+ * Pure function that parses process.argv-style string arrays into typed options.
5
+ *
6
+ * @module cli/args
7
+ */
8
+
9
+ /**
10
+ * Parsed CLI arguments.
11
+ */
12
+ export type CliArgs = {
13
+ notation: string | undefined;
14
+ verbose: boolean;
15
+ seed: string | undefined;
16
+ showHelp: boolean;
17
+ showVersion: boolean;
18
+ };
19
+
20
+ /**
21
+ * Result of parsing CLI arguments — either success or a usage error.
22
+ */
23
+ export type ParseArgsResult = { ok: true; args: CliArgs } | { ok: false; error: string };
24
+
25
+ /**
26
+ * Parses a raw argument array into typed CLI options.
27
+ *
28
+ * @param argv - Arguments to parse (typically `process.argv.slice(2)`)
29
+ * @returns Parsed result or an error message for usage errors
30
+ */
31
+ export function parseArgs(argv: string[]): ParseArgsResult {
32
+ let verbose = false;
33
+ let seed: string | undefined;
34
+ let showHelp = false;
35
+ let showVersion = false;
36
+ const positional: string[] = [];
37
+
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const arg = argv[i] as string;
40
+
41
+ if (arg === '--help' || arg === '-h') {
42
+ showHelp = true;
43
+ } else if (arg === '--version') {
44
+ showVersion = true;
45
+ } else if (arg === '--verbose' || arg === '-v') {
46
+ verbose = true;
47
+ } else if (arg === '--seed') {
48
+ const next = argv[i + 1];
49
+ if (next === undefined || (next.startsWith('-') && !/^-\d/.test(next))) {
50
+ return { ok: false, error: 'Missing value for --seed' };
51
+ }
52
+ seed = next;
53
+ i++;
54
+ } else if (arg.startsWith('--seed=')) {
55
+ const value = arg.slice('--seed='.length);
56
+ if (value === '') {
57
+ return { ok: false, error: 'Missing value for --seed' };
58
+ }
59
+ seed = value;
60
+ } else if (arg.startsWith('--')) {
61
+ return { ok: false, error: `Unknown option: ${arg}` };
62
+ } else if (arg.startsWith('-') && arg.length > 1 && !/^\d/.test(arg.slice(1))) {
63
+ return { ok: false, error: `Unknown option: ${arg}` };
64
+ } else {
65
+ positional.push(arg);
66
+ }
67
+ }
68
+
69
+ const notation = positional.length > 0 ? positional.join(' ') : undefined;
70
+
71
+ return { ok: true, args: { notation, verbose, seed, showHelp, showVersion } };
72
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * CLI output formatting.
3
+ *
4
+ * Transforms RollResult into terminal-appropriate strings.
5
+ *
6
+ * @module cli/format
7
+ */
8
+
9
+ import type { RollResult } from '../types';
10
+
11
+ /**
12
+ * Formats a roll result for terminal display.
13
+ *
14
+ * In normal mode, returns just the total. In verbose mode, returns the
15
+ * rendered breakdown with terminal-safe formatting for dropped dice.
16
+ *
17
+ * @param result - The roll result to format
18
+ * @param verbose - Whether to show the detailed breakdown
19
+ * @returns Formatted string for terminal output
20
+ */
21
+ export function formatResult(result: RollResult, verbose: boolean): string {
22
+ if (!verbose) {
23
+ return String(result.total);
24
+ }
25
+
26
+ return formatRendered(result.rendered);
27
+ }
28
+
29
+ /**
30
+ * Converts markdown-style dice markers to terminal-friendly forms.
31
+ *
32
+ * The evaluator uses markdown syntax in the rendered field:
33
+ * `~~value~~` — dropped dice
34
+ * `**value**` — dice counted as success
35
+ * `__value__` — dice counted as failure
36
+ *
37
+ * For plain terminals these become `(value)`, `[value]`, and `{value}` so
38
+ * the per-die classification stays visible without any markup dependency.
39
+ */
40
+ function formatRendered(rendered: string): string {
41
+ return rendered
42
+ .replace(/~~(-?\d+)~~/g, '($1)')
43
+ .replace(/\*\*(-?\d+)\*\*/g, '[$1]')
44
+ .replace(/__(-?\d+)__/g, '{$1}');
45
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI entry point for roll-parser.
5
+ *
6
+ * @module cli/index
7
+ */
8
+
9
+ import { isRollParserError } from '../errors';
10
+ import { VERSION } from '../index';
11
+ import { roll } from '../roll';
12
+ import { parseArgs } from './args';
13
+ import { formatResult } from './format';
14
+
15
+ const HELP_TEXT = `roll-parser v${VERSION}
16
+
17
+ Usage: roll-parser <notation> [options]
18
+
19
+ Options:
20
+ -h, --help Show this help message
21
+ --version Show version number
22
+ -v, --verbose Show detailed roll breakdown
23
+ --seed <value> Use seed for reproducible rolls
24
+
25
+ Examples:
26
+ roll-parser 2d6+3
27
+ roll-parser 4d6kh3 --verbose
28
+ roll-parser 4d6dl1 --seed "character-str"
29
+ `;
30
+
31
+ function main(): void {
32
+ const parsed = parseArgs(process.argv.slice(2));
33
+
34
+ if (!parsed.ok) {
35
+ process.stderr.write(`Error: ${parsed.error}\n`);
36
+ process.stderr.write('Run "roll-parser --help" for usage.\n');
37
+ process.exitCode = 2;
38
+ return;
39
+ }
40
+
41
+ const { args } = parsed;
42
+
43
+ if (args.showHelp) {
44
+ process.stdout.write(HELP_TEXT);
45
+ return;
46
+ }
47
+
48
+ if (args.showVersion) {
49
+ process.stdout.write(`${VERSION}\n`);
50
+ return;
51
+ }
52
+
53
+ if (args.notation == null) {
54
+ process.stderr.write('Error: No dice notation provided.\n');
55
+ process.stderr.write('Run "roll-parser --help" for usage.\n');
56
+ process.exitCode = 2;
57
+ return;
58
+ }
59
+
60
+ try {
61
+ const options = args.seed != null ? { seed: args.seed } : {};
62
+ const result = roll(args.notation, options);
63
+ const output = formatResult(result, args.verbose);
64
+ process.stdout.write(`${output}\n`);
65
+ } catch (error) {
66
+ if (isRollParserError(error)) {
67
+ process.stderr.write(`Error: ${error.message}\n`);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ main();
package/src/errors.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Common error base class and error codes for roll-parser.
3
+ *
4
+ * @module errors
5
+ */
6
+
7
+ /**
8
+ * All known roll-parser error codes. Single source of truth — the
9
+ * `RollParserErrorCode` type and the runtime `VALID_CODES` set are
10
+ * both derived from this array.
11
+ *
12
+ * Lexer: `UNEXPECTED_CHARACTER`, `UNEXPECTED_IDENTIFIER`
13
+ * Parser: `UNEXPECTED_TOKEN`, `UNEXPECTED_END`, `EXPECTED_TOKEN`,
14
+ * `INVALID_EXPLODE_TARGET`, `INVALID_SUCCESS_COUNT_TARGET`, `NESTED_VERSUS`,
15
+ * `INVALID_FUNCTION_ARITY`
16
+ * Evaluator: `INVALID_DICE_COUNT`, `INVALID_DICE_SIDES`, `DICE_LIMIT_EXCEEDED`,
17
+ * `DIVISION_BY_ZERO`, `MODULO_BY_ZERO`, `UNKNOWN_OPERATOR`, `UNKNOWN_NODE_TYPE`,
18
+ * `INVALID_MODIFIER_COUNT`, `EXPLODE_LIMIT_EXCEEDED`, `REROLL_LIMIT_EXCEEDED`,
19
+ * `INVALID_THRESHOLD`, `NESTED_VERSUS`, `UNKNOWN_FUNCTION`
20
+ */
21
+ const ROLL_PARSER_ERROR_CODES = [
22
+ 'UNEXPECTED_CHARACTER',
23
+ 'UNEXPECTED_IDENTIFIER',
24
+ 'UNEXPECTED_TOKEN',
25
+ 'UNEXPECTED_END',
26
+ 'EXPECTED_TOKEN',
27
+ 'INVALID_DICE_COUNT',
28
+ 'INVALID_DICE_SIDES',
29
+ 'DICE_LIMIT_EXCEEDED',
30
+ 'DIVISION_BY_ZERO',
31
+ 'MODULO_BY_ZERO',
32
+ 'UNKNOWN_OPERATOR',
33
+ 'UNKNOWN_NODE_TYPE',
34
+ 'INVALID_MODIFIER_COUNT',
35
+ 'EXPLODE_LIMIT_EXCEEDED',
36
+ 'INVALID_EXPLODE_TARGET',
37
+ 'REROLL_LIMIT_EXCEEDED',
38
+ 'INVALID_SUCCESS_COUNT_TARGET',
39
+ 'INVALID_THRESHOLD',
40
+ 'NESTED_VERSUS',
41
+ 'INVALID_FUNCTION_ARITY',
42
+ 'UNKNOWN_FUNCTION',
43
+ ] as const;
44
+
45
+ export type RollParserErrorCode = (typeof ROLL_PARSER_ERROR_CODES)[number];
46
+
47
+ /**
48
+ * Base error class for all roll-parser errors.
49
+ *
50
+ * Provides a typed `code` field for programmatic error handling.
51
+ * All library errors (`LexerError`, `ParseError`, `EvaluatorError`)
52
+ * extend this class.
53
+ */
54
+ export class RollParserError extends Error {
55
+ readonly code: RollParserErrorCode;
56
+
57
+ constructor(message: string, code: RollParserErrorCode) {
58
+ super(message);
59
+ this.name = 'RollParserError';
60
+ this.code = code;
61
+ }
62
+ }
63
+
64
+ const VALID_CODES: Set<string> = new Set<string>(ROLL_PARSER_ERROR_CODES);
65
+
66
+ /**
67
+ * Type guard for roll-parser errors. Checks `instanceof` first, then
68
+ * falls back to duck-typing for cross-realm safety.
69
+ */
70
+ export function isRollParserError(value: unknown): value is RollParserError {
71
+ if (value instanceof RollParserError) return true;
72
+ return (
73
+ value instanceof Error &&
74
+ 'code' in value &&
75
+ typeof (value as RollParserError).code === 'string' &&
76
+ VALID_CODES.has((value as RollParserError).code)
77
+ );
78
+ }