From 94adcb152cc073e7e7e54b832146b98fc4372390 Mon Sep 17 00:00:00 2001 From: Grimsace Date: Thu, 12 Feb 2026 09:22:11 -0600 Subject: [PATCH] fixed an issue with calculating lowest and highest dice roll numbers --- calculations.go | 28 ++++++++++++++++++-------- statistics.go | 52 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/calculations.go b/calculations.go index 9789389..79e51a5 100644 --- a/calculations.go +++ b/calculations.go @@ -44,9 +44,9 @@ func expandDiceNotation(expression string) (string, string, error) { result := expression diceRollsStr := expression - // Pattern to match dice notation: [count]d[sides][H|L] - // Examples: d20, 2d6, 3d6H, 4d8L, dx (where x is placeholder) - dicePattern := regexp.MustCompile(`(\d+)?d(\d+|x)([HL])?`) + // Pattern to match dice notation: [H|L]?[count]d[sides][H|L]? + // Examples: d20, 2d6, 3d6H, 4d8L, h2d20, l3d6, dx (where x is placeholder) + dicePattern := regexp.MustCompile(`([HL])?(\d+)?d(\d+|x)([HL])?`) // Process all dice matches matches := dicePattern.FindAllStringSubmatchIndex(result, -1) @@ -58,14 +58,26 @@ func expandDiceNotation(expression string) (string, string, error) { end := match[1] // Extract components - countStr := "" + prefixModifier := "" if match[2] != -1 { - countStr = result[match[2]:match[3]] + prefixModifier = result[match[2]:match[3]] } - sidesStr := result[match[4]:match[5]] + countStr := "" + if match[4] != -1 { + countStr = result[match[4]:match[5]] + } + sidesStr := result[match[6]:match[7]] + suffixModifier := "" + if match[8] != -1 { + suffixModifier = result[match[8]:match[9]] + } + + // Determine which modifier to use (priority: suffix > prefix) modifier := "" - if match[6] != -1 { - modifier = result[match[6]:match[7]] + if suffixModifier != "" { + modifier = suffixModifier + } else if prefixModifier != "" { + modifier = prefixModifier } // Determine count (default is 1) diff --git a/statistics.go b/statistics.go index 0a7c84a..83421ef 100644 --- a/statistics.go +++ b/statistics.go @@ -85,14 +85,20 @@ type Term struct { } // parseTerms parses a dice expression into terms +// parseTerms parses a dice expression into terms, handling H/L flexibly func parseTerms(expression string) ([]Term, error) { var terms []Term - // Split by + and -, keeping the operators + // Remove spaces + expression = strings.TrimSpace(expression) + + // Split by + and - while keeping the operators parts := regexp.MustCompile(`([+\-])`).Split(expression, -1) currentOp := "+" - dicePattern := regexp.MustCompile(`^(\d*)d(\d+)([HL])?$`) + pendingModifier := "" // Store H or L to apply to the next dice roll + // Pattern to match: optional H/L prefix, count, d, sides, optional H/L suffix + dicePattern := regexp.MustCompile(`^([HL])?(\d*)d(\d+)([HL])?$`) for _, part := range parts { part = strings.TrimSpace(part) @@ -107,19 +113,32 @@ func parseTerms(expression string) ([]Term, error) { continue } + // Check for standalone H or L modifier + if part == "H" || part == "L" { + pendingModifier = part + continue + } + // Try to match dice notation matches := dicePattern.FindStringSubmatch(part) + if matches != nil { + // Extract components + prefixModifier := matches[1] // H or L before the dice + countStr := matches[2] + sidesStr := matches[3] + suffixModifier := matches[4] // H or L after the dice + count := 1 - if matches[1] != "" { - c, err := strconv.Atoi(matches[1]) + if countStr != "" { + c, err := strconv.Atoi(countStr) if err != nil { return nil, err } count = c } - sides, err := strconv.Atoi(matches[2]) + sides, err := strconv.Atoi(sidesStr) if err != nil { return nil, err } @@ -128,7 +147,15 @@ func parseTerms(expression string) ([]Term, error) { return nil, fmt.Errorf("invalid dice: %dd%d", count, sides) } - modifier := matches[3] + // Determine which modifier to use (priority: suffix > prefix > pending) + modifier := "" + if suffixModifier != "" { + modifier = suffixModifier + } else if prefixModifier != "" { + modifier = prefixModifier + } else if pendingModifier != "" { + modifier = pendingModifier + } terms = append(terms, Term{ isDice: true, @@ -137,9 +164,15 @@ func parseTerms(expression string) ([]Term, error) { modifier: modifier, op: currentOp, }) + currentOp = "+" + pendingModifier = "" } else { - // Try to parse as constant + // Try to parse as constant (but reset pending modifier if it was set) + if pendingModifier != "" { + return nil, fmt.Errorf("modifier %s can only be applied to dice rolls", pendingModifier) + } + val, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("invalid term: %s", part) @@ -154,6 +187,11 @@ func parseTerms(expression string) ([]Term, error) { } } + // If we ended with a pending modifier, that's an error + if pendingModifier != "" { + return nil, fmt.Errorf("modifier %s at end of expression with no dice roll to apply to", pendingModifier) + } + return terms, nil }