Skip to content

feat: implement AST evaluator with modifiers #6

@edloidas

Description

@edloidas

Implement AST evaluator that produces roll results, including keep/drop modifier logic.


Rationale

Evaluator is the final pipeline stage that produces actual dice roll results:

AST → [Evaluator + RNG] → RollResult

The evaluator must:

  • Traverse AST nodes recursively
  • Execute dice rolls using injected RNG
  • Apply modifiers (keep/drop) to dice pools
  • Track metadata (criticals, fumbles, dropped dice)

References


Things to Consider

  • CRITICAL: Negative results must NOT be clamped1d4-5 can produce -4
  • Modifier order: Keep/drop applied before arithmetic
  • Critical/Fumble: Only on dice that roll max/1, not on computed totals
  • Dropped dice: Must be tracked in result metadata

Implementation

Important

This is not a step-by-step guide — it's a functional checklist ordered logically.

Files to Create

  1. /src/types.ts — Shared result types:

    interface RollResult {
      total: number;
      notation: string;      // Original input
      expression: string;    // Normalized: "1d20 + 5"
      rendered: string;      // "1d20[15] + 5 = 20"
      rolls: DieResult[];
    }
    
    interface DieResult {
      sides: number;
      result: number;
      modifiers: ('dropped' | 'kept')[];
      critical: boolean;     // Rolled max
      fumble: boolean;       // Rolled 1
    }
  2. /src/evaluator/evaluator.ts — AST visitor/evaluator:

    • evaluate(ast: ASTNode, rng: RNG): RollResult
    • Handlers for each AST node type
    • Dice pool accumulation for modifiers
  3. /src/evaluator/modifiers/keep-drop.ts — Modifier logic:

    function applyKeepHighest(dice: DieResult[], count: number): DieResult[]
    function applyKeepLowest(dice: DieResult[], count: number): DieResult[]
    function applyDropHighest(dice: DieResult[], count: number): DieResult[]
    function applyDropLowest(dice: DieResult[], count: number): DieResult[]
  4. /src/evaluator/evaluator.test.ts — Unit tests with MockRNG

Test Cases (with MockRNG)

Notation Mock Values Expected Notes
1d6 [4] total: 4 Basic roll
1d6+3 [4] total: 7 Arithmetic
1d4-5 [1] total: -4 Negative NOT clamped!
-1d4 [3] total: -3 Unary minus
4d6kh3 [3,1,4,2] total: 9 Keep highest 3
4d6dl1 [3,1,4,2] total: 9 Drop lowest 1
1d20 [20] critical: true Max roll
1d20 [1] fumble: true Min roll

Modifier Logic

Keep Highest (kh)

4d6kh3 with [3,1,4,2]:
  Sort descending: [4,3,2,1]
  Keep first 3: [4,3,2] → total: 9
  Mark [1] as dropped

Drop Lowest (dl)

4d6dl1 with [3,1,4,2]:
  Sort ascending: [1,2,3,4]
  Drop first 1: [1] dropped
  Keep remaining: [2,3,4] → total: 9

Acceptance Criteria

  • Basic arithmetic operations (+, -, *, /, %, **) work correctly
  • Dice rolls use injected RNG
  • CRITICAL: Negative results are NOT clamped to 0
  • Keep highest modifier works (kh, k)
  • Keep lowest modifier works (kl)
  • Drop highest modifier works (dh)
  • Drop lowest modifier works (dl)
  • Critical detection: die rolled max value
  • Fumble detection: die rolled 1
  • Dropped dice tracked in result metadata
  • All test cases pass with MockRNG

Drafted with AI assistance

Metadata

Metadata

Assignees

Labels

featureNew functionality

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions