Files
desktop_dice_statistics_cal…/statistics.go
T

488 lines
11 KiB
Go

package main
import (
"fmt"
"math"
"regexp"
"sort"
"strconv"
"strings"
)
// DiceStatistics holds the theoretical statistics for a dice roll
type DiceStatistics struct {
MinValue int
MaxValue int
Results map[int]int // outcome -> count of ways to achieve it
Total int // total number of possible outcomes
Percentages map[int]float64 // outcome -> percentage
Average float64 // average/mean value
MostCommon int // most common (median) value
}
// Distribution represents the frequency distribution of outcomes
type Distribution map[int]int
// Regex patterns for parsing
var (
diceTokenPattern = regexp.MustCompile(`^([HL])?(\d*)d(\d+)([HL])?`)
// Updated numberTokenPattern to include optional decimal part
numberTokenPattern = regexp.MustCompile(`^(\d+(\.\d+)?)`)
)
// CalculateDiceStatistics calculates the theoretical distribution of possible outcomes for a dice expression
func CalculateDiceStatistics(expression string) (*DiceStatistics, error) {
expression = strings.TrimSpace(expression)
if expression == "" {
return nil, fmt.Errorf("empty expression")
}
parser := &statParser{expr: expression, pos: 0}
outcomes, err := parser.parseExpression()
if err != nil {
return nil, err
}
parser.skipWhitespace()
if parser.pos < len(parser.expr) {
return nil, fmt.Errorf("unexpected character at position %d: '%c'", parser.pos, parser.expr[parser.pos])
}
if len(outcomes) == 0 {
return nil, fmt.Errorf("no valid outcomes for expression")
}
// Find min and max
minVal := 0
maxVal := 0
first := true
totalCount := 0
for value, count := range outcomes {
totalCount += count
if first {
minVal = value
maxVal = value
first = false
} else {
if value < minVal {
minVal = value
}
if value > maxVal {
maxVal = value
}
}
}
// Calculate percentages
percentages := make(map[int]float64)
for value, count := range outcomes {
percentages[value] = (float64(count) / float64(totalCount)) * 100
}
stats := &DiceStatistics{
MinValue: minVal,
MaxValue: maxVal,
Results: outcomes,
Total: totalCount,
Percentages: percentages,
}
// Calculate average and most common value
stats.calculateAverageAndMedian()
return stats, nil
}
// statParser implementation
type statParser struct {
expr string
pos int
}
func (p *statParser) skipWhitespace() {
for p.pos < len(p.expr) && (p.expr[p.pos] == ' ' || p.expr[p.pos] == '\t') {
p.pos++
}
}
// parseExpression handles addition and subtraction
func (p *statParser) parseExpression() (Distribution, error) {
left, err := p.parseTerm()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
if p.pos >= len(p.expr) {
break
}
if p.expr[p.pos] == '+' {
p.pos++
right, err := p.parseTerm()
if err != nil {
return nil, err
}
left = addDist(left, right)
} else if p.expr[p.pos] == '-' {
p.pos++
right, err := p.parseTerm()
if err != nil {
return nil, err
}
left = subDist(left, right)
} else {
break
}
}
return left, nil
}
// parseTerm handles multiplication, division and implicit multiplication
func (p *statParser) parseTerm() (Distribution, error) {
left, err := p.parsePower()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
if p.pos >= len(p.expr) {
break
}
c := p.expr[p.pos]
if c == '*' {
p.pos++
right, err := p.parsePower()
if err != nil {
return nil, err
}
left = multDist(left, right)
} else if c == '/' {
p.pos++
right, err := p.parsePower()
if err != nil {
return nil, err
}
left = divDist(left, right)
} else if c == '(' || (c >= '0' && c <= '9') || c == 'd' || c == 'H' || c == 'L' {
// Implicit multiplication for things that look like factors
right, err := p.parsePower()
if err != nil {
return nil, err
}
left = multDist(left, right)
} else {
break
}
}
return left, nil
}
// parsePower handles exponentiation
func (p *statParser) parsePower() (Distribution, error) {
left, err := p.parseFactor()
if err != nil {
return nil, err
}
for {
p.skipWhitespace()
if p.pos >= len(p.expr) {
break
}
if p.expr[p.pos] == '^' {
p.pos++
right, err := p.parseFactor() // Left-associative to match calculator
if err != nil {
return nil, err
}
left = powDist(left, right)
} else {
break
}
}
return left, nil
}
// parseFactor handles parentheses, dice, and numbers
func (p *statParser) parseFactor() (Distribution, error) {
p.skipWhitespace()
if p.pos >= len(p.expr) {
return nil, fmt.Errorf("unexpected end of expression")
}
// Parentheses
if p.expr[p.pos] == '(' {
p.pos++
dist, err := p.parseExpression()
if err != nil {
return nil, err
}
p.skipWhitespace()
if p.pos >= len(p.expr) || p.expr[p.pos] != ')' {
return nil, fmt.Errorf("missing closing parenthesis")
}
p.pos++
return dist, nil
}
// Try Dice Pattern
remaining := p.expr[p.pos:]
if loc := diceTokenPattern.FindStringIndex(remaining); loc != nil {
token := remaining[loc[0]:loc[1]]
p.pos += loc[1]
return parseDiceToken(token)
}
// Try Number Pattern
if loc := numberTokenPattern.FindStringIndex(remaining); loc != nil {
token := remaining[loc[0]:loc[1]]
p.pos += loc[1]
// Parse as float then cast to int (truncate/floor) to handle buttons like "."
valFloat, err := strconv.ParseFloat(token, 64)
if err != nil {
return nil, fmt.Errorf("invalid number: %s", token)
}
return Distribution{int(valFloat): 1}, nil
}
return nil, fmt.Errorf("unexpected character: %c", p.expr[p.pos])
}
func parseDiceToken(token string) (Distribution, error) {
matches := diceTokenPattern.FindStringSubmatch(token)
if matches != nil {
// It is a dice expression
prefixModifier := matches[1]
countStr := matches[2]
sidesStr := matches[3]
suffixModifier := matches[4]
count := 1
if countStr != "" {
c, err := strconv.Atoi(countStr)
if err != nil {
return nil, err
}
count = c
}
sides, err := strconv.Atoi(sidesStr)
if err != nil {
return nil, err
}
modifier := ""
if suffixModifier != "" {
modifier = suffixModifier
} else if prefixModifier != "" {
modifier = prefixModifier
}
return getDiceOutcomes(count, sides, modifier), nil
}
return nil, fmt.Errorf("invalid dice term: %s", token)
}
// Operations on Distributions
func addDist(a, b Distribution) Distribution {
res := make(Distribution)
for valA, countA := range a {
for valB, countB := range b {
res[valA+valB] += countA * countB
}
}
return res
}
func subDist(a, b Distribution) Distribution {
res := make(Distribution)
for valA, countA := range a {
for valB, countB := range b {
res[valA-valB] += countA * countB
}
}
return res
}
func multDist(a, b Distribution) Distribution {
res := make(Distribution)
for valA, countA := range a {
for valB, countB := range b {
res[valA*valB] += countA * countB
}
}
return res
}
func divDist(a, b Distribution) Distribution {
res := make(Distribution)
for valA, countA := range a {
for valB, countB := range b {
if valB == 0 {
continue // Division by zero yields no outcome
}
res[valA/valB] += countA * countB
}
}
return res
}
func powDist(a, b Distribution) Distribution {
res := make(Distribution)
for valA, countA := range a {
for valB, countB := range b {
// Integer exponentiation
// Standard behavior for non-negative exponents
// Negative exponents with int base result in 0 (unless -1, 1).
val := 0
if valB >= 0 {
val = int(math.Pow(float64(valA), float64(valB)))
} else {
// Integer division for 1/(a^-b) usually 0
val = int(math.Pow(float64(valA), float64(valB)))
}
res[val] += countA * countB
}
}
return res
}
// getDiceOutcomes returns a map of all possible outcomes for a dice roll and their frequencies
func getDiceOutcomes(count int, sides int, modifier string) map[int]int {
outcomes := make(map[int]int)
if modifier == "H" {
// Keep only the highest die
generateHighestOutcomes(count, sides, []int{}, outcomes)
} else if modifier == "L" {
// Keep only the lowest die
generateLowestOutcomes(count, sides, []int{}, outcomes)
} else {
// Sum all dice
generateSumOutcomes(count, sides, []int{}, outcomes)
}
return outcomes
}
// generateSumOutcomes recursively generates all sums
func generateSumOutcomes(remaining int, sides int, current []int, outcomes map[int]int) {
if remaining == 0 {
sum := 0
for _, val := range current {
sum += val
}
outcomes[sum]++
return
}
for die := 1; die <= sides; die++ {
generateSumOutcomes(remaining-1, sides, append(current, die), outcomes)
}
}
// generateHighestOutcomes recursively generates all highest-die outcomes
func generateHighestOutcomes(remaining int, sides int, current []int, outcomes map[int]int) {
if remaining == 0 {
highest := 0
for _, val := range current {
if val > highest {
highest = val
}
}
outcomes[highest]++
return
}
for die := 1; die <= sides; die++ {
generateHighestOutcomes(remaining-1, sides, append(current, die), outcomes)
}
}
// generateLowestOutcomes recursively generates all lowest-die outcomes
func generateLowestOutcomes(remaining int, sides int, current []int, outcomes map[int]int) {
if remaining == 0 {
lowest := sides + 1
for _, val := range current {
if val < lowest {
lowest = val
}
}
outcomes[lowest]++
return
}
for die := 1; die <= sides; die++ {
generateLowestOutcomes(remaining-1, sides, append(current, die), outcomes)
}
}
// GetSortedOutcomes returns sorted unique outcomes
func (s *DiceStatistics) GetSortedOutcomes() []int {
var outcomes []int
for value := range s.Results {
outcomes = append(outcomes, value)
}
sort.Ints(outcomes)
return outcomes
}
// GetMaxPercentage returns the maximum percentage value
func (s *DiceStatistics) GetMaxPercentage() float64 {
maxPercentage := 0.0
for _, percentage := range s.Percentages {
if percentage > maxPercentage {
maxPercentage = percentage
}
}
return maxPercentage
}
// calculateAverageAndMedian calculates the average and most common value
func (s *DiceStatistics) calculateAverageAndMedian() {
if len(s.Results) == 0 {
s.Average = 0
s.MostCommon = 0
return
}
// Calculate average (mean)
sum := 0
totalCount := 0
for value, count := range s.Results {
sum += value * count
totalCount += count
}
s.Average = float64(sum) / float64(totalCount)
// Find most common (mode) - the value with highest count
maxCount := 0
for value, count := range s.Results {
if count > maxCount {
maxCount = count
s.MostCommon = value
}
}
// If there are tied values, choose the smallest one
if maxCount > 0 {
for value, count := range s.Results {
if count == maxCount && value < s.MostCommon {
s.MostCommon = value
}
}
}
}