test: add Vitest + 30 dice-engine tests
- Install vitest and @vitest/coverage-v8 - Add test scripts to package.json - Create vitest.config.ts - Add 30 comprehensive tests for parseDiceNotation, rollDice, rollAdvantage, buildNotation, isAdvancedNotation - Fix isAdvancedNotation regex to detect ro (reroll once)
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
parseDiceNotation,
|
||||
rollDice,
|
||||
rollAdvantage,
|
||||
buildNotation,
|
||||
isAdvancedNotation,
|
||||
} from "./dice-engine";
|
||||
|
||||
describe("parseDiceNotation", () => {
|
||||
it('parses basic notation "d6"', () => {
|
||||
const parsed = parseDiceNotation("d6");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.count).toBe(1);
|
||||
expect(parsed!.sides).toBe(6);
|
||||
expect(parsed!.modifier).toBe(0);
|
||||
expect(parsed!.keepDrop).toBeNull();
|
||||
expect(parsed!.explode.active).toBe(false);
|
||||
expect(parsed!.reroll.active).toBe(false);
|
||||
});
|
||||
|
||||
it('parses "2d20+3"', () => {
|
||||
const parsed = parseDiceNotation("2d20+3");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.count).toBe(2);
|
||||
expect(parsed!.sides).toBe(20);
|
||||
expect(parsed!.modifier).toBe(3);
|
||||
});
|
||||
|
||||
it('parses "3d8-1"', () => {
|
||||
const parsed = parseDiceNotation("3d8-1");
|
||||
expect(parsed!.count).toBe(3);
|
||||
expect(parsed!.sides).toBe(8);
|
||||
expect(parsed!.modifier).toBe(-1);
|
||||
});
|
||||
|
||||
it('parses "4d6kh3"', () => {
|
||||
const parsed = parseDiceNotation("4d6kh3");
|
||||
expect(parsed!.keepDrop).toEqual({ type: "kh", count: 3 });
|
||||
});
|
||||
|
||||
it('parses "5d6dl2"', () => {
|
||||
const parsed = parseDiceNotation("5d6dl2");
|
||||
expect(parsed!.keepDrop).toEqual({ type: "dl", count: 2 });
|
||||
});
|
||||
|
||||
it('parses exploding "3d6!"', () => {
|
||||
const parsed = parseDiceNotation("3d6!");
|
||||
expect(parsed!.explode.active).toBe(true);
|
||||
expect(parsed!.explode.penetrating).toBe(false);
|
||||
expect(parsed!.explode.threshold).toBe(6);
|
||||
});
|
||||
|
||||
it('parses penetrating explode "2d10!p"', () => {
|
||||
const parsed = parseDiceNotation("2d10!p");
|
||||
expect(parsed!.explode.active).toBe(true);
|
||||
expect(parsed!.explode.penetrating).toBe(true);
|
||||
});
|
||||
|
||||
it('parses explode with custom threshold "2d6!>4"', () => {
|
||||
const parsed = parseDiceNotation("2d6!>4");
|
||||
expect(parsed!.explode.active).toBe(true);
|
||||
expect(parsed!.explode.threshold).toBe(4);
|
||||
});
|
||||
|
||||
it('parses reroll "2d6r1"', () => {
|
||||
const parsed = parseDiceNotation("2d6r1");
|
||||
expect(parsed!.reroll.active).toBe(true);
|
||||
expect(parsed!.reroll.once).toBe(false);
|
||||
expect(parsed!.reroll.values).toContain(1);
|
||||
});
|
||||
|
||||
it('parses reroll once "2d6ro1"', () => {
|
||||
const parsed = parseDiceNotation("2d6ro1");
|
||||
expect(parsed!.reroll.active).toBe(true);
|
||||
expect(parsed!.reroll.once).toBe(true);
|
||||
});
|
||||
|
||||
it('parses reroll less-than "2d6r<3"', () => {
|
||||
const parsed = parseDiceNotation("2d6r<3");
|
||||
expect(parsed!.reroll.active).toBe(true);
|
||||
expect(parsed!.reroll.values).toContain(1);
|
||||
expect(parsed!.reroll.values).toContain(2);
|
||||
expect(parsed!.reroll.values).not.toContain(3);
|
||||
});
|
||||
|
||||
it("ignores whitespace", () => {
|
||||
const parsed = parseDiceNotation(" 2 d 20 + 5 ");
|
||||
expect(parsed!.count).toBe(2);
|
||||
expect(parsed!.sides).toBe(20);
|
||||
expect(parsed!.modifier).toBe(5);
|
||||
});
|
||||
|
||||
it("returns null for invalid notation", () => {
|
||||
expect(parseDiceNotation("")).toBeNull();
|
||||
expect(parseDiceNotation("abc")).toBeNull();
|
||||
expect(parseDiceNotation("2d6+1+2")).toBeNull();
|
||||
expect(parseDiceNotation("0d6")).toBeNull();
|
||||
expect(parseDiceNotation("21d6")).toBeNull();
|
||||
expect(parseDiceNotation("2d0")).toBeNull();
|
||||
expect(parseDiceNotation("2d6kh0")).toBeNull();
|
||||
expect(parseDiceNotation("2d6kh2")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollDice", () => {
|
||||
let mathRandomSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
afterEach(() => {
|
||||
mathRandomSpy?.mockRestore();
|
||||
});
|
||||
|
||||
it("rolls basic dice", () => {
|
||||
mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
const parsed = parseDiceNotation("3d6")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice).toHaveLength(3);
|
||||
expect(result.kept).toHaveLength(3);
|
||||
expect(result.dropped).toHaveLength(0);
|
||||
expect(result.total).toBe(12); // each die: floor(0.5*6)+1 = 4, 3*4 = 12
|
||||
});
|
||||
|
||||
it("applies modifier", () => {
|
||||
mathRandomSpy = vi.spyOn(Math, "random").mockReturnValue(0.5);
|
||||
const parsed = parseDiceNotation("1d20+5")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.total).toBe(16); // 11 + 5
|
||||
});
|
||||
|
||||
it("keeps highest rolls (kh)", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.1) // 1
|
||||
.mockReturnValueOnce(0.9) // 6
|
||||
.mockReturnValueOnce(0.5); // 4
|
||||
|
||||
const parsed = parseDiceNotation("3d6kh2")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.kept).toHaveLength(2);
|
||||
expect(result.dropped).toHaveLength(1);
|
||||
expect(result.dropped[0].dropped).toBe(true);
|
||||
expect(result.total).toBe(10); // 6 + 4
|
||||
});
|
||||
|
||||
it("drops highest rolls (dh)", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.1) // 1
|
||||
.mockReturnValueOnce(0.9) // 6
|
||||
.mockReturnValueOnce(0.5); // 4
|
||||
|
||||
const parsed = parseDiceNotation("3d6dh1")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.kept).toHaveLength(2);
|
||||
expect(result.dropped).toHaveLength(1);
|
||||
expect(result.dropped[0].value).toBe(6);
|
||||
expect(result.total).toBe(5); // 1 + 4
|
||||
});
|
||||
|
||||
it("handles exploding dice", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.99) // first roll: 6 (max, triggers explode)
|
||||
.mockReturnValueOnce(0.99) // explosion 1: 6 (triggers again)
|
||||
.mockReturnValueOnce(0.1); // explosion 2: 1 (stops)
|
||||
|
||||
const parsed = parseDiceNotation("1d6!")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].exploded).toBe(true);
|
||||
expect(result.dice[0].explosions).toEqual([6, 1]);
|
||||
expect(result.total).toBe(13); // 6 + 6 + 1
|
||||
});
|
||||
|
||||
it("handles penetrating explode", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.99) // first roll: 6 (explode)
|
||||
.mockReturnValueOnce(0.99); // explosion: 6 -> penetrating -> 5, then 5 < 6 stops
|
||||
|
||||
const parsed = parseDiceNotation("1d6!p")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].explosions).toEqual([5]);
|
||||
expect(result.total).toBe(11); // 6 + 5
|
||||
});
|
||||
|
||||
it("handles reroll", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.0) // first roll: 1 (reroll)
|
||||
.mockReturnValueOnce(0.5); // reroll: 4
|
||||
|
||||
const parsed = parseDiceNotation("1d6r1")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].rerolledFrom).toBe(1);
|
||||
expect(result.dice[0].value).toBe(4);
|
||||
});
|
||||
|
||||
it("handles reroll once (ro)", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.0) // first: 1 (reroll once)
|
||||
.mockReturnValueOnce(0.0); // second: 1 (stays, no more rerolls)
|
||||
|
||||
const parsed = parseDiceNotation("1d6ro1")!;
|
||||
const result = rollDice(parsed);
|
||||
|
||||
expect(result.dice[0].rerolledFrom).toBe(1);
|
||||
expect(result.dice[0].value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollAdvantage", () => {
|
||||
let mathRandomSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
afterEach(() => {
|
||||
mathRandomSpy?.mockRestore();
|
||||
});
|
||||
|
||||
it("returns highest for advantage", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.1) // 3
|
||||
.mockReturnValueOnce(0.9); // 19
|
||||
|
||||
const result = rollAdvantage(20, 0, true);
|
||||
expect(result.total).toBe(19);
|
||||
expect(result.advantageRolls).toEqual([3, 19]);
|
||||
});
|
||||
|
||||
it("returns lowest for disadvantage", () => {
|
||||
mathRandomSpy = vi
|
||||
.spyOn(Math, "random")
|
||||
.mockReturnValueOnce(0.9) // 19
|
||||
.mockReturnValueOnce(0.1); // 3
|
||||
|
||||
const result = rollAdvantage(20, 2, false);
|
||||
expect(result.total).toBe(5); // 3 + 2
|
||||
expect(result.advantageRolls).toEqual([19, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildNotation", () => {
|
||||
it("builds basic notation", () => {
|
||||
expect(buildNotation(1, 6, 0)).toBe("1d6");
|
||||
expect(buildNotation(2, 20, 0)).toBe("2d20");
|
||||
});
|
||||
|
||||
it("adds positive modifier", () => {
|
||||
expect(buildNotation(2, 6, 3)).toBe("2d6+3");
|
||||
});
|
||||
|
||||
it("adds negative modifier", () => {
|
||||
expect(buildNotation(2, 6, -2)).toBe("2d6-2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAdvancedNotation", () => {
|
||||
it("returns false for basic notation", () => {
|
||||
expect(isAdvancedNotation("2d6")).toBe(false);
|
||||
expect(isAdvancedNotation("1d20+5")).toBe(false);
|
||||
expect(isAdvancedNotation("3d8-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for keep/drop", () => {
|
||||
expect(isAdvancedNotation("4d6kh3")).toBe(true);
|
||||
expect(isAdvancedNotation("5d6dl2")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for explode", () => {
|
||||
expect(isAdvancedNotation("3d6!")).toBe(true);
|
||||
expect(isAdvancedNotation("2d10!p")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for reroll", () => {
|
||||
expect(isAdvancedNotation("2d6r1")).toBe(true);
|
||||
expect(isAdvancedNotation("2d6ro1")).toBe(true);
|
||||
expect(isAdvancedNotation("2d6r<3")).toBe(true);
|
||||
});
|
||||
});
|
||||
+90
-34
@@ -2,7 +2,7 @@
|
||||
// Supports: XdY±Z, kh/kl/dh/dl, ! exploding, r reroll, advantage/disadvantage
|
||||
// Caps: explode chain ≤100, reroll recursion ≤1000
|
||||
|
||||
type KeepDrop = { type: 'kh' | 'kl' | 'dh' | 'dl'; count: number } | null;
|
||||
type KeepDrop = { type: "kh" | "kl" | "dh" | "dl"; count: number } | null;
|
||||
|
||||
type Explode = {
|
||||
active: boolean;
|
||||
@@ -14,7 +14,7 @@ type Reroll = {
|
||||
active: boolean;
|
||||
values: Set<number>;
|
||||
once: boolean; // "ro" = once only; "r" = recursive
|
||||
operator: 'eq' | 'lt' | 'gt';
|
||||
operator: "eq" | "lt" | "gt";
|
||||
};
|
||||
|
||||
export interface Parsed {
|
||||
@@ -32,7 +32,7 @@ export interface DieResult {
|
||||
value: number;
|
||||
original: number;
|
||||
exploded: boolean;
|
||||
explosions: number[]; // chained explosion rolls
|
||||
explosions: number[]; // chained explosion rolls
|
||||
rerolledFrom: number | null;
|
||||
dropped: boolean;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ const REROLL_CAP = 1000;
|
||||
|
||||
/** Remove all whitespace and lowercase for parsing */
|
||||
function normalize(raw: string): string {
|
||||
return raw.replace(/\s+/g, '').toLowerCase();
|
||||
return raw.replace(/\s+/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +67,7 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
const main = s.match(/^(\d*)d(\d+)(.*)$/);
|
||||
if (!main) return null;
|
||||
|
||||
const count = main[1] === '' ? 1 : parseInt(main[1], 10);
|
||||
const count = main[1] === "" ? 1 : parseInt(main[1], 10);
|
||||
const sides = parseInt(main[2], 10);
|
||||
const rest = main[3];
|
||||
|
||||
@@ -81,7 +81,10 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
if (kdMatch) {
|
||||
const countKD = parseInt(kdMatch[2], 10);
|
||||
if (countKD < 1 || countKD >= count) return null;
|
||||
keepDrop = { type: kdMatch[1] as 'kh' | 'kl' | 'dh' | 'dl', count: countKD };
|
||||
keepDrop = {
|
||||
type: kdMatch[1] as "kh" | "kl" | "dh" | "dl",
|
||||
count: countKD,
|
||||
};
|
||||
remaining = remaining.slice(kdMatch[0].length);
|
||||
}
|
||||
|
||||
@@ -90,8 +93,8 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
const expMatch = remaining.match(/^(!p|!)(>?)(\d*)/);
|
||||
if (expMatch) {
|
||||
explode.active = true;
|
||||
explode.penetrating = expMatch[1] === '!p';
|
||||
if (expMatch[2] === '>' && expMatch[3]) {
|
||||
explode.penetrating = expMatch[1] === "!p";
|
||||
if (expMatch[2] === ">" && expMatch[3]) {
|
||||
explode.threshold = parseInt(expMatch[3], 10);
|
||||
if (explode.threshold < 2 || explode.threshold > sides) return null;
|
||||
} else {
|
||||
@@ -101,21 +104,26 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
}
|
||||
|
||||
// Reroll: rN, roN, r<N, ro<N
|
||||
let reroll: Reroll = { active: false, values: new Set(), once: false, operator: 'eq' };
|
||||
let reroll: Reroll = {
|
||||
active: false,
|
||||
values: new Set(),
|
||||
once: false,
|
||||
operator: "eq",
|
||||
};
|
||||
const rrMatch = remaining.match(/^(ro|r)([<>]?)(\d+)/);
|
||||
if (rrMatch) {
|
||||
reroll.active = true;
|
||||
reroll.once = rrMatch[1] === 'ro';
|
||||
const op = rrMatch[2] as '' | '<' | '>';
|
||||
reroll.once = rrMatch[1] === "ro";
|
||||
const op = rrMatch[2] as "" | "<" | ">";
|
||||
const val = parseInt(rrMatch[3], 10);
|
||||
if (op === '<') {
|
||||
reroll.operator = 'lt';
|
||||
if (op === "<") {
|
||||
reroll.operator = "lt";
|
||||
for (let i = 1; i < val && i < sides; i++) reroll.values.add(i);
|
||||
} else if (op === '>') {
|
||||
reroll.operator = 'gt';
|
||||
} else if (op === ">") {
|
||||
reroll.operator = "gt";
|
||||
for (let i = val + 1; i <= sides; i++) reroll.values.add(i);
|
||||
} else {
|
||||
reroll.operator = 'eq';
|
||||
reroll.operator = "eq";
|
||||
reroll.values.add(val);
|
||||
}
|
||||
remaining = remaining.slice(rrMatch[0].length);
|
||||
@@ -133,7 +141,16 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
||||
// If anything remains unparsed, it's invalid
|
||||
if (remaining.length > 0) return null;
|
||||
|
||||
return { count, sides, modifier, keepDrop, explode, reroll, advantage: false, disadvantage: false };
|
||||
return {
|
||||
count,
|
||||
sides,
|
||||
modifier,
|
||||
keepDrop,
|
||||
explode,
|
||||
reroll,
|
||||
advantage: false,
|
||||
disadvantage: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Roll a single die with optional reroll and explode logic */
|
||||
@@ -194,34 +211,47 @@ export function rollDice(parsed: Parsed): RollResult {
|
||||
|
||||
if (parsed.keepDrop) {
|
||||
const sorted = [...dice].map((d, i) => ({ die: d, idx: i }));
|
||||
if (parsed.keepDrop.type === 'kh') {
|
||||
if (parsed.keepDrop.type === "kh") {
|
||||
sorted.sort((a, b) => b.die.value - a.die.value);
|
||||
const keepIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const keepIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => keepIndices.has(i));
|
||||
dropped = dice.filter((_, i) => !keepIndices.has(i));
|
||||
} else if (parsed.keepDrop.type === 'kl') {
|
||||
} else if (parsed.keepDrop.type === "kl") {
|
||||
sorted.sort((a, b) => a.die.value - b.die.value);
|
||||
const keepIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const keepIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => keepIndices.has(i));
|
||||
dropped = dice.filter((_, i) => !keepIndices.has(i));
|
||||
} else if (parsed.keepDrop.type === 'dh') {
|
||||
} else if (parsed.keepDrop.type === "dh") {
|
||||
sorted.sort((a, b) => b.die.value - a.die.value);
|
||||
const dropIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const dropIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => !dropIndices.has(i));
|
||||
dropped = dice.filter((_, i) => dropIndices.has(i));
|
||||
} else if (parsed.keepDrop.type === 'dl') {
|
||||
} else if (parsed.keepDrop.type === "dl") {
|
||||
sorted.sort((a, b) => a.die.value - b.die.value);
|
||||
const dropIndices = new Set(sorted.slice(0, parsed.keepDrop.count).map(x => x.idx));
|
||||
const dropIndices = new Set(
|
||||
sorted.slice(0, parsed.keepDrop.count).map((x) => x.idx),
|
||||
);
|
||||
kept = dice.filter((_, i) => !dropIndices.has(i));
|
||||
dropped = dice.filter((_, i) => dropIndices.has(i));
|
||||
}
|
||||
}
|
||||
|
||||
dropped.forEach(d => { d.dropped = true; });
|
||||
dropped.forEach((d) => {
|
||||
d.dropped = true;
|
||||
});
|
||||
|
||||
// Calculate total
|
||||
let total = kept.reduce((sum, d) => sum + d.value, 0);
|
||||
total += kept.reduce((sum, d) => sum + d.explosions.reduce((s, v) => s + v, 0), 0);
|
||||
total += kept.reduce(
|
||||
(sum, d) => sum + d.explosions.reduce((s, v) => s + v, 0),
|
||||
0,
|
||||
);
|
||||
total += parsed.modifier;
|
||||
|
||||
return {
|
||||
@@ -230,30 +260,56 @@ export function rollDice(parsed: Parsed): RollResult {
|
||||
dropped,
|
||||
modifier: parsed.modifier,
|
||||
total,
|
||||
notation: '', // filled by caller
|
||||
notation: "", // filled by caller
|
||||
advantageRolls: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Roll with advantage or disadvantage (2dX keep highest/lowest) */
|
||||
export function rollAdvantage(sides: number, modifier: number, advantage: boolean): RollResult {
|
||||
export function rollAdvantage(
|
||||
sides: number,
|
||||
modifier: number,
|
||||
advantage: boolean,
|
||||
): RollResult {
|
||||
const r1 = Math.floor(Math.random() * sides) + 1;
|
||||
const r2 = Math.floor(Math.random() * sides) + 1;
|
||||
const keptVal = advantage ? Math.max(r1, r2) : Math.min(r1, r2);
|
||||
|
||||
return {
|
||||
dice: [{ value: keptVal, original: keptVal, exploded: false, explosions: [], rerolledFrom: null, dropped: false }],
|
||||
kept: [{ value: keptVal, original: keptVal, exploded: false, explosions: [], rerolledFrom: null, dropped: false }],
|
||||
dice: [
|
||||
{
|
||||
value: keptVal,
|
||||
original: keptVal,
|
||||
exploded: false,
|
||||
explosions: [],
|
||||
rerolledFrom: null,
|
||||
dropped: false,
|
||||
},
|
||||
],
|
||||
kept: [
|
||||
{
|
||||
value: keptVal,
|
||||
original: keptVal,
|
||||
exploded: false,
|
||||
explosions: [],
|
||||
rerolledFrom: null,
|
||||
dropped: false,
|
||||
},
|
||||
],
|
||||
dropped: [],
|
||||
modifier,
|
||||
total: keptVal + modifier,
|
||||
notation: '',
|
||||
notation: "",
|
||||
advantageRolls: [r1, r2],
|
||||
};
|
||||
}
|
||||
|
||||
/** Build notation string from parsed object (basic only, for simple sync) */
|
||||
export function buildNotation(count: number, sides: number, modifier: number): string {
|
||||
export function buildNotation(
|
||||
count: number,
|
||||
sides: number,
|
||||
modifier: number,
|
||||
): string {
|
||||
let s = `${count}d${sides}`;
|
||||
if (modifier > 0) s += `+${modifier}`;
|
||||
else if (modifier < 0) s += modifier;
|
||||
@@ -263,5 +319,5 @@ export function buildNotation(count: number, sides: number, modifier: number): s
|
||||
/** Check if notation contains advanced features beyond basic XdY±Z */
|
||||
export function isAdvancedNotation(raw: string): boolean {
|
||||
const s = normalize(raw);
|
||||
return /(kh|kl|dh|dl|!|[<>]?r\d)/.test(s);
|
||||
return /(kh|kl|dh|dl|!|ro?[<>]?\d)/.test(s);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user