fixed issue with expoentially more time for large dice rolls, and also fixed an integer overflow bug
This commit is contained in:
+201
-120
@@ -11,34 +11,77 @@ import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// barGraphCanvas is a custom widget that renders a bar graph
|
||||
// barGraphCanvas is a custom widget that renders a bar graph.
|
||||
type barGraphCanvas struct {
|
||||
widget.BaseWidget
|
||||
stats *DiceStatistics
|
||||
}
|
||||
|
||||
func newBarGraphCanvas(stats *DiceStatistics) *barGraphCanvas {
|
||||
graph := &barGraphCanvas{
|
||||
stats: stats,
|
||||
}
|
||||
graph := &barGraphCanvas{stats: stats}
|
||||
graph.ExtendBaseWidget(graph)
|
||||
return graph
|
||||
}
|
||||
|
||||
func (b *barGraphCanvas) CreateRenderer() fyne.WidgetRenderer {
|
||||
b.ExtendBaseWidget(b)
|
||||
return &barGraphCanvasRenderer{
|
||||
graph: b,
|
||||
|
||||
background := canvas.NewRectangle(color.NRGBA{R: 20, G: 20, B: 20, A: 255})
|
||||
yAxisLine := canvas.NewLine(color.White)
|
||||
yAxisLine.StrokeWidth = 2
|
||||
xAxisLine := canvas.NewLine(color.White)
|
||||
xAxisLine.StrokeWidth = 2
|
||||
title := canvas.NewText("Probability Distribution", color.White)
|
||||
statsLine1 := canvas.NewText("", color.White)
|
||||
statsLine2 := canvas.NewText("", color.White)
|
||||
yLabel := canvas.NewText("Probability (%)", color.White)
|
||||
xLabel := canvas.NewText("Result Value", color.White)
|
||||
|
||||
renderer := &barGraphCanvasRenderer{
|
||||
graph: b,
|
||||
background: background,
|
||||
yAxisLine: yAxisLine,
|
||||
xAxisLine: xAxisLine,
|
||||
title: title,
|
||||
statsLine1: statsLine1,
|
||||
statsLine2: statsLine2,
|
||||
yLabel: yLabel,
|
||||
xLabel: xLabel,
|
||||
objects: []fyne.CanvasObject{
|
||||
background,
|
||||
yAxisLine,
|
||||
xAxisLine,
|
||||
title,
|
||||
statsLine1,
|
||||
statsLine2,
|
||||
yLabel,
|
||||
xLabel,
|
||||
},
|
||||
}
|
||||
|
||||
renderer.Refresh()
|
||||
return renderer
|
||||
}
|
||||
|
||||
type barGraphCanvasRenderer struct {
|
||||
graph *barGraphCanvas
|
||||
objects []fyne.CanvasObject
|
||||
graph *barGraphCanvas
|
||||
background *canvas.Rectangle
|
||||
yAxisLine *canvas.Line
|
||||
xAxisLine *canvas.Line
|
||||
title *canvas.Text
|
||||
statsLine1 *canvas.Text
|
||||
statsLine2 *canvas.Text
|
||||
yLabel *canvas.Text
|
||||
xLabel *canvas.Text
|
||||
yTicks []*canvas.Line
|
||||
yTickLabels []*canvas.Text
|
||||
bars []*canvas.Rectangle
|
||||
barLabels []*canvas.Text
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) Layout(size fyne.Size) {
|
||||
r.Refresh()
|
||||
r.layout(size)
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) MinSize() fyne.Size {
|
||||
@@ -46,24 +89,75 @@ func (r *barGraphCanvasRenderer) MinSize() fyne.Size {
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) Refresh() {
|
||||
r.objects = []fyne.CanvasObject{}
|
||||
|
||||
if r.graph.stats == nil || len(r.graph.stats.Results) == 0 {
|
||||
stats := r.graph.stats
|
||||
if stats == nil || len(stats.Results) == 0 {
|
||||
r.hideAllButBackground()
|
||||
r.background.Show()
|
||||
r.background.Refresh()
|
||||
return
|
||||
}
|
||||
|
||||
stats := r.graph.stats
|
||||
outcomes := stats.GetSortedOutcomes()
|
||||
maxPercentage := stats.GetMaxPercentage()
|
||||
|
||||
axisMaxPercent, tickStep := calculateYAxisScale(maxPercentage)
|
||||
|
||||
r.background.Show()
|
||||
r.syncData(stats)
|
||||
size := r.graph.Size()
|
||||
if size.Width == 0 || size.Height == 0 {
|
||||
size = fyne.NewSize(900, 550)
|
||||
}
|
||||
r.layout(size)
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) syncData(stats *DiceStatistics) {
|
||||
r.title.Show()
|
||||
r.statsLine1.Show()
|
||||
r.statsLine2.Show()
|
||||
r.yLabel.Show()
|
||||
r.xLabel.Show()
|
||||
r.yAxisLine.Show()
|
||||
r.xAxisLine.Show()
|
||||
|
||||
r.statsLine1.Text = fmt.Sprintf("Range: %d to %d | Total Outcomes: %s", stats.MinValue, stats.MaxValue, stats.TotalOutcomesText)
|
||||
r.statsLine2.Text = fmt.Sprintf("Average: %.2f | Most Common: %d", stats.Average, stats.MostCommon)
|
||||
|
||||
axisMaxPercent, tickStep := calculateYAxisScale(stats.GetMaxPercentage())
|
||||
numYTicks := int(math.Round(axisMaxPercent / tickStep))
|
||||
r.ensureYTicks(numYTicks + 1)
|
||||
for i := 0; i <= numYTicks; i++ {
|
||||
percent := float64(i) * tickStep
|
||||
r.yTicks[i].Show()
|
||||
r.yTickLabels[i].Text = formatPercentLabel(percent)
|
||||
r.yTickLabels[i].Show()
|
||||
}
|
||||
for i := numYTicks + 1; i < len(r.yTicks); i++ {
|
||||
r.yTicks[i].Hide()
|
||||
r.yTickLabels[i].Hide()
|
||||
}
|
||||
|
||||
outcomes := stats.GetSortedOutcomes()
|
||||
r.ensureBars(len(outcomes))
|
||||
for i := range outcomes {
|
||||
r.bars[i].Show()
|
||||
r.barLabels[i].Hide()
|
||||
}
|
||||
for i := len(outcomes); i < len(r.bars); i++ {
|
||||
r.bars[i].Hide()
|
||||
r.barLabels[i].Hide()
|
||||
}
|
||||
|
||||
r.refreshObjects()
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) layout(size fyne.Size) {
|
||||
r.background.Move(fyne.NewPos(0, 0))
|
||||
r.background.Resize(size)
|
||||
|
||||
stats := r.graph.stats
|
||||
if stats == nil || len(stats.Results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
axisMaxPercent, tickStep := calculateYAxisScale(stats.GetMaxPercentage())
|
||||
outcomes := stats.GetSortedOutcomes()
|
||||
|
||||
// Padding and typography scale with the available area so the graph fits the window.
|
||||
topPadding := clamp(size.Height*0.14, 48, 75)
|
||||
bottomPadding := clamp(size.Height*0.15, 56, 80)
|
||||
leftPadding := clamp(size.Width*0.12, 60, 100)
|
||||
@@ -80,79 +174,43 @@ func (r *barGraphCanvasRenderer) Refresh() {
|
||||
labelTextSize := clamp(size.Height*0.022, 10, 12)
|
||||
tickTextSize := clamp(size.Height*0.018, 8, 10)
|
||||
|
||||
// Background
|
||||
background := canvas.NewRectangle(color.NRGBA{R: 20, G: 20, B: 20, A: 255})
|
||||
background.Move(fyne.NewPos(0, 0))
|
||||
background.Resize(size)
|
||||
r.objects = append(r.objects, background)
|
||||
r.yAxisLine.Move(fyne.NewPos(leftPadding, topPadding))
|
||||
r.yAxisLine.Resize(fyne.NewSize(0, graphHeight))
|
||||
r.xAxisLine.Move(fyne.NewPos(leftPadding, topPadding+graphHeight))
|
||||
r.xAxisLine.Resize(fyne.NewSize(graphWidth, 0))
|
||||
|
||||
// Y-axis
|
||||
yAxisLine := canvas.NewLine(color.White)
|
||||
yAxisLine.StrokeWidth = 2
|
||||
yAxisLine.Move(fyne.NewPos(leftPadding, topPadding))
|
||||
yAxisLine.Resize(fyne.NewSize(0, graphHeight))
|
||||
r.objects = append(r.objects, yAxisLine)
|
||||
r.title.TextSize = titleSize
|
||||
r.title.Move(fyne.NewPos(leftPadding, 5))
|
||||
|
||||
// X-axis
|
||||
xAxisLine := canvas.NewLine(color.White)
|
||||
xAxisLine.StrokeWidth = 2
|
||||
xAxisLine.Move(fyne.NewPos(leftPadding, topPadding+graphHeight))
|
||||
xAxisLine.Resize(fyne.NewSize(graphWidth, 0))
|
||||
r.objects = append(r.objects, xAxisLine)
|
||||
r.statsLine1.TextSize = bodyTextSize
|
||||
r.statsLine1.Move(fyne.NewPos(leftPadding, 5+r.title.MinSize().Height))
|
||||
|
||||
// Title
|
||||
title := canvas.NewText("Probability Distribution", color.White)
|
||||
title.TextSize = titleSize
|
||||
title.Move(fyne.NewPos(leftPadding, 5))
|
||||
r.objects = append(r.objects, title)
|
||||
r.statsLine2.TextSize = bodyTextSize
|
||||
r.statsLine2.Move(fyne.NewPos(leftPadding, 5+r.title.MinSize().Height+r.statsLine1.MinSize().Height))
|
||||
|
||||
// Statistics info line 1
|
||||
statsLine1 := canvas.NewText(fmt.Sprintf("Range: %d to %d | Total Outcomes: %d", stats.MinValue, stats.MaxValue, stats.Total), color.White)
|
||||
statsLine1.TextSize = bodyTextSize
|
||||
statsLine1.Move(fyne.NewPos(leftPadding, 5+title.MinSize().Height))
|
||||
r.objects = append(r.objects, statsLine1)
|
||||
r.yLabel.TextSize = labelTextSize
|
||||
r.yLabel.Move(fyne.NewPos(clamp(leftPadding*0.15, 8, 15), topPadding+graphHeight/2-r.yLabel.MinSize().Height/2))
|
||||
|
||||
// Statistics info line 2
|
||||
statsLine2 := canvas.NewText(fmt.Sprintf("Average: %.2f | Most Common: %d", stats.Average, stats.MostCommon), color.White)
|
||||
statsLine2.TextSize = bodyTextSize
|
||||
statsLine2.Move(fyne.NewPos(leftPadding, 5+title.MinSize().Height+statsLine1.MinSize().Height))
|
||||
r.objects = append(r.objects, statsLine2)
|
||||
r.xLabel.TextSize = labelTextSize
|
||||
r.xLabel.Move(fyne.NewPos(leftPadding+graphWidth/2-r.xLabel.MinSize().Width/2, topPadding+graphHeight+clamp(bottomPadding*0.35, 18, 28)))
|
||||
|
||||
// Y-axis label
|
||||
yLabel := canvas.NewText("Probability (%)", color.White)
|
||||
yLabel.TextSize = labelTextSize
|
||||
yLabel.Move(fyne.NewPos(clamp(leftPadding*0.15, 8, 15), topPadding+graphHeight/2-yLabel.MinSize().Height/2))
|
||||
r.objects = append(r.objects, yLabel)
|
||||
|
||||
// X-axis label
|
||||
xLabel := canvas.NewText("Result Value", color.White)
|
||||
xLabel.TextSize = labelTextSize
|
||||
xLabel.Move(fyne.NewPos(leftPadding+graphWidth/2-xLabel.MinSize().Width/2, topPadding+graphHeight+clamp(bottomPadding*0.35, 18, 28)))
|
||||
r.objects = append(r.objects, xLabel)
|
||||
|
||||
// Y-axis tick marks and labels
|
||||
numYTicks := int(math.Round(axisMaxPercent / tickStep))
|
||||
for i := 0; i <= numYTicks; i++ {
|
||||
for i := 0; i <= numYTicks && i < len(r.yTicks); i++ {
|
||||
percent := float64(i) * tickStep
|
||||
|
||||
yPos := topPadding + graphHeight - (float32(percent/axisMaxPercent) * graphHeight)
|
||||
|
||||
// Tick mark
|
||||
tick := canvas.NewLine(color.White)
|
||||
tick.StrokeWidth = 1
|
||||
tick.Move(fyne.NewPos(leftPadding-5, yPos))
|
||||
tick.Resize(fyne.NewSize(5, 0))
|
||||
r.objects = append(r.objects, tick)
|
||||
|
||||
// Label
|
||||
label := canvas.NewText(formatPercentLabel(percent), color.White)
|
||||
label.TextSize = tickTextSize
|
||||
label.Move(fyne.NewPos(leftPadding-50, yPos-7))
|
||||
r.objects = append(r.objects, label)
|
||||
r.yTicks[i].Move(fyne.NewPos(leftPadding-5, yPos))
|
||||
r.yTicks[i].Resize(fyne.NewSize(5, 0))
|
||||
r.yTickLabels[i].TextSize = tickTextSize
|
||||
r.yTickLabels[i].Move(fyne.NewPos(leftPadding-50, yPos-7))
|
||||
}
|
||||
|
||||
// Draw bars
|
||||
numBars := len(outcomes)
|
||||
if numBars == 0 {
|
||||
r.refreshObjects()
|
||||
return
|
||||
}
|
||||
|
||||
barSpacing := float32(2)
|
||||
totalSpacing := float32(numBars+1) * barSpacing
|
||||
barWidth := (graphWidth - totalSpacing) / float32(numBars)
|
||||
@@ -160,45 +218,77 @@ func (r *barGraphCanvasRenderer) Refresh() {
|
||||
barWidth = 1
|
||||
}
|
||||
|
||||
// Calculate label step to prevent overlapping
|
||||
labelStep := calculateLabelStep(graphWidth, numBars)
|
||||
|
||||
for i, value := range outcomes {
|
||||
percentage := stats.Percentages[value]
|
||||
|
||||
// Bar height proportional to percentage
|
||||
barHeight := (float32(percentage) / float32(axisMaxPercent)) * graphHeight
|
||||
|
||||
// X position
|
||||
xPos := leftPadding + barSpacing + float32(i)*(barWidth+barSpacing)
|
||||
|
||||
// Draw bar
|
||||
bar := canvas.NewRectangle(color.NRGBA{R: 100, G: 180, B: 255, A: 255})
|
||||
bar.Move(fyne.NewPos(xPos, topPadding+graphHeight-barHeight))
|
||||
bar.Resize(fyne.NewSize(barWidth, barHeight))
|
||||
r.objects = append(r.objects, bar)
|
||||
r.bars[i].Move(fyne.NewPos(xPos, topPadding+graphHeight-barHeight))
|
||||
r.bars[i].Resize(fyne.NewSize(barWidth, barHeight))
|
||||
|
||||
// X-axis label
|
||||
// Always show first and last label
|
||||
isFirst := i == 0
|
||||
isLast := i == numBars-1
|
||||
|
||||
// Determine if we should show this intermediate label
|
||||
// We show it if it matches the step, BUT we also need to make sure it doesn't clash with the last label
|
||||
// So if we are very close to the end, don't show it (unless it IS the end)
|
||||
showIntermediate := i%labelStep == 0 && i < numBars-labelStep
|
||||
|
||||
if isFirst || isLast || showIntermediate {
|
||||
label := canvas.NewText(fmt.Sprintf("%d", value), color.White)
|
||||
label.TextSize = tickTextSize
|
||||
|
||||
// Center label under bar
|
||||
label.Alignment = fyne.TextAlignCenter
|
||||
label.Move(fyne.NewPos(xPos+barWidth/2-label.MinSize().Width/2, topPadding+graphHeight+10))
|
||||
|
||||
r.objects = append(r.objects, label)
|
||||
r.barLabels[i].TextSize = tickTextSize
|
||||
r.barLabels[i].Alignment = fyne.TextAlignCenter
|
||||
if i == 0 || i == numBars-1 || (labelStep > 0 && i%labelStep == 0 && i < numBars-labelStep) {
|
||||
r.barLabels[i].Text = fmt.Sprintf("%d", value)
|
||||
r.barLabels[i].Show()
|
||||
r.barLabels[i].Move(fyne.NewPos(xPos+barWidth/2-r.barLabels[i].MinSize().Width/2, topPadding+graphHeight+10))
|
||||
} else {
|
||||
r.barLabels[i].Hide()
|
||||
}
|
||||
}
|
||||
|
||||
r.refreshObjects()
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) ensureYTicks(count int) {
|
||||
for len(r.yTicks) < count {
|
||||
tick := canvas.NewLine(color.White)
|
||||
tick.StrokeWidth = 1
|
||||
label := canvas.NewText("", color.White)
|
||||
r.yTicks = append(r.yTicks, tick)
|
||||
r.yTickLabels = append(r.yTickLabels, label)
|
||||
r.objects = append(r.objects, tick, label)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) ensureBars(count int) {
|
||||
for len(r.bars) < count {
|
||||
bar := canvas.NewRectangle(color.NRGBA{R: 100, G: 180, B: 255, A: 255})
|
||||
label := canvas.NewText("", color.White)
|
||||
r.bars = append(r.bars, bar)
|
||||
r.barLabels = append(r.barLabels, label)
|
||||
r.objects = append(r.objects, bar, label)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) availableGraphWidth() float32 {
|
||||
size := r.graph.Size()
|
||||
if size.Width == 0 {
|
||||
size.Width = 900
|
||||
}
|
||||
leftPadding := clamp(size.Width*0.12, 60, 100)
|
||||
rightPadding := clamp(size.Width*0.03, 16, 24)
|
||||
return size.Width - leftPadding - rightPadding
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) hideAllButBackground() {
|
||||
for _, object := range r.objects {
|
||||
object.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) refreshObjects() {
|
||||
for _, object := range r.objects {
|
||||
object.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func clamp(value, minValue, maxValue float32) float32 {
|
||||
@@ -212,7 +302,7 @@ func clamp(value, minValue, maxValue float32) float32 {
|
||||
}
|
||||
|
||||
func calculateLabelStep(graphWidth float32, numBars int) int {
|
||||
labelWidthEstimate := float32(35) // Estimate width of a label
|
||||
labelWidthEstimate := float32(35)
|
||||
maxLabels := int(graphWidth / labelWidthEstimate)
|
||||
if maxLabels < 1 {
|
||||
maxLabels = 1
|
||||
@@ -258,14 +348,7 @@ func formatPercentLabel(percent float64) string {
|
||||
return fmt.Sprintf("%.1f%%", percent)
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) Objects() []fyne.CanvasObject {
|
||||
return r.objects
|
||||
}
|
||||
|
||||
func (r *barGraphCanvasRenderer) Destroy() {
|
||||
}
|
||||
|
||||
// ShowStatisticsWindow creates and shows a statistics window for the given expression
|
||||
// ShowStatisticsWindow creates and shows a statistics window for the given expression.
|
||||
func ShowStatisticsWindow(expression string) {
|
||||
stats, err := CalculateDiceStatistics(expression)
|
||||
if err != nil {
|
||||
@@ -273,10 +356,8 @@ func ShowStatisticsWindow(expression string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create the bar graph
|
||||
graph := newBarGraphCanvas(stats)
|
||||
|
||||
// Create and show the window
|
||||
window := fyne.CurrentApp().NewWindow("Statistics: " + expression)
|
||||
window.SetContent(container.NewMax(graph))
|
||||
window.Resize(fyne.NewSize(900, 550))
|
||||
|
||||
+414
-269
@@ -3,34 +3,55 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DiceStatistics holds the theoretical statistics for a dice roll
|
||||
// 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
|
||||
MinValue int
|
||||
MaxValue int
|
||||
Results Distribution // outcome -> probability
|
||||
TotalOutcomes *big.Int // exact number of equally likely underlying outcomes
|
||||
TotalOutcomesText string // cached string form for UI
|
||||
Percentages map[int]float64
|
||||
SortedOutcomes []int
|
||||
MaxPercentage float64
|
||||
Average float64
|
||||
MostCommon int
|
||||
}
|
||||
|
||||
// Distribution represents the frequency distribution of outcomes
|
||||
type Distribution map[int]int
|
||||
// Distribution represents the probability distribution of outcomes.
|
||||
type Distribution map[int]float64
|
||||
|
||||
type distResult struct {
|
||||
dist Distribution
|
||||
totalOutcomes *big.Int
|
||||
}
|
||||
|
||||
type distributionEntry struct {
|
||||
value int
|
||||
weight float64
|
||||
}
|
||||
|
||||
const (
|
||||
combineParallelThreshold = 4096
|
||||
convolutionParallelThreshold = 4096
|
||||
probabilityEpsilon = 1e-12
|
||||
)
|
||||
|
||||
// Regex patterns for parsing
|
||||
var (
|
||||
diceTokenPattern = regexp.MustCompile(`^([HL])?(\d*)d(\d+)([HL])?`)
|
||||
// Updated numberTokenPattern to include optional decimal part
|
||||
diceTokenPattern = regexp.MustCompile(`^([HL])?(\d*)d(\d+)([HL])?`)
|
||||
numberTokenPattern = regexp.MustCompile(`^(\d+(\.\d+)?)`)
|
||||
)
|
||||
|
||||
// CalculateDiceStatistics calculates the theoretical distribution of possible outcomes for a dice expression
|
||||
// CalculateDiceStatistics calculates the theoretical distribution of possible outcomes for a dice expression.
|
||||
func CalculateDiceStatistics(expression string) (*DiceStatistics, error) {
|
||||
expression = strings.TrimSpace(expression)
|
||||
if expression == "" {
|
||||
@@ -38,7 +59,7 @@ func CalculateDiceStatistics(expression string) (*DiceStatistics, error) {
|
||||
}
|
||||
|
||||
parser := &statParser{expr: expression, pos: 0}
|
||||
outcomes, err := parser.parseExpression()
|
||||
result, err := parser.parseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -48,49 +69,18 @@ func CalculateDiceStatistics(expression string) (*DiceStatistics, error) {
|
||||
return nil, fmt.Errorf("unexpected character at position %d: '%c'", parser.pos, parser.expr[parser.pos])
|
||||
}
|
||||
|
||||
if len(outcomes) == 0 {
|
||||
if len(result.dist) == 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,
|
||||
Results: normalizeDistribution(result.dist),
|
||||
TotalOutcomes: cloneBigInt(result.totalOutcomes),
|
||||
TotalOutcomesText: cloneBigInt(result.totalOutcomes).String(),
|
||||
Percentages: make(map[int]float64, len(result.dist)),
|
||||
}
|
||||
|
||||
// Calculate average and most common value
|
||||
stats.calculateAverageAndMedian()
|
||||
|
||||
stats.populateDerivedFields()
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -106,11 +96,11 @@ func (p *statParser) skipWhitespace() {
|
||||
}
|
||||
}
|
||||
|
||||
// parseExpression handles addition and subtraction
|
||||
func (p *statParser) parseExpression() (Distribution, error) {
|
||||
// parseExpression handles addition and subtraction.
|
||||
func (p *statParser) parseExpression() (distResult, error) {
|
||||
left, err := p.parseTerm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -123,16 +113,16 @@ func (p *statParser) parseExpression() (Distribution, error) {
|
||||
p.pos++
|
||||
right, err := p.parseTerm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
left = addDist(left, right)
|
||||
left = combineIndependent(left, right, func(a, b int) int { return a + b })
|
||||
} else if p.expr[p.pos] == '-' {
|
||||
p.pos++
|
||||
right, err := p.parseTerm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
left = subDist(left, right)
|
||||
left = combineIndependent(left, right, func(a, b int) int { return a - b })
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -141,11 +131,11 @@ func (p *statParser) parseExpression() (Distribution, error) {
|
||||
return left, nil
|
||||
}
|
||||
|
||||
// parseTerm handles multiplication, division and implicit multiplication
|
||||
func (p *statParser) parseTerm() (Distribution, error) {
|
||||
// parseTerm handles multiplication, division and implicit multiplication.
|
||||
func (p *statParser) parseTerm() (distResult, error) {
|
||||
left, err := p.parsePower()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -159,23 +149,22 @@ func (p *statParser) parseTerm() (Distribution, error) {
|
||||
p.pos++
|
||||
right, err := p.parsePower()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
left = multDist(left, right)
|
||||
left = combineIndependent(left, right, func(a, b int) int { return a * b })
|
||||
} else if c == '/' {
|
||||
p.pos++
|
||||
right, err := p.parsePower()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
left = divDist(left, right)
|
||||
left = combineIndependentFiltered(left, right, divideValues)
|
||||
} 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
|
||||
return distResult{}, err
|
||||
}
|
||||
left = multDist(left, right)
|
||||
left = combineIndependent(left, right, func(a, b int) int { return a * b })
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -184,11 +173,11 @@ func (p *statParser) parseTerm() (Distribution, error) {
|
||||
return left, nil
|
||||
}
|
||||
|
||||
// parsePower handles exponentiation
|
||||
func (p *statParser) parsePower() (Distribution, error) {
|
||||
// parsePower handles exponentiation.
|
||||
func (p *statParser) parsePower() (distResult, error) {
|
||||
left, err := p.parseFactor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -201,9 +190,9 @@ func (p *statParser) parsePower() (Distribution, error) {
|
||||
p.pos++
|
||||
right, err := p.parseFactor() // Left-associative to match calculator
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
left = powDist(left, right)
|
||||
left = combineIndependent(left, right, powerValues)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -212,29 +201,27 @@ func (p *statParser) parsePower() (Distribution, error) {
|
||||
return left, nil
|
||||
}
|
||||
|
||||
// parseFactor handles parentheses, dice, and numbers
|
||||
func (p *statParser) parseFactor() (Distribution, error) {
|
||||
// parseFactor handles parentheses, dice, and numbers.
|
||||
func (p *statParser) parseFactor() (distResult, error) {
|
||||
p.skipWhitespace()
|
||||
if p.pos >= len(p.expr) {
|
||||
return nil, fmt.Errorf("unexpected end of expression")
|
||||
return distResult{}, fmt.Errorf("unexpected end of expression")
|
||||
}
|
||||
|
||||
// Parentheses
|
||||
if p.expr[p.pos] == '(' {
|
||||
p.pos++
|
||||
dist, err := p.parseExpression()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
p.skipWhitespace()
|
||||
if p.pos >= len(p.expr) || p.expr[p.pos] != ')' {
|
||||
return nil, fmt.Errorf("missing closing parenthesis")
|
||||
return distResult{}, 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]]
|
||||
@@ -242,246 +229,404 @@ func (p *statParser) parseFactor() (Distribution, error) {
|
||||
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 distResult{}, fmt.Errorf("invalid number: %s", token)
|
||||
}
|
||||
return Distribution{int(valFloat): 1}, nil
|
||||
return newDistResult(Distribution{int(valFloat): 1}), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unexpected character: %c", p.expr[p.pos])
|
||||
return distResult{}, fmt.Errorf("unexpected character: %c", p.expr[p.pos])
|
||||
}
|
||||
|
||||
func parseDiceToken(token string) (Distribution, error) {
|
||||
func parseDiceToken(token string) (distResult, error) {
|
||||
matches := diceTokenPattern.FindStringSubmatch(token)
|
||||
if matches == nil {
|
||||
return distResult{}, fmt.Errorf("invalid dice term: %s", token)
|
||||
}
|
||||
|
||||
if matches != nil {
|
||||
// It is a dice expression
|
||||
prefixModifier := matches[1]
|
||||
countStr := matches[2]
|
||||
sidesStr := matches[3]
|
||||
suffixModifier := matches[4]
|
||||
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)
|
||||
count := 1
|
||||
if countStr != "" {
|
||||
c, err := strconv.Atoi(countStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return distResult{}, err
|
||||
}
|
||||
|
||||
modifier := ""
|
||||
if suffixModifier != "" {
|
||||
modifier = suffixModifier
|
||||
} else if prefixModifier != "" {
|
||||
modifier = prefixModifier
|
||||
}
|
||||
|
||||
return getDiceOutcomes(count, sides, modifier), nil
|
||||
count = c
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
sides, err := strconv.Atoi(sidesStr)
|
||||
if err != nil {
|
||||
return distResult{}, err
|
||||
}
|
||||
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
|
||||
}
|
||||
if count <= 0 || sides <= 0 {
|
||||
return distResult{}, fmt.Errorf("dice terms must use positive counts and sides")
|
||||
}
|
||||
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
|
||||
}
|
||||
modifier := ""
|
||||
if suffixModifier != "" {
|
||||
modifier = suffixModifier
|
||||
} else if prefixModifier != "" {
|
||||
modifier = prefixModifier
|
||||
}
|
||||
return res
|
||||
|
||||
return getDiceOutcomes(count, sides, modifier), nil
|
||||
}
|
||||
|
||||
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
|
||||
func newDistResult(dist Distribution) distResult {
|
||||
return distResult{
|
||||
dist: normalizeDistribution(dist),
|
||||
totalOutcomes: big.NewInt(1),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneBigInt(v *big.Int) *big.Int {
|
||||
if v == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
return new(big.Int).Set(v)
|
||||
}
|
||||
|
||||
func multiplyOutcomeCounts(a, b *big.Int) *big.Int {
|
||||
return new(big.Int).Mul(cloneBigInt(a), cloneBigInt(b))
|
||||
}
|
||||
|
||||
func normalizeDistribution(dist Distribution) Distribution {
|
||||
total := 0.0
|
||||
for _, weight := range dist {
|
||||
total += weight
|
||||
}
|
||||
if total == 0 {
|
||||
return dist
|
||||
}
|
||||
|
||||
if math.Abs(total-1) <= probabilityEpsilon {
|
||||
pruned := make(Distribution, len(dist))
|
||||
for value, weight := range dist {
|
||||
if weight != 0 {
|
||||
pruned[value] = weight
|
||||
}
|
||||
res[valA/valB] += countA * countB
|
||||
}
|
||||
return pruned
|
||||
}
|
||||
|
||||
normalized := make(Distribution, len(dist))
|
||||
for value, weight := range dist {
|
||||
probability := weight / total
|
||||
if probability != 0 {
|
||||
normalized[value] = probability
|
||||
}
|
||||
}
|
||||
return res
|
||||
return normalized
|
||||
}
|
||||
|
||||
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)))
|
||||
func combineIndependent(a, b distResult, op func(int, int) int) distResult {
|
||||
return distResult{
|
||||
dist: combineDistributions(a.dist, b.dist, op),
|
||||
totalOutcomes: multiplyOutcomeCounts(a.totalOutcomes, b.totalOutcomes),
|
||||
}
|
||||
}
|
||||
|
||||
func combineIndependentFiltered(a, b distResult, op func(int, int) (int, bool)) distResult {
|
||||
return distResult{
|
||||
dist: combineDistributionsFiltered(a.dist, b.dist, op),
|
||||
totalOutcomes: multiplyOutcomeCounts(a.totalOutcomes, b.totalOutcomes),
|
||||
}
|
||||
}
|
||||
|
||||
func combineDistributions(a, b Distribution, op func(int, int) int) Distribution {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return Distribution{}
|
||||
}
|
||||
|
||||
entriesA := make([]distributionEntry, 0, len(a))
|
||||
for value, weight := range a {
|
||||
entriesA = append(entriesA, distributionEntry{value: value, weight: weight})
|
||||
}
|
||||
|
||||
entriesB := make([]distributionEntry, 0, len(b))
|
||||
for value, weight := range b {
|
||||
entriesB = append(entriesB, distributionEntry{value: value, weight: weight})
|
||||
}
|
||||
|
||||
workSize := len(entriesA) * len(entriesB)
|
||||
if workSize < combineParallelThreshold || len(entriesA) < 2 {
|
||||
return combineDistributionsSerial(entriesA, entriesB, op)
|
||||
}
|
||||
|
||||
workerCount := runtime.GOMAXPROCS(0)
|
||||
if workerCount > len(entriesA) {
|
||||
workerCount = len(entriesA)
|
||||
}
|
||||
if workerCount < 2 {
|
||||
return combineDistributionsSerial(entriesA, entriesB, op)
|
||||
}
|
||||
|
||||
chunkSize := (len(entriesA) + workerCount - 1) / workerCount
|
||||
partials := make([]Distribution, workerCount)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for worker := 0; worker < workerCount; worker++ {
|
||||
start := worker * chunkSize
|
||||
if start >= len(entriesA) {
|
||||
break
|
||||
}
|
||||
end := start + chunkSize
|
||||
if end > len(entriesA) {
|
||||
end = len(entriesA)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(workerIndex, from, to int) {
|
||||
defer wg.Done()
|
||||
local := make(Distribution)
|
||||
for _, left := range entriesA[from:to] {
|
||||
for _, right := range entriesB {
|
||||
local[op(left.value, right.value)] += left.weight * right.weight
|
||||
}
|
||||
}
|
||||
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)
|
||||
partials[workerIndex] = local
|
||||
}(worker, start, end)
|
||||
}
|
||||
|
||||
return outcomes
|
||||
wg.Wait()
|
||||
|
||||
result := make(Distribution)
|
||||
for _, partial := range partials {
|
||||
for value, weight := range partial {
|
||||
result[value] += weight
|
||||
}
|
||||
}
|
||||
return normalizeDistribution(result)
|
||||
}
|
||||
|
||||
// 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
|
||||
func combineDistributionsFiltered(a, b Distribution, op func(int, int) (int, bool)) Distribution {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return Distribution{}
|
||||
}
|
||||
|
||||
result := make(Distribution)
|
||||
for leftValue, leftWeight := range a {
|
||||
for rightValue, rightWeight := range b {
|
||||
value, ok := op(leftValue, rightValue)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
result[value] += leftWeight * rightWeight
|
||||
}
|
||||
outcomes[sum]++
|
||||
}
|
||||
return normalizeDistribution(result)
|
||||
}
|
||||
|
||||
func combineDistributionsSerial(entriesA, entriesB []distributionEntry, op func(int, int) int) Distribution {
|
||||
result := make(Distribution)
|
||||
for _, left := range entriesA {
|
||||
for _, right := range entriesB {
|
||||
result[op(left.value, right.value)] += left.weight * right.weight
|
||||
}
|
||||
}
|
||||
return normalizeDistribution(result)
|
||||
}
|
||||
|
||||
func divideValues(a, b int) (int, bool) {
|
||||
if b == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return a / b, true
|
||||
}
|
||||
|
||||
func powerValues(a, b int) int {
|
||||
return int(math.Pow(float64(a), float64(b)))
|
||||
}
|
||||
|
||||
// getDiceOutcomes returns a probability distribution for a dice roll and the exact total outcome count.
|
||||
func getDiceOutcomes(count int, sides int, modifier string) distResult {
|
||||
totalOutcomes := new(big.Int).Exp(big.NewInt(int64(sides)), big.NewInt(int64(count)), nil)
|
||||
|
||||
var outcomes Distribution
|
||||
switch modifier {
|
||||
case "H":
|
||||
outcomes = highestDieDistribution(count, sides)
|
||||
case "L":
|
||||
outcomes = lowestDieDistribution(count, sides)
|
||||
default:
|
||||
outcomes = sumDiceDistribution(count, sides)
|
||||
}
|
||||
|
||||
return distResult{
|
||||
dist: outcomes,
|
||||
totalOutcomes: totalOutcomes,
|
||||
}
|
||||
}
|
||||
|
||||
func sumDiceDistribution(count int, sides int) Distribution {
|
||||
current := []float64{1}
|
||||
singleDieProbability := 1 / float64(sides)
|
||||
|
||||
for die := 0; die < count; die++ {
|
||||
next := make([]float64, len(current)+sides)
|
||||
if len(current)*sides >= convolutionParallelThreshold {
|
||||
convolveStepParallel(current, next, sides, singleDieProbability)
|
||||
} else {
|
||||
convolveStepSerial(current, next, sides, singleDieProbability)
|
||||
}
|
||||
current = next
|
||||
}
|
||||
|
||||
outcomes := make(Distribution, count*(sides-1)+1)
|
||||
for sum, probability := range current {
|
||||
if probability != 0 {
|
||||
outcomes[sum] = probability
|
||||
}
|
||||
}
|
||||
return normalizeDistribution(outcomes)
|
||||
}
|
||||
|
||||
func convolveStepSerial(current, next []float64, sides int, singleDieProbability float64) {
|
||||
for sum, probability := range current {
|
||||
if probability == 0 {
|
||||
continue
|
||||
}
|
||||
for face := 1; face <= sides; face++ {
|
||||
next[sum+face] += probability * singleDieProbability
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convolveStepParallel(current, next []float64, sides int, singleDieProbability float64) {
|
||||
workerCount := runtime.GOMAXPROCS(0)
|
||||
if workerCount > len(current) {
|
||||
workerCount = len(current)
|
||||
}
|
||||
if workerCount < 2 {
|
||||
convolveStepSerial(current, next, sides, singleDieProbability)
|
||||
return
|
||||
}
|
||||
|
||||
for die := 1; die <= sides; die++ {
|
||||
generateSumOutcomes(remaining-1, sides, append(current, die), outcomes)
|
||||
}
|
||||
}
|
||||
chunkSize := (len(current) + workerCount - 1) / workerCount
|
||||
partials := make([][]float64, workerCount)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 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
|
||||
for worker := 0; worker < workerCount; worker++ {
|
||||
start := worker * chunkSize
|
||||
if start >= len(current) {
|
||||
break
|
||||
}
|
||||
end := start + chunkSize
|
||||
if end > len(current) {
|
||||
end = len(current)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(workerIndex, from, to int) {
|
||||
defer wg.Done()
|
||||
local := make([]float64, len(next))
|
||||
for sum := from; sum < to; sum++ {
|
||||
probability := current[sum]
|
||||
if probability == 0 {
|
||||
continue
|
||||
}
|
||||
for face := 1; face <= sides; face++ {
|
||||
local[sum+face] += probability * singleDieProbability
|
||||
}
|
||||
}
|
||||
partials[workerIndex] = local
|
||||
}(worker, start, end)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, partial := range partials {
|
||||
if partial == nil {
|
||||
continue
|
||||
}
|
||||
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
|
||||
for i, probability := range partial {
|
||||
next[i] += probability
|
||||
}
|
||||
}
|
||||
return maxPercentage
|
||||
}
|
||||
|
||||
// calculateAverageAndMedian calculates the average and most common value
|
||||
func (s *DiceStatistics) calculateAverageAndMedian() {
|
||||
func highestDieDistribution(count int, sides int) Distribution {
|
||||
outcomes := make(Distribution, sides)
|
||||
denominator := float64(sides)
|
||||
for value := 1; value <= sides; value++ {
|
||||
current := math.Pow(float64(value)/denominator, float64(count))
|
||||
previous := math.Pow(float64(value-1)/denominator, float64(count))
|
||||
probability := current - previous
|
||||
if probability != 0 {
|
||||
outcomes[value] = probability
|
||||
}
|
||||
}
|
||||
return normalizeDistribution(outcomes)
|
||||
}
|
||||
|
||||
func lowestDieDistribution(count int, sides int) Distribution {
|
||||
outcomes := make(Distribution, sides)
|
||||
denominator := float64(sides)
|
||||
for value := 1; value <= sides; value++ {
|
||||
current := math.Pow(float64(sides-value+1)/denominator, float64(count))
|
||||
next := math.Pow(float64(sides-value)/denominator, float64(count))
|
||||
probability := current - next
|
||||
if probability != 0 {
|
||||
outcomes[value] = probability
|
||||
}
|
||||
}
|
||||
return normalizeDistribution(outcomes)
|
||||
}
|
||||
|
||||
func (s *DiceStatistics) populateDerivedFields() {
|
||||
if len(s.Results) == 0 {
|
||||
s.Average = 0
|
||||
s.MostCommon = 0
|
||||
s.TotalOutcomesText = cloneBigInt(s.TotalOutcomes).String()
|
||||
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)
|
||||
s.SortedOutcomes = make([]int, 0, len(s.Results))
|
||||
s.Percentages = make(map[int]float64, len(s.Results))
|
||||
|
||||
// Find most common (mode) - the value with highest count
|
||||
maxCount := 0
|
||||
for value, count := range s.Results {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
first := true
|
||||
maxProbability := 0.0
|
||||
for value, probability := range s.Results {
|
||||
s.SortedOutcomes = append(s.SortedOutcomes, value)
|
||||
s.Percentages[value] = probability * 100
|
||||
|
||||
if first {
|
||||
s.MinValue = value
|
||||
s.MaxValue = value
|
||||
s.MostCommon = value
|
||||
first = false
|
||||
} else {
|
||||
if value < s.MinValue {
|
||||
s.MinValue = value
|
||||
}
|
||||
if value > s.MaxValue {
|
||||
s.MaxValue = value
|
||||
}
|
||||
}
|
||||
|
||||
s.Average += float64(value) * probability
|
||||
if probability > maxProbability || (math.Abs(probability-maxProbability) <= probabilityEpsilon && value < s.MostCommon) {
|
||||
maxProbability = probability
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Ints(s.SortedOutcomes)
|
||||
s.MaxPercentage = maxProbability * 100
|
||||
}
|
||||
|
||||
// GetSortedOutcomes returns cached sorted unique outcomes.
|
||||
func (s *DiceStatistics) GetSortedOutcomes() []int {
|
||||
return s.SortedOutcomes
|
||||
}
|
||||
|
||||
// GetMaxPercentage returns the cached maximum percentage value.
|
||||
func (s *DiceStatistics) GetMaxPercentage() float64 {
|
||||
return s.MaxPercentage
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user