From e838f9a4b18a381e861a78be642737a1e8385c5f Mon Sep 17 00:00:00 2001 From: hyper Date: Sat, 12 Sep 2026 23:44:24 +0800 Subject: [PATCH] fix(journal): tokenize bare dice notation The tokenizer only recognized dice when the pattern started with a count (3d6), so a bare d20 fell through to the identifier branch and failed as an unknown identifier. Add a dedicated branch for the bare d-form; DICE_RE already accepted it. --- src/cli/completions/variable-system.test.ts | 14 ++++++++++++++ src/components/journal/variable-expression.ts | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/cli/completions/variable-system.test.ts b/src/cli/completions/variable-system.test.ts index 7989084..97bcfbf 100644 --- a/src/cli/completions/variable-system.test.ts +++ b/src/cli/completions/variable-system.test.ts @@ -379,6 +379,20 @@ describe("evaluateExpression", () => { expect(result.value.value).toBeLessThanOrEqual(23); }); + test("evaluates bare dice notation", () => { + const result = evaluateExpression("d20", { lookup: () => undefined }); + expect(result.value.kind).toBe("number"); + expect(result.value.value).toBeGreaterThanOrEqual(1); + expect(result.value.value).toBeLessThanOrEqual(20); + }); + + test("evaluates bare dice alongside modifiers", () => { + const result = evaluateExpression("d6 + 5", { lookup: () => undefined }); + expect(result.value.kind).toBe("number"); + expect(result.value.value).toBeGreaterThanOrEqual(6); + expect(result.value.value).toBeLessThanOrEqual(11); + }); + test("throws on division by zero", () => { expect(() => evaluateExpression("10 / 0", { lookup: () => undefined }), diff --git a/src/components/journal/variable-expression.ts b/src/components/journal/variable-expression.ts index 0cb21d4..9ad2362 100644 --- a/src/components/journal/variable-expression.ts +++ b/src/components/journal/variable-expression.ts @@ -6,7 +6,7 @@ * - Number literals (integer or decimal) * - Tagmap literals: #warrior:1;#druid:2 (bare #warrior → #warrior:1) * - $var references (resolved via lookup; auto-detects number vs tagmap) - * - Dice patterns: 3d6, 2d8kh1, etc. (delegates to rollFormula) + * - Dice patterns: 3d6, 2d8kh1, d20, etc. (delegates to rollFormula) * - Arithmetic: + - * / (type-checked via registry) * - Functions: floor(x), ceil(x), round(x) (numbers only) * - Parentheses for grouping @@ -225,6 +225,22 @@ function tokenize(input: string): Token[] { continue; } + // Dice pattern without a leading count: e.g. "d20", "d6kh1". The number + // branch above only catches dice that start with a digit ("3d6"), so bare + // forms need their own branch before the generic identifier branch. + if (/[dD]/.test(ch) && i + 1 < input.length && /[0-9]/.test(input[i + 1])) { + let dice = ""; + while (i < input.length && /[a-zA-Z0-9]/.test(input[i])) { + dice += input[i]; + i++; + } + if (!DICE_RE.test(dice)) { + throw new Error(`Invalid dice notation: "${dice}"`); + } + tokens.push({ kind: "number", value: String(rollDice(dice)), raw: dice }); + continue; + } + // Tagmap literal: #warrior:1;#druid:2 or bare #warrior if (ch === "#") { let raw = "";