roll-parser 3.3.0 → 3.4.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 +42 -1
- package/README.md +60 -32
- package/dist/cli/args.d.ts +11 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +67 -42
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/format.d.ts +8 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +4 -3
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/main.d.ts +3 -3
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +50 -19
- package/dist/cli/main.js.map +1 -1
- package/dist/evaluator/evaluator.d.ts +15 -3
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +134 -105
- package/dist/evaluator/evaluator.js.map +1 -1
- package/dist/evaluator/modifiers/flags.d.ts +7 -1
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -1
- package/dist/evaluator/modifiers/flags.js +9 -1
- package/dist/evaluator/modifiers/flags.js.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.js +19 -16
- package/dist/evaluator/modifiers/keep-drop.js.map +1 -1
- package/dist/lexer/lexer.d.ts +5 -0
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +3 -0
- package/dist/lexer/lexer.js.map +1 -1
- package/dist/parser/parser.d.ts +1 -0
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +23 -20
- package/dist/parser/parser.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +2 -1
- package/dist/render.js.map +1 -1
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/rng/seeded.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -14
- package/src/cli/args.ts +112 -40
- package/src/cli/format.ts +11 -3
- package/src/cli/main.ts +97 -22
- package/src/evaluator/evaluator.ts +189 -150
- package/src/evaluator/modifiers/flags.ts +18 -2
- package/src/evaluator/modifiers/keep-drop.ts +42 -16
- package/src/lexer/lexer.ts +8 -0
- package/src/parser/parser.ts +36 -32
- package/src/render.ts +2 -1
- package/src/rng/seeded.ts +4 -0
- package/src/version.ts +1 -1
|
@@ -68,11 +68,27 @@ export function stripFlags(
|
|
|
68
68
|
return modifiers.filter((modifier) => !excluded.includes(modifier));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Returns `modifiers` with `excluded` removed and `added` appended, in order.
|
|
73
|
+
*
|
|
74
|
+
* ! Always a fresh array. `mergeMetaRolls` clones a die as
|
|
75
|
+
* ! `{ ...die, modifiers: rewriteFlags(...) }`, so returning the input when it
|
|
76
|
+
* ! already matches would leave clone and original sharing mutated flags.
|
|
77
|
+
*/
|
|
72
78
|
export function rewriteFlags(
|
|
73
79
|
modifiers: readonly DieModifier[],
|
|
74
80
|
excluded: readonly DieModifier[],
|
|
75
81
|
...added: DieModifier[]
|
|
76
82
|
): DieModifier[] {
|
|
77
|
-
|
|
83
|
+
// One array, not `stripFlags`'s filter plus a spread — this runs per die.
|
|
84
|
+
const rewritten: DieModifier[] = [];
|
|
85
|
+
|
|
86
|
+
for (const modifier of modifiers) {
|
|
87
|
+
if (!excluded.includes(modifier)) rewritten.push(modifier);
|
|
88
|
+
}
|
|
89
|
+
for (const flag of added) {
|
|
90
|
+
rewritten.push(flag);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return rewritten;
|
|
78
94
|
}
|
|
@@ -10,6 +10,29 @@ import { isVersusDc } from './flags.js';
|
|
|
10
10
|
/** One selectable die: its rolled value and its slot in the original pool. */
|
|
11
11
|
type EligibleDie = { result: number; index: number };
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The die at pool slot `index` when it is eligible for keep/drop selection,
|
|
15
|
+
* `null` otherwise. Marks an ineligible-but-droppable die in `droppedMask` on
|
|
16
|
+
* the way past.
|
|
17
|
+
*/
|
|
18
|
+
function selectOrMarkDropped(
|
|
19
|
+
dice: DieResult[],
|
|
20
|
+
index: number,
|
|
21
|
+
droppedMask: Uint8Array,
|
|
22
|
+
hasVersusDc: boolean,
|
|
23
|
+
): DieResult | null {
|
|
24
|
+
const die = dice[index];
|
|
25
|
+
if (die == null) return null;
|
|
26
|
+
if (hasVersusDc && isVersusDc(die)) return null;
|
|
27
|
+
|
|
28
|
+
if (die.modifiers.includes('dropped')) {
|
|
29
|
+
droppedMask[index] = 1;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return die;
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
/**
|
|
14
37
|
* Records into `droppedMask` every pool slot that `kind` / `selector` /
|
|
15
38
|
* `count` drops.
|
|
@@ -39,14 +62,8 @@ export function markDroppedIndices(
|
|
|
39
62
|
const eligible: EligibleDie[] = [];
|
|
40
63
|
|
|
41
64
|
for (let index = 0; index < dice.length; index++) {
|
|
42
|
-
const die = dice
|
|
65
|
+
const die = selectOrMarkDropped(dice, index, droppedMask, hasVersusDc);
|
|
43
66
|
if (die == null) continue;
|
|
44
|
-
if (hasVersusDc && isVersusDc(die)) continue;
|
|
45
|
-
|
|
46
|
-
if (die.modifiers.includes('dropped')) {
|
|
47
|
-
droppedMask[index] = 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
67
|
|
|
51
68
|
eligible.push({ result: die.result, index });
|
|
52
69
|
}
|
|
@@ -64,6 +81,20 @@ export function markDroppedIndices(
|
|
|
64
81
|
return;
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
dropBySelection(eligible, count, isKeep, selector, droppedMask);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reached only once both whole-pool cases are ruled out, so the selected range
|
|
89
|
+
* is always a strict subset of `eligible`.
|
|
90
|
+
*/
|
|
91
|
+
function dropBySelection(
|
|
92
|
+
eligible: EligibleDie[],
|
|
93
|
+
count: number,
|
|
94
|
+
isKeep: boolean,
|
|
95
|
+
selector: KeepDropSpec['selector'],
|
|
96
|
+
droppedMask: Uint8Array,
|
|
97
|
+
): void {
|
|
67
98
|
// Stable sort — ties resolve by original pool order.
|
|
68
99
|
eligible.sort(
|
|
69
100
|
selector === 'highest' ? (a, b) => b.result - a.result : (a, b) => a.result - b.result,
|
|
@@ -95,20 +126,15 @@ function markSingleExtreme(
|
|
|
95
126
|
hasVersusDc: boolean,
|
|
96
127
|
): void {
|
|
97
128
|
const isKeep = kind === 'keep';
|
|
98
|
-
const
|
|
129
|
+
const isMoreExtreme =
|
|
130
|
+
selector === 'highest' ? (a: number, b: number) => a > b : (a: number, b: number) => a < b;
|
|
99
131
|
|
|
100
132
|
let extremeIndex = -1;
|
|
101
133
|
let extremeResult = 0;
|
|
102
134
|
|
|
103
135
|
for (let index = 0; index < dice.length; index++) {
|
|
104
|
-
const die = dice
|
|
136
|
+
const die = selectOrMarkDropped(dice, index, droppedMask, hasVersusDc);
|
|
105
137
|
if (die == null) continue;
|
|
106
|
-
if (hasVersusDc && isVersusDc(die)) continue;
|
|
107
|
-
|
|
108
|
-
if (die.modifiers.includes('dropped')) {
|
|
109
|
-
droppedMask[index] = 1;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
138
|
|
|
113
139
|
const { result } = die;
|
|
114
140
|
|
|
@@ -118,7 +144,7 @@ function markSingleExtreme(
|
|
|
118
144
|
continue;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
|
-
if (
|
|
147
|
+
if (isMoreExtreme(result, extremeResult)) {
|
|
122
148
|
// A keep drops the dethroned extreme; a drop keeps everything else.
|
|
123
149
|
if (isKeep) droppedMask[extremeIndex] = 1;
|
|
124
150
|
extremeIndex = index;
|
package/src/lexer/lexer.ts
CHANGED
|
@@ -215,6 +215,14 @@ export class Lexer {
|
|
|
215
215
|
return this.scanAt();
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
return this.scanOperator(char, startPos);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Single- and two-character operators. The last resort in `nextToken`'s
|
|
223
|
+
* dispatch, so an unrecognised character can only be a lexer error.
|
|
224
|
+
*/
|
|
225
|
+
private scanOperator(char: string, startPos: number): Token {
|
|
218
226
|
this.advance();
|
|
219
227
|
|
|
220
228
|
switch (char) {
|
package/src/parser/parser.ts
CHANGED
|
@@ -850,6 +850,41 @@ export class Parser {
|
|
|
850
850
|
}
|
|
851
851
|
}
|
|
852
852
|
|
|
853
|
+
// ! A single-sub-roll Group is the flat-pool escape hatch, so `containsDicePool`
|
|
854
|
+
// ! deep-walks the arithmetic that `(1d6+5)kh1` rejects outright — but the flat
|
|
855
|
+
// ! path totals `sumKeptDice`, faces only. `{2d6+3}kh2` and `{2d6-1d4}kh3` are
|
|
856
|
+
// ! exactly the drop that reject exists to prevent.
|
|
857
|
+
private rejectSingleSubGroupTarget(target: ASTNode, token: Token): void {
|
|
858
|
+
const base = unwrapAllTransparent(target);
|
|
859
|
+
if (base.type !== 'Group' || base.expressions.length !== 1) return;
|
|
860
|
+
|
|
861
|
+
const inner = base.expressions[0];
|
|
862
|
+
if (inner == null) return;
|
|
863
|
+
|
|
864
|
+
// Single-sub Groups hide a count from the shallow reject in `parseKeepDrop`,
|
|
865
|
+
// so peel them to any depth and `{4d6>=5}kh1`, `{{4d6>=5}}kh1`, and
|
|
866
|
+
// `(4d6>=5)kh1` all report one code. Peeling only picks which error the
|
|
867
|
+
// caller sees — `sumsToKeptFaces` below refuses a count either way.
|
|
868
|
+
let innermost = inner;
|
|
869
|
+
while (true) {
|
|
870
|
+
const peeled = unwrapGrouped(innermost);
|
|
871
|
+
if (peeled.type !== 'Group' || peeled.expressions.length !== 1) break;
|
|
872
|
+
const next = peeled.expressions[0];
|
|
873
|
+
if (next == null) break;
|
|
874
|
+
innermost = next;
|
|
875
|
+
}
|
|
876
|
+
this.rejectSuccessCountTarget(innermost, token);
|
|
877
|
+
|
|
878
|
+
if (!sumsToKeptFaces(inner)) {
|
|
879
|
+
throw new ParseError(
|
|
880
|
+
`Keep/drop on a single-sub-roll group requires added dice terms only`,
|
|
881
|
+
'INVALID_KEEP_DROP_TARGET',
|
|
882
|
+
token.position,
|
|
883
|
+
token,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
853
888
|
private parseKeepDrop(target: ASTNode, token: Token): KeepDropNode {
|
|
854
889
|
this.rejectSuccessCountTarget(target, token);
|
|
855
890
|
// ! Keep/drop on a Versus target silently drops `degree`/`natural` metadata,
|
|
@@ -869,38 +904,7 @@ export class Parser {
|
|
|
869
904
|
);
|
|
870
905
|
}
|
|
871
906
|
|
|
872
|
-
|
|
873
|
-
// ! deep-walks the arithmetic that `(1d6+5)kh1` rejects outright — but the flat
|
|
874
|
-
// ! path totals `sumKeptDice`, faces only. `{2d6+3}kh2` and `{2d6-1d4}kh3` are
|
|
875
|
-
// ! exactly the drop that reject exists to prevent.
|
|
876
|
-
const base = unwrapAllTransparent(target);
|
|
877
|
-
if (base.type === 'Group' && base.expressions.length === 1) {
|
|
878
|
-
const inner = base.expressions[0];
|
|
879
|
-
if (inner != null) {
|
|
880
|
-
// Single-sub Groups hide a count from the shallow reject at the top of this
|
|
881
|
-
// method, so peel them to any depth and `{4d6>=5}kh1`, `{{4d6>=5}}kh1`, and
|
|
882
|
-
// `(4d6>=5)kh1` all report one code. Peeling only picks which error the
|
|
883
|
-
// caller sees — `sumsToKeptFaces` below refuses a count either way.
|
|
884
|
-
let innermost = inner;
|
|
885
|
-
while (true) {
|
|
886
|
-
const peeled = unwrapGrouped(innermost);
|
|
887
|
-
if (peeled.type !== 'Group' || peeled.expressions.length !== 1) break;
|
|
888
|
-
const next = peeled.expressions[0];
|
|
889
|
-
if (next == null) break;
|
|
890
|
-
innermost = next;
|
|
891
|
-
}
|
|
892
|
-
this.rejectSuccessCountTarget(innermost, token);
|
|
893
|
-
|
|
894
|
-
if (!sumsToKeptFaces(inner)) {
|
|
895
|
-
throw new ParseError(
|
|
896
|
-
`Keep/drop on a single-sub-roll group requires added dice terms only`,
|
|
897
|
-
'INVALID_KEEP_DROP_TARGET',
|
|
898
|
-
token.position,
|
|
899
|
-
token,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
907
|
+
this.rejectSingleSubGroupTarget(target, token);
|
|
904
908
|
|
|
905
909
|
const kind =
|
|
906
910
|
token.type === TokenType.KEEP_HIGH || token.type === TokenType.KEEP_LOW ? 'keep' : 'drop';
|
package/src/render.ts
CHANGED
|
@@ -342,8 +342,9 @@ function renderGroup(
|
|
|
342
342
|
plain: boolean,
|
|
343
343
|
): string {
|
|
344
344
|
const { keptIndices } = part;
|
|
345
|
+
const kept = keptIndices == null ? null : new Set(keptIndices);
|
|
345
346
|
const subRolls = part.parts.map((sub, index) => {
|
|
346
|
-
if (
|
|
347
|
+
if (kept == null || kept.has(index)) {
|
|
347
348
|
return renderPart(sub, marks, plain);
|
|
348
349
|
}
|
|
349
350
|
const inner = renderPart(sub, marks, true);
|
package/src/rng/seeded.ts
CHANGED
|
@@ -246,6 +246,10 @@ export class SeededRNG implements RNG {
|
|
|
246
246
|
*/
|
|
247
247
|
private toSeedString(seed?: string | number): string {
|
|
248
248
|
// ! The library's one permitted `Math.random()` site — both draws stay here.
|
|
249
|
+
// ! Decimal double-to-string is a JIT fast path and other radixes are not, so
|
|
250
|
+
// ! every shorter encoding measured slower despite the shorter `hashSeed` loop.
|
|
251
|
+
// ! Only truncating each draw to a uint32 beats it, at 64 draw bits against the
|
|
252
|
+
// ! ~100 documented above. A faster auto-seed must skip the string, not shorten it.
|
|
249
253
|
if (seed == null) return `${Date.now()}-${Math.random()}-${Math.random()}`;
|
|
250
254
|
return String(seed);
|
|
251
255
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by `bun run generate:version` from package.json — do not edit.
|
|
2
|
-
export const version = '3.
|
|
2
|
+
export const version = '3.4.0';
|