From 462a7218fff6365976d8076f6b1add594f5e07a4 Mon Sep 17 00:00:00 2001 From: Grimsace Date: Mon, 9 Feb 2026 12:54:44 -0600 Subject: [PATCH] setup a basic dice calculator --- .gitignore | 1 + README.md | 168 +++++++++++++++++++++++++- calculations.go | 307 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 40 +++++++ go.sum | 80 +++++++++++++ main.go | 164 ++++++++++++++++++++++++++ 6 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 calculations.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore index aaadf73..ab2f9dd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +binaries/ # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index 15cd63b..efa7691 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,168 @@ # Desktop Dice Statistics Calculator -A desktop app made to calculate dice statistics. I've found that there are many great mobile apps and web apps to do this but I could not find a native desktop app on linux with the features I want. + +A native desktop application for calculating dice statistics for tabletop RPGs. Built with Go and the Fyne GUI library to support both Windows and Linux. + +## Features + +- **Dice Rolling**: Roll any combination of dice with notation like `2d20`, `3d6+5`, etc. +- **Highest/Lowest Selection**: Use `H` and `L` modifiers to take the highest or lowest result from multiple dice + - Example: `2d20H` rolls two d20s and takes the highest + - Example: `4d6L` rolls four d6s and takes the lowest +- **Calculator Functionality**: Perform arithmetic operations alongside dice rolls + - Supports: `+`, `-`, `*`, `/`, and parentheses + - Example: `2d6 + 5 * 3` +- **Standard Dice Support**: d4, d6, d8, d10, d12, d20, d100 +- **Custom Dice**: Use `dx` to define custom dice (e.g., `d24`, `d30`) +- **Calculator-Style Interface**: Familiar button layout resembling a traditional calculator + +## Building + +### Prerequisites + +- Go 1.25.7 or later +- [Fyne GUI library](https://fyne.io/) (automatically fetched by `go mod`) + +### Build Instructions + +```bash +cd desktop_dice_statistics_calculator +go build -o dice_calculator +``` + +This creates an executable named `dice_calculator` in the current directory. + +### Running + +```bash +./dice_calculator +``` + +On Windows, use: +```bash +dice_calculator.exe +``` + +## Application Structure + +### main.go +The main application file containing all UI logic: +- Window setup and layout management +- Dice control bar with quick-access buttons (d4, d6, d8, d10, d12, d20, d100, dx, H, L) +- Calculator-style number pad and operation buttons +- Input field for dice expressions +- Output display for results +- Event handlers for button clicks + +### calculations.go +The calculation engine containing all dice logic: +- **CalculateDice()**: Main entry point for evaluating dice expressions +- **expandDiceNotations()**: Parses and rolls dice notation (e.g., `2d20H`) +- **rollDiceSet()**: Rolls a specified number of dice with a given number of sides +- **evaluateMathExpression()**: Evaluates mathematical expressions with proper operator precedence +- **ExpressionParser**: Recursive descent parser handling +, -, *, /, and parentheses + +## Usage Examples + +### Basic Dice Rolls +- `d20` - Roll a single d20 +- `2d6` - Roll two d6s and sum them +- `3d4` - Roll three d4s and sum them + +### With Modifiers +- `2d20H` - Roll two d20s, take the highest (advantage in D&D 5e) +- `2d20L` - Roll two d20s, take the lowest (disadvantage in D&D 5e) +- `4d6L` - Roll four d6s, take the lowest (typical stat rolling method) + +### With Arithmetic +- `2d6 + 5` - Roll 2d6 and add 5 +- `3d6 + 2d4 + 3` - Multiple dice and modifiers +- `2d6 * 2` - Roll 2d6 and multiply by 2 +- `(2d6 + 1) * 3` - Using parentheses for complex calculations + +### Custom Dice +- `d24` - Roll a 24-sided die +- `3d30` - Roll three 30-sided dice +- `d100` - Roll a percentile die + +## Interface Layout + +``` +┌─────────────────────────────────────┐ +│ Result: [output] │ +├─────────────────────────────────────┤ +│ Dice Options │ +│ [d4] [d6] [d8] [d10] [d12] │ +│ [d20] [d100] [dx] [H] [L] │ +├─────────────────────────────────────┤ +│ Calculator │ +│ [7] [8] [9] [+] [-] │ +│ [4] [5] [6] [*] [/] │ +│ [1] [2] [3] [CLR] [BACKSPACE] │ +│ [0] [.] [ ] [ ] [ ] │ +├─────────────────────────────────────┤ +│ Dice Expression: │ +│ [input field showing: 2d20H] │ +│ [ROLL] │ +└─────────────────────────────────────┘ +``` + +## Supported Operations + +### Dice Notation +- `[count]d[sides]` - Standard dice notation (count defaults to 1) +- `H` - Take highest result (when count > 1) +- `L` - Take lowest result (when count > 1) + +### Arithmetic Operations +- `+` Addition +- `-` Subtraction +- `*` Multiplication +- `/` Division (integer division) +- `()` Parentheses for grouping + +### Operator Precedence +1. Parentheses +2. Unary minus (e.g., `-5`) +3. Multiplication and Division (left-to-right) +4. Addition and Subtraction (left-to-right) + +## Error Handling + +The calculator provides error messages for: +- Invalid dice notation (e.g., `d0`, `0d6`) +- Division by zero +- Malformed expressions +- Unexpected characters + +When an error occurs, it displays in the result field with an "Error: " prefix. + +## Dependencies + +- `fyne.io/fyne/v2` - Cross-platform GUI library +- Go standard library (math/rand, regexp, strconv, strings, time, etc.) + +## Cross-Platform Support + +This application is built with Fyne, which supports: +- **Linux** - All major distributions +- **Windows** - Windows 7 and later +- **macOS** - (Can be built, but not tested in this project scope) + +To build for a different platform, use Go's cross-compilation flags: +```bash +GOOS=windows GOARCH=amd64 go build +GOOS=linux GOARCH=amd64 go build +``` + +## Future Enhancement Ideas + +- History of recent rolls +- Statistics display (average, min, max for dice rolls) +- Save/load custom dice definitions +- Keyboard shortcuts for common operations +- Dark/light theme support +- Sound effects for dice rolls + +## License + +See LICENSE file for details. \ No newline at end of file diff --git a/calculations.go b/calculations.go new file mode 100644 index 0000000..5f8f6dc --- /dev/null +++ b/calculations.go @@ -0,0 +1,307 @@ +package main + +import ( + "fmt" + "math" + "math/rand" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +// init initializes the random seed +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// CalculateDice parses a dice expression and returns the result +// Supports formats like: 2d20, 3d6+5, 2d20H, 2d20L, 1d20+2d6, etc. +func CalculateDice(expression string) (int, error) { + expression = strings.TrimSpace(expression) + if expression == "" { + return 0, fmt.Errorf("empty expression") + } + + // Expand all dice notations to their rolled values + expanded, err := expandDiceNotation(expression) + if err != nil { + return 0, err + } + + // Evaluate the resulting mathematical expression + result, err := evaluateMathExpression(expanded) + if err != nil { + return 0, err + } + + return result, nil +} + +// expandDiceNotation finds all dice notation in the expression and replaces them with rolled values +func expandDiceNotation(expression string) (string, error) { + result := 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])?`) + + // Process all dice matches + matches := dicePattern.FindAllStringSubmatchIndex(result, -1) + + // Process matches in reverse to maintain string indices + for i := len(matches) - 1; i >= 0; i-- { + match := matches[i] + start := match[0] + end := match[1] + + // Extract components + countStr := "" + if match[2] != -1 { + countStr = result[match[2]:match[3]] + } + sidesStr := result[match[4]:match[5]] + modifier := "" + if match[6] != -1 { + modifier = result[match[6]:match[7]] + } + + // Determine count (default is 1) + count := 1 + if countStr != "" { + var err error + count, err = strconv.Atoi(countStr) + if err != nil || count <= 0 { + return "", fmt.Errorf("invalid dice count: %s", countStr) + } + } + + // Determine sides + var sides int + if sidesStr == "x" { + return "", fmt.Errorf("dx requires a number (e.g., d20). Please use a specific die like d20 or d100") + } + var err error + sides, err = strconv.Atoi(sidesStr) + if err != nil || sides <= 0 { + return "", fmt.Errorf("invalid dice sides: %s", sidesStr) + } + + // Roll the dice + rolls := rollDiceSet(count, sides) + + // Apply modifier (H for highest, L for lowest) + var value int + if modifier == "H" { + if count == 1 { + value = rolls[0] + } else { + sort.Ints(rolls) + value = rolls[len(rolls)-1] // Highest + } + } else if modifier == "L" { + if count == 1 { + value = rolls[0] + } else { + sort.Ints(rolls) + value = rolls[0] // Lowest + } + } else { + // Sum all rolls + value = 0 + for _, roll := range rolls { + value += roll + } + } + + // Replace the dice notation with its value in the result string + result = result[:start] + strconv.Itoa(value) + result[end:] + } + + return result, nil +} + +// rollDiceSet rolls count dice with the given number of sides +func rollDiceSet(count int, sides int) []int { + rolls := make([]int, count) + for i := 0; i < count; i++ { + rolls[i] = rand.Intn(sides) + 1 // Results in 1 to sides inclusive + } + return rolls +} + +// evaluateMathExpression evaluates a mathematical expression with +, -, *, /, and parentheses +// Uses a recursive descent parser to handle operator precedence +func evaluateMathExpression(expr string) (int, error) { + expr = strings.TrimSpace(expr) + if expr == "" { + return 0, fmt.Errorf("empty expression") + } + + parser := &parser{expr: expr, pos: 0} + result, err := parser.parseExpression() + if err != nil { + return 0, err + } + + parser.skipWhitespace() + if parser.pos < len(parser.expr) { + return 0, fmt.Errorf("unexpected character at position %d: '%c'", parser.pos, parser.expr[parser.pos]) + } + + return result, nil +} + +// parser is a simple recursive descent parser for mathematical expressions +type parser struct { + expr string + pos int +} + +// parseExpression handles addition and subtraction (lowest precedence) +func (p *parser) parseExpression() (int, error) { + left, err := p.parseTerm() + if err != nil { + return 0, 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 0, err + } + left = left + right + } else if p.expr[p.pos] == '-' { + p.pos++ + right, err := p.parseTerm() + if err != nil { + return 0, err + } + left = left - right + } else { + break + } + } + + return left, nil +} + +// parseTerm handles multiplication and division (higher precedence) +func (p *parser) parseTerm() (int, error) { + left, err := p.parseFactor() + if err != nil { + return 0, err + } + + for { + p.skipWhitespace() + if p.pos >= len(p.expr) { + break + } + + if p.expr[p.pos] == '*' { + p.pos++ + right, err := p.parseFactor() + if err != nil { + return 0, err + } + left = left * right + } else if p.expr[p.pos] == '/' { + p.pos++ + right, err := p.parseFactor() + if err != nil { + return 0, err + } + if right == 0 { + return 0, fmt.Errorf("division by zero") + } + left = int(math.Floor(float64(left) / float64(right))) + } else { + break + } + } + + return left, nil +} + +// parseFactor handles parentheses and unary operators (highest precedence) +func (p *parser) parseFactor() (int, error) { + p.skipWhitespace() + + if p.pos >= len(p.expr) { + return 0, fmt.Errorf("unexpected end of expression") + } + + // Handle parentheses + if p.expr[p.pos] == '(' { + p.pos++ + result, err := p.parseExpression() + if err != nil { + return 0, err + } + p.skipWhitespace() + if p.pos >= len(p.expr) || p.expr[p.pos] != ')' { + return 0, fmt.Errorf("missing closing parenthesis") + } + p.pos++ + return result, nil + } + + // Handle unary minus + if p.expr[p.pos] == '-' { + p.pos++ + value, err := p.parseFactor() + if err != nil { + return 0, err + } + return -value, nil + } + + // Parse a number + return p.parseNumber() +} + +// parseNumber parses an integer from the expression +func (p *parser) parseNumber() (int, error) { + p.skipWhitespace() + start := p.pos + + for p.pos < len(p.expr) && isDigit(p.expr[p.pos]) { + p.pos++ + } + + if start == p.pos { + if p.pos < len(p.expr) { + return 0, fmt.Errorf("expected number at position %d, got '%c'", p.pos, p.expr[p.pos]) + } + return 0, fmt.Errorf("expected number at end of expression") + } + + numStr := p.expr[start:p.pos] + num, err := strconv.Atoi(numStr) + if err != nil { + return 0, fmt.Errorf("invalid number: %s", numStr) + } + + return num, nil +} + +// skipWhitespace skips over whitespace characters +func (p *parser) skipWhitespace() { + for p.pos < len(p.expr) && (p.expr[p.pos] == ' ' || p.expr[p.pos] == '\t' || p.expr[p.pos] == '\n' || p.expr[p.pos] == '\r') { + p.pos++ + } +} + +// isDigit checks if a character is a digit +func isDigit(ch byte) bool { + return ch >= '0' && ch <= '9' +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..170e96d --- /dev/null +++ b/go.mod @@ -0,0 +1,40 @@ +module desktop_dice_statistics_calculator + +go 1.25.7 + +require fyne.io/fyne/v2 v2.7.2 + +require ( + fyne.io/systray v1.12.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fredbi/uri v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fyne-io/gl-js v0.2.0 // indirect + github.com/fyne-io/glfw-js v0.3.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.2.0 // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-text/render v0.2.0 // indirect + github.com/go-text/typesetting v0.2.1 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rymdport/portal v0.4.2 // indirect + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d6a6a77 --- /dev/null +++ b/go.sum @@ -0,0 +1,80 @@ +fyne.io/fyne/v2 v2.7.2 h1:XiNpWkn0PzX43ZCjbb0QYGg1RCxVbugwfVgikWZBCMw= +fyne.io/fyne/v2 v2.7.2/go.mod h1:PXbqY3mQmJV3J1NRUR2VbVgUUx3vgvhuFJxyjRK/4Ug= +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko= +github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs= +github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk= +github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= +github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= +github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= +github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..df0c19d --- /dev/null +++ b/main.go @@ -0,0 +1,164 @@ +package main + +import ( + "fmt" + "image/color" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +func main() { + myApp := app.New() + myWindow := myApp.NewWindow("Dice Statistics Calculator") + + // Output display + outputDisplay := canvas.NewText("0", color.White) + outputDisplay.TextSize = 32 + + // Dice input bar + diceInputEntry := widget.NewEntry() + diceInputEntry.SetPlaceHolder("e.g., 2d20H, 3d6+5") + + // Dice buttons for quick input + diceButtonsContainer := container.NewVBox( + container.NewHBox( + widget.NewButton("d4", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d4") + }), + widget.NewButton("d6", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d6") + }), + widget.NewButton("d8", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d8") + }), + widget.NewButton("d10", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d10") + }), + widget.NewButton("d12", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d12") + }), + ), + container.NewHBox( + widget.NewButton("d20", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d20") + }), + widget.NewButton("d100", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d100") + }), + widget.NewButton("dx", func() { + diceInputEntry.SetText(diceInputEntry.Text + "dx") + }), + widget.NewButton("H", func() { + diceInputEntry.SetText(diceInputEntry.Text + "H") + }), + widget.NewButton("L", func() { + diceInputEntry.SetText(diceInputEntry.Text + "L") + }), + ), + ) + + // Roll button + rollButton := widget.NewButton("ROLL", func() { + diceInput := strings.TrimSpace(diceInputEntry.Text) + if diceInput == "" { + outputDisplay.Text = "Error: Empty input" + outputDisplay.Refresh() + return + } + + result, err := CalculateDice(diceInput) + if err != nil { + outputDisplay.Text = fmt.Sprintf("Error: %v", err) + } else { + outputDisplay.Text = fmt.Sprintf("%d", result) + } + outputDisplay.Refresh() + }) + rollButton.Importance = widget.HighImportance + + // Clear button + clearButton := widget.NewButton("CLEAR", func() { + diceInputEntry.SetText("") + outputDisplay.Text = "0" + outputDisplay.Refresh() + }) + + // Calculator-style number buttons + numberButtonsContainer := container.NewVBox( + container.NewHBox( + createCalcButton("7", diceInputEntry), + createCalcButton("8", diceInputEntry), + createCalcButton("9", diceInputEntry), + widget.NewButton("+", func() { + diceInputEntry.SetText(diceInputEntry.Text + "+") + }), + widget.NewButton("-", func() { + diceInputEntry.SetText(diceInputEntry.Text + "-") + }), + ), + container.NewHBox( + createCalcButton("4", diceInputEntry), + createCalcButton("5", diceInputEntry), + createCalcButton("6", diceInputEntry), + widget.NewButton("*", func() { + diceInputEntry.SetText(diceInputEntry.Text + "*") + }), + widget.NewButton("/", func() { + diceInputEntry.SetText(diceInputEntry.Text + "/") + }), + ), + container.NewHBox( + createCalcButton("1", diceInputEntry), + createCalcButton("2", diceInputEntry), + createCalcButton("3", diceInputEntry), + clearButton, + widget.NewButton("Backspace", func() { + text := diceInputEntry.Text + if len(text) > 0 { + diceInputEntry.SetText(text[:len(text)-1]) + } + }), + ), + container.NewHBox( + createCalcButton("0", diceInputEntry), + widget.NewButton(".", func() { + diceInputEntry.SetText(diceInputEntry.Text + ".") + }), + widget.NewLabel(""), + widget.NewLabel(""), + widget.NewLabel(""), + ), + ) + + // Output section + outputSection := container.NewVBox( + widget.NewLabel("Result:"), + outputDisplay, + ) + + // Main layout: output at top, dice bar below, calculator buttons below that + mainContent := container.NewVBox( + outputSection, + widget.NewCard("Dice Options", "", diceButtonsContainer), + widget.NewCard("Calculator", "", numberButtonsContainer), + widget.NewLabel("Dice Expression:"), + diceInputEntry, + rollButton, + ) + + scrollContainer := container.NewScroll(mainContent) + myWindow.SetContent(scrollContainer) + myWindow.Resize(fyne.NewSize(400, 600)) + myWindow.ShowAndRun() +} + +func createCalcButton(label string, entry *widget.Entry) *widget.Button { + return widget.NewButton(label, func() { + entry.SetText(entry.Text + label) + }) +}