@dicelette/core 1.28.3 → 1.28.4

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/README.md CHANGED
@@ -123,9 +123,6 @@ Note: when a parameter `engine` is shown it usually defaults to the `NumberGener
123
123
 
124
124
  ### Utility functions (`src/utils.ts`)
125
125
 
126
- #### Function: escapeRegex(string: string): string
127
- - Escape input string to be used in a RegExp.
128
-
129
126
  #### Function: standardizeDice(dice: string): string
130
127
  - Standardizes dice notation while preserving bracketed text.
131
128
 
package/dist/index.d.mts CHANGED
@@ -355,6 +355,7 @@ declare function resolveFormulaHint(formula: string, allAttributes: Record<strin
355
355
  declare function calculateSimilarity(str1: string, str2: string): number;
356
356
  /**
357
357
  * Calculates the Levenshtein distance between two strings.
358
+ * Uses two rolling rows instead of a full matrix: O(min(m,n)) space instead of O(m×n).
358
359
  */
359
360
  declare function levenshteinDistance(str1: string, str2: string): number;
360
361
  declare function findBestStatMatch<T>(searchTerm: string, normalizedStats: Map<string, T>, similarityThreshold?: number): T | undefined;
@@ -380,11 +381,6 @@ declare function splitDiceComment(dice: string): {
380
381
  dice: string;
381
382
  comment: string | undefined;
382
383
  };
383
- /**
384
- * Escape regex string
385
- * @param string {string}
386
- */
387
- declare function escapeRegex(string: string): string;
388
384
  /**
389
385
  * Allow to keep the text as if in brackets
390
386
  * @param dice {string}
@@ -463,7 +459,7 @@ declare function evalOneCombinaison(combinaison: string, stats: Record<string, n
463
459
  /**
464
460
  * Parse the provided JSON and verify each field to check if everything could work when rolling
465
461
  * @param {unknown} template
466
- * @param {boolean} verify - If true, will roll the dices to check if everything is valid
462
+ * @param verify - If true, will roll the dices to check if everything is valid
467
463
  * @param engine
468
464
  * @returns {StatisticalTemplate}
469
465
  */
@@ -490,4 +486,4 @@ declare function testStatCombinaison(template: StatisticalTemplate, engine?: Eng
490
486
  */
491
487
  declare function generateRandomStat(total?: number | undefined, max?: number, min?: number, engine?: Engine | null): number;
492
488
 
493
- export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, escapeRegex, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, splitDiceComment, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
489
+ export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, splitDiceComment, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
package/dist/index.d.ts CHANGED
@@ -355,6 +355,7 @@ declare function resolveFormulaHint(formula: string, allAttributes: Record<strin
355
355
  declare function calculateSimilarity(str1: string, str2: string): number;
356
356
  /**
357
357
  * Calculates the Levenshtein distance between two strings.
358
+ * Uses two rolling rows instead of a full matrix: O(min(m,n)) space instead of O(m×n).
358
359
  */
359
360
  declare function levenshteinDistance(str1: string, str2: string): number;
360
361
  declare function findBestStatMatch<T>(searchTerm: string, normalizedStats: Map<string, T>, similarityThreshold?: number): T | undefined;
@@ -380,11 +381,6 @@ declare function splitDiceComment(dice: string): {
380
381
  dice: string;
381
382
  comment: string | undefined;
382
383
  };
383
- /**
384
- * Escape regex string
385
- * @param string {string}
386
- */
387
- declare function escapeRegex(string: string): string;
388
384
  /**
389
385
  * Allow to keep the text as if in brackets
390
386
  * @param dice {string}
@@ -463,7 +459,7 @@ declare function evalOneCombinaison(combinaison: string, stats: Record<string, n
463
459
  /**
464
460
  * Parse the provided JSON and verify each field to check if everything could work when rolling
465
461
  * @param {unknown} template
466
- * @param {boolean} verify - If true, will roll the dices to check if everything is valid
462
+ * @param verify - If true, will roll the dices to check if everything is valid
467
463
  * @param engine
468
464
  * @returns {StatisticalTemplate}
469
465
  */
@@ -490,4 +486,4 @@ declare function testStatCombinaison(template: StatisticalTemplate, engine?: Eng
490
486
  */
491
487
  declare function generateRandomStat(total?: number | undefined, max?: number, min?: number, engine?: Engine | null): number;
492
488
 
493
- export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, escapeRegex, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, splitDiceComment, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
489
+ export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, splitDiceComment, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
package/dist/index.js CHANGED
@@ -41,7 +41,6 @@ __export(index_exports, {
41
41
  createCriticalCustom: () => createCriticalCustom,
42
42
  diceRandomParse: () => diceRandomParse,
43
43
  diceTypeRandomParse: () => diceTypeRandomParse,
44
- escapeRegex: () => escapeRegex,
45
44
  evalCombinaison: () => evalCombinaison,
46
45
  evalOneCombinaison: () => evalOneCombinaison,
47
46
  evalStatsDice: () => evalStatsDice,
@@ -240,6 +239,7 @@ var templateSchema = import_zod.z.object({
240
239
  });
241
240
 
242
241
  // src/regex.ts
242
+ var REGEX_CACHE_MAX = 500;
243
243
  var regexCache = /* @__PURE__ */ new Map();
244
244
  var NORMALIZE_SINGLE_DICE = (str) => str.replace(/\b1d(\d+)/gi, "d$1");
245
245
  var REMOVER_PATTERN = {
@@ -254,6 +254,10 @@ function getCachedRegex(pattern, flags = "") {
254
254
  const key = `${pattern}|${flags}`;
255
255
  let regex = regexCache.get(key);
256
256
  if (!regex) {
257
+ if (regexCache.size >= REGEX_CACHE_MAX) {
258
+ const oldest = regexCache.keys().next().value;
259
+ if (oldest !== void 0) regexCache.delete(oldest);
260
+ }
257
261
  regex = new RegExp(pattern, flags);
258
262
  regexCache.set(key, regex);
259
263
  }
@@ -299,12 +303,12 @@ var import_rpg_dice_roller2 = require("@dice-roller/rpg-dice-roller");
299
303
  function evalStatsDice(testDice, allStats, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto, pity) {
300
304
  let dice = testDice.trimEnd();
301
305
  if (allStats && Object.keys(allStats).length > 0) {
306
+ dice = dice.standardize();
302
307
  const names = Object.keys(allStats);
303
308
  for (const name of names) {
304
- const regex = new RegExp(escapeRegex(name.standardize()), "gi");
305
- if (dice.standardize().match(regex)) {
306
- const statValue = allStats[name];
307
- dice = dice.standardize().replace(regex, statValue.toString()).trimEnd();
309
+ const regex = getCachedRegex(name.standardize().escapeRegex(), "gi");
310
+ if (dice.match(regex)) {
311
+ dice = dice.replace(regex, allStats[name].toString()).trimEnd();
308
312
  }
309
313
  }
310
314
  }
@@ -322,8 +326,8 @@ function diceRandomParse(value, template, engine = import_rpg_dice_roller2.Numbe
322
326
  const statNames = Object.keys(template.statistics);
323
327
  let newDice = value;
324
328
  for (const name of statNames) {
325
- const regex = new RegExp(escapeRegex(name.standardize()), "gi");
326
- if (value.match(regex)) {
329
+ const regex = getCachedRegex(name.standardize().escapeRegex(), "gi");
330
+ if (newDice.match(regex)) {
327
331
  let max;
328
332
  let min;
329
333
  const foundStat = template.statistics?.[name];
@@ -333,7 +337,7 @@ function diceRandomParse(value, template, engine = import_rpg_dice_roller2.Numbe
333
337
  }
334
338
  const total = template.total || 100;
335
339
  const randomStatValue = generateRandomStat(total, max, min, engine);
336
- newDice = value.replace(regex, randomStatValue.toString());
340
+ newDice = newDice.replace(regex, randomStatValue.toString());
337
341
  }
338
342
  }
339
343
  return replaceFormulaInDice(newDice);
@@ -356,7 +360,7 @@ function evalCombinaison(combinaison, stats) {
356
360
  for (const [stat, combin] of Object.entries(combinaison)) {
357
361
  let formula = combin.standardize();
358
362
  for (const [statName, value] of Object.entries(stats)) {
359
- const regex = new RegExp(statName.standardize(), "gi");
363
+ const regex = getCachedRegex(statName.standardize().escapeRegex(), "gi");
360
364
  formula = formula.replace(regex, value.toString());
361
365
  }
362
366
  try {
@@ -370,7 +374,7 @@ function evalCombinaison(combinaison, stats) {
370
374
  function evalOneCombinaison(combinaison, stats) {
371
375
  let formula = combinaison.standardize();
372
376
  for (const [statName, value] of Object.entries(stats)) {
373
- const regex = new RegExp(statName.standardize(), "gi");
377
+ const regex = getCachedRegex(statName.standardize().escapeRegex(), "gi");
374
378
  formula = formula.replace(regex, value.toString());
375
379
  }
376
380
  try {
@@ -418,7 +422,7 @@ function verifyTemplateValue(template, verify = true, engine = import_rpg_dice_r
418
422
  engine
419
423
  );
420
424
  const rolled = roll(cleanedDice2, engine);
421
- if (!rolled) throw new DiceTypeError(cleanedDice2, "no_roll_result", "no roll result");
425
+ if (!rolled) throw new DiceTypeError(cleanedDice2, "roll");
422
426
  }
423
427
  if (statistiqueTemplate.customCritical) {
424
428
  if (!statistiqueTemplate.diceType) {
@@ -443,15 +447,16 @@ function verifyTemplateValue(template, verify = true, engine = import_rpg_dice_r
443
447
  }
444
448
  function testDiceRegistered(template, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto) {
445
449
  if (!template.damage) return;
446
- if (Object.keys(template.damage).length === 0) throw new EmptyObjectError();
447
- if (Object.keys(template.damage).length > 25) throw new TooManyDice();
448
- for (const [name, dice] of Object.entries(template.damage)) {
450
+ const damageEntries = Object.entries(template.damage);
451
+ if (damageEntries.length === 0) throw new EmptyObjectError();
452
+ if (damageEntries.length > 25) throw new TooManyDice();
453
+ for (const [name, dice] of damageEntries) {
449
454
  if (!dice) continue;
450
455
  const diceReplaced = replaceExpByRandom(dice);
451
456
  const randomDiceParsed = diceRandomParse(diceReplaced, template, engine);
452
457
  try {
453
458
  const rolled = roll(randomDiceParsed, engine);
454
- if (!rolled) throw new DiceTypeError(name, "no_roll_result", dice);
459
+ if (!rolled) throw new DiceTypeError(name, "testDiceRegistered", dice);
455
460
  } catch (error) {
456
461
  throw new DiceTypeError(name, "testDiceRegistered", error);
457
462
  }
@@ -459,19 +464,14 @@ function testDiceRegistered(template, engine = import_rpg_dice_roller2.NumberGen
459
464
  }
460
465
  function testStatCombinaison(template, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto) {
461
466
  if (!template.statistics) return;
462
- const onlycombinaisonStats = Object.fromEntries(
463
- Object.entries(template.statistics).filter(
464
- ([_, value]) => value.combinaison !== void 0
465
- )
466
- );
467
- const allOtherStats = Object.fromEntries(
468
- Object.entries(template.statistics).filter(([_, value]) => !value.combinaison)
469
- );
467
+ const onlycombinaisonStats = {};
468
+ const allOtherStats = {};
469
+ for (const [k, v] of Object.entries(template.statistics)) {
470
+ if (v.combinaison !== void 0) onlycombinaisonStats[k] = v;
471
+ else allOtherStats[k] = v;
472
+ }
470
473
  if (Object.keys(onlycombinaisonStats).length === 0) return;
471
- const allStats = Object.keys(template.statistics).filter(
472
- (stat) => !template.statistics[stat].combinaison
473
- );
474
- if (allStats.length === 0) throw new NoStatisticsError();
474
+ if (Object.keys(allOtherStats).length === 0) throw new NoStatisticsError();
475
475
  const error = [];
476
476
  for (const [stat, value] of Object.entries(onlycombinaisonStats)) {
477
477
  let formula = value.combinaison;
@@ -479,7 +479,7 @@ function testStatCombinaison(template, engine = import_rpg_dice_roller2.NumberGe
479
479
  const { max, min } = data;
480
480
  const total = template.total || 100;
481
481
  const randomStatValue = generateRandomStat(total, max, min, engine);
482
- const regex = new RegExp(other, "gi");
482
+ const regex = getCachedRegex(other.escapeRegex(), "gi");
483
483
  formula = formula.replace(regex, randomStatValue.toString());
484
484
  }
485
485
  try {
@@ -492,27 +492,21 @@ function testStatCombinaison(template, engine = import_rpg_dice_roller2.NumberGe
492
492
  return;
493
493
  }
494
494
  function generateRandomStat(total = 100, max, min, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto) {
495
- let randomStatValue = total + 1;
495
+ if (total <= 1) throw new RangeError(`total must be greater than 1, got ${total}`);
496
496
  const random = new import_random_js.Random(engine || import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto);
497
- while (randomStatValue >= total || randomStatValue === 0) {
498
- if (max && min) randomStatValue = randomInt(min, max, engine, random);
499
- else if (max) randomStatValue = randomInt(1, max, engine, random);
500
- else if (min) randomStatValue = randomInt(min, total, engine, random);
501
- else randomStatValue = randomInt(1, total, engine, random);
502
- }
503
- return randomStatValue;
497
+ const effectiveMin = Math.max(min ?? 1, 1);
498
+ const effectiveMax = Math.min(max ?? total - 1, total - 1);
499
+ if (effectiveMin > effectiveMax) throw new MaxGreater(effectiveMin, effectiveMax);
500
+ return randomInt(effectiveMin, effectiveMax, engine, random);
504
501
  }
505
502
 
506
503
  // src/utils.ts
507
504
  function splitDiceComment(dice) {
508
- const match = /\s+(#|\/{2}|\[|\/\*)(?<comment>.*)/i.exec(dice);
505
+ const match = /\s(#|\/{2}|\[|\/\*)(?<comment>.*)/i.exec(dice);
509
506
  if (!match?.groups) return { dice: dice.trimEnd(), comment: void 0 };
510
507
  const comment = match.groups.comment.trim() || void 0;
511
508
  return { dice: dice.slice(0, match.index).trimEnd(), comment };
512
509
  }
513
- function escapeRegex(string) {
514
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
515
- }
516
510
  function standardizeDice(dice) {
517
511
  return dice.replace(
518
512
  /(\[[^\]]+])|([^[]+)/g,
@@ -657,32 +651,31 @@ var import_rpg_dice_roller5 = require("@dice-roller/rpg-dice-roller");
657
651
  var import_mathjs3 = require("mathjs");
658
652
 
659
653
  // src/dice/replace.ts
654
+ var PARENTHESIS_REGEX = /d\((\d+)\)/g;
655
+ var BRACKET_COMMENT_REGEX = /\[(?<comments>.*?)\]/;
656
+ var OPTIONAL_COMMENT_REGEX = /\s+(#|\/\/)(?<comment>.*)/;
660
657
  function replaceUnwantedText(dice, sortOrder) {
661
- const d = dice.replaceAll(/[{}]/g, "").replaceAll(/s[ad]/gi, "");
662
- if (sortOrder) return sortDice(d, sortOrder);
658
+ let d = dice.replaceAll(/[{}]/g, "").replaceAll(/s[ad]/gi, "");
659
+ if (sortOrder) d = sortDice(d, sortOrder);
660
+ if (!d.length) throw new DiceTypeError(dice, "empty_dice");
663
661
  return d;
664
662
  }
665
663
  function sortDice(dice, sortOrder) {
666
664
  if (sortOrder === "none" /* None */) return dice;
667
665
  const dices = dice.split(/; ?/);
666
+ const decorated = dices.map((d) => ({
667
+ d,
668
+ v: Number.parseInt(d.split("= ")[1], 10) || 0
669
+ }));
668
670
  if (sortOrder === "sa" /* Ascending */) {
669
- dices.sort((a, b) => {
670
- const totalA = Number.parseInt(a.split("= ")[1], 10) || 0;
671
- const totalB = Number.parseInt(b.split("= ")[1], 10) || 0;
672
- return totalB - totalA;
673
- });
671
+ decorated.sort((a, b) => b.v - a.v);
674
672
  } else if (sortOrder === "sd" /* Descending */) {
675
- dices.sort((a, b) => {
676
- const totalA = Number.parseInt(a.split("= ")[1], 10) || 0;
677
- const totalB = Number.parseInt(b.split("= ")[1], 10) || 0;
678
- return totalA - totalB;
679
- });
673
+ decorated.sort((a, b) => a.v - b.v);
680
674
  }
681
- return dices.join("; ");
675
+ return decorated.map((x) => x.d).join("; ");
682
676
  }
683
677
  function fixParenthesis(dice) {
684
- const parenthesisRegex = /d\((\d+)\)/g;
685
- return dice.replaceAll(parenthesisRegex, (_match, p1) => `d${p1}`);
678
+ return dice.replaceAll(PARENTHESIS_REGEX, (_match, p1) => `d${p1}`);
686
679
  }
687
680
  function replaceText(element, total, dice) {
688
681
  return {
@@ -691,12 +684,10 @@ function replaceText(element, total, dice) {
691
684
  };
692
685
  }
693
686
  function formatComment(dice) {
694
- const commentsRegex = /\[(?<comments>.*?)\]/;
695
- const commentsMatch = commentsRegex.exec(dice);
687
+ const commentsMatch = BRACKET_COMMENT_REGEX.exec(dice);
696
688
  const comments = commentsMatch?.groups?.comments ? `${commentsMatch.groups.comments}` : "";
697
- const diceWithoutBrackets = dice.replace(commentsRegex, "");
698
- const optionalCommentsRegex = /\s+(#|\/\/)(?<comment>.*)/;
699
- const optionalComments = optionalCommentsRegex.exec(diceWithoutBrackets);
689
+ const diceWithoutBrackets = dice.replace(BRACKET_COMMENT_REGEX, "");
690
+ const optionalComments = OPTIONAL_COMMENT_REGEX.exec(diceWithoutBrackets);
700
691
  const optional = optionalComments?.groups?.comment ? `${optionalComments.groups.comment.trim()}` : "";
701
692
  let finalComment = "";
702
693
  if (comments && optional) finalComment = `__${comments} ${optional}__ \u2014 `;
@@ -848,23 +839,27 @@ function calculateSimilarity(str1, str2) {
848
839
  return (longer.length - distance) / longer.length;
849
840
  }
850
841
  function levenshteinDistance(str1, str2) {
851
- const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
852
- for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
853
- for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
842
+ if (str1.length > str2.length) {
843
+ [str1, str2] = [str2, str1];
844
+ }
845
+ let prev = Array.from({ length: str1.length + 1 }, (_, i) => i);
846
+ let curr = new Array(str1.length + 1);
854
847
  for (let j = 1; j <= str2.length; j++) {
848
+ curr[0] = j;
855
849
  for (let i = 1; i <= str1.length; i++) {
856
850
  const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
857
- matrix[j][i] = Math.min(
858
- matrix[j][i - 1] + 1,
851
+ curr[i] = Math.min(
852
+ curr[i - 1] + 1,
859
853
  // insertion
860
- matrix[j - 1][i] + 1,
854
+ prev[i] + 1,
861
855
  // deletion
862
- matrix[j - 1][i - 1] + cost
856
+ prev[i - 1] + cost
863
857
  // substitution
864
858
  );
865
859
  }
860
+ [prev, curr] = [curr, prev];
866
861
  }
867
- return matrix[str2.length][str1.length];
862
+ return prev[str1.length];
868
863
  }
869
864
  function findBestStatMatch(searchTerm, normalizedStats, similarityThreshold = MIN_THRESHOLD_MATCH) {
870
865
  const exact = normalizedStats.get(searchTerm);
@@ -1141,25 +1136,20 @@ function prepareDice(diceInput) {
1141
1136
  dice = dice.replaceAll(REMOVER_PATTERN.CRITICAL_BLOCK, "").trimEnd();
1142
1137
  const explodingSuccess = normalizeExplodingSuccess(dice);
1143
1138
  if (explodingSuccess) dice = explodingSuccess.dice;
1144
- let diceDisplay;
1145
- if (dice.includes(";")) {
1146
- const mainDice = dice.split(";")[0];
1147
- diceDisplay = explodingSuccess?.originalDice ?? mainDice;
1148
- } else {
1149
- diceDisplay = explodingSuccess?.originalDice ?? dice;
1150
- }
1139
+ const sharedSeparatorIndex = dice.indexOf(";");
1140
+ const hasSharedSeparator = sharedSeparatorIndex !== -1;
1141
+ let diceDisplay = explodingSuccess?.originalDice ?? (hasSharedSeparator ? dice.slice(0, sharedSeparatorIndex) : dice);
1151
1142
  const curlyBulkMatch = dice.match(/^\{(\d+#.*)\}$/);
1152
1143
  const isCurlyBulk = !!curlyBulkMatch;
1153
1144
  const bulkContent = isCurlyBulk ? curlyBulkMatch[1] : "";
1154
- const isSharedRoll = dice.includes(";");
1155
1145
  let isSharedCurly = false;
1156
- if (isSharedRoll && dice.match(/^\{.*;\s*.*\}$/)) {
1146
+ if (hasSharedSeparator && dice.match(/^\{.*;\s*.*\}$/)) {
1157
1147
  dice = dice.slice(1, -1);
1158
1148
  isSharedCurly = true;
1159
1149
  diceDisplay = diceDisplay.slice(1);
1160
1150
  }
1161
1151
  let isSimpleCurly = false;
1162
- if (!isCurlyBulk && !isSharedRoll && dice.match(/^\{.*\}$/)) {
1152
+ if (!isCurlyBulk && !hasSharedSeparator && dice.match(/^\{.*\}$/)) {
1163
1153
  const innerContent = dice.slice(1, -1);
1164
1154
  const hasModifiers = innerContent.match(/[+\-*/%^]/);
1165
1155
  const hasComparison = innerContent.match(/(([><=!]+\d+f)|([><=]|!=)+\d+)/);
@@ -1172,7 +1162,7 @@ function prepareDice(diceInput) {
1172
1162
  dice,
1173
1163
  diceDisplay,
1174
1164
  explodingSuccess,
1175
- isSharedRoll,
1165
+ isSharedRoll: hasSharedSeparator,
1176
1166
  isSharedCurly,
1177
1167
  isCurlyBulk,
1178
1168
  bulkContent,
@@ -1189,7 +1179,13 @@ function getSortOrder(dice) {
1189
1179
  function handleBulkRolls(dice, isCurlyBulk, bulkContent, compare, explodingSuccess, diceDisplay, engine, sort) {
1190
1180
  const bulkProcessContent = isCurlyBulk ? bulkContent : dice;
1191
1181
  const diceArray = bulkProcessContent.split("#");
1182
+ if (!isNumber(diceArray[0])) {
1183
+ throw new DiceTypeError(dice, "bulk_number");
1184
+ }
1192
1185
  const numberOfDice = Number.parseInt(diceArray[0], 10);
1186
+ if (numberOfDice <= 0) {
1187
+ throw new DiceTypeError(dice, "bulk_zero");
1188
+ }
1193
1189
  const { dice: diceToRollBase, comment: comments } = splitDiceComment(diceArray[1]);
1194
1190
  let diceToRoll = diceToRollBase;
1195
1191
  let curlyCompare;
@@ -1372,6 +1368,7 @@ function handlePitySystem(dice, compare, diceRoll, roller, engine) {
1372
1368
  isFail = (0, import_mathjs9.evaluate)(`${res.total}${compare.sign}${compare.value}`);
1373
1369
  }
1374
1370
  }
1371
+ if (!res?.result.length) throw new DiceTypeError(dice, "empty_dice");
1375
1372
  return { rerollCount, result: res };
1376
1373
  }
1377
1374
 
@@ -1473,6 +1470,7 @@ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nod
1473
1470
  prepared.explodingSuccess.normalizedSegment,
1474
1471
  prepared.explodingSuccess.originalSegment
1475
1472
  );
1473
+ if (!resultOutput.length) throw new DiceTypeError(dice, "empty_dice");
1476
1474
  return {
1477
1475
  dice: prepared.isSimpleCurly ? finalDiceDisplay : prepared.diceDisplay,
1478
1476
  result: resultOutput,
@@ -1683,7 +1681,6 @@ function replaceInFormula(element, diceResult, compareResult, res, engine = impo
1683
1681
  createCriticalCustom,
1684
1682
  diceRandomParse,
1685
1683
  diceTypeRandomParse,
1686
- escapeRegex,
1687
1684
  evalCombinaison,
1688
1685
  evalOneCombinaison,
1689
1686
  evalStatsDice,