From 6d35c9443a6d845d347b428f1090766b94810245 Mon Sep 17 00:00:00 2001 From: grimsace Date: Mon, 29 Jun 2026 08:38:36 -0500 Subject: [PATCH] added tests to repo --- .gitignore | 4 - calculations_test.go | 517 +++++++++++++++++++++++++++++++ statistics_test.go | 706 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1223 insertions(+), 4 deletions(-) create mode 100644 calculations_test.go create mode 100644 statistics_test.go diff --git a/.gitignore b/.gitignore index 2401178..ee7efb8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,10 +9,6 @@ *.dylib binaries/ -# Test binary, built with `go test -c` -*.test -*_test.go - # Code coverage profiles and other test artifacts *.out coverage.* diff --git a/calculations_test.go b/calculations_test.go new file mode 100644 index 0000000..0a77589 --- /dev/null +++ b/calculations_test.go @@ -0,0 +1,517 @@ +package main + +import ( + "strings" + "testing" +) + +// TestCalculateDice_BasicDiceRoll tests simple dice rolls +func TestCalculateDice_BasicDiceRoll(t *testing.T) { + result, diceRolls, err := CalculateDice("d20") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 1 || result > 20 { + t.Errorf("d20 should roll 1-20, got %v", result) + } + if !strings.Contains(diceRolls, "1d20") { + t.Errorf("diceRolls should contain '1d20', got %q", diceRolls) + } +} + +// TestCalculateDice_MultipleDice tests multiple dice +func TestCalculateDice_MultipleDice(t *testing.T) { + result, _, err := CalculateDice("2d6") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 2 || result > 12 { + t.Errorf("2d6 should roll 2-12, got %v", result) + } +} + +// TestCalculateDice_AdditionWithDice tests dice with addition +func TestCalculateDice_AdditionWithDice(t *testing.T) { + result, _, err := CalculateDice("1d6+5") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 6 || result > 11 { + t.Errorf("1d6+5 should roll 6-11, got %v", result) + } +} + +// TestCalculateDice_SubtractionWithDice tests dice with subtraction +func TestCalculateDice_SubtractionWithDice(t *testing.T) { + result, _, err := CalculateDice("2d10-5") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < -3 || result > 15 { + t.Errorf("2d10-5 should roll -3 to 15, got %v", result) + } +} + +// TestCalculateDice_MultiplicationWithDice tests dice with multiplication +func TestCalculateDice_MultiplicationWithDice(t *testing.T) { + result, _, err := CalculateDice("1d5*2") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 2 || result > 10 { + t.Errorf("1d5*2 should roll 2-10, got %v", result) + } +} + +// TestCalculateDice_DivisionWithDice tests dice with division +func TestCalculateDice_DivisionWithDice(t *testing.T) { + result, _, err := CalculateDice("1d6/2") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 0 || result > 3 { + t.Errorf("1d6/2 should roll 0-3, got %v", result) + } +} + +// TestCalculateDice_HighestDiceModifier tests the 'H' modifier +func TestCalculateDice_HighestDiceModifier(t *testing.T) { + // 2d20H should return only the highest die + result, _, err := CalculateDice("2d20H") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 1 || result > 20 { + t.Errorf("2d20H should roll 1-20 (highest of two d20s), got %v", result) + } +} + +// TestCalculateDice_LowestDiceModifier tests the 'L' modifier +func TestCalculateDice_LowestDiceModifier(t *testing.T) { + // 2d20L should return only the lowest die + result, _, err := CalculateDice("2d20L") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 1 || result > 20 { + t.Errorf("2d20L should roll 1-20 (lowest of two d20s), got %v", result) + } +} + +// TestCalculateDice_ComplexExpression tests complex mathematical expressions +func TestCalculateDice_ComplexExpression(t *testing.T) { + result, _, err := CalculateDice("2d6+3*2") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // 2d6 (2-12) + 3*2 (6) = 8-18 + if result < 8 || result > 18 { + t.Errorf("2d6+3*2 should roll 8-18, got %v", result) + } +} + +// TestCalculateDice_ParenthesesExpression tests expressions with parentheses +func TestCalculateDice_ParenthesesExpression(t *testing.T) { + result, _, err := CalculateDice("(1d4+2)*3") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // (1d4+2)*3 = (3-6)*3 = 9-18 + if result < 9 || result > 18 { + t.Errorf("(1d4+2)*3 should roll 9-18, got %v", result) + } +} + +// TestCalculateDice_MultipleAddends tests multiple addends +func TestCalculateDice_MultipleAddends(t *testing.T) { + result, _, err := CalculateDice("1d4+1d6+1d8") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // 1d4 (1-4) + 1d6 (1-6) + 1d8 (1-8) = 3-18 + if result < 3 || result > 18 { + t.Errorf("1d4+1d6+1d8 should roll 3-18, got %v", result) + } +} + +// TestCalculateDice_PrefixModifier tests prefix H/L modifiers +func TestCalculateDice_PrefixModifier(t *testing.T) { + result, _, err := CalculateDice("H2d20") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result < 1 || result > 20 { + t.Errorf("H2d20 should roll 1-20 (highest of two d20s), got %v", result) + } +} + +// TestCalculateDice_EmptyExpression tests error handling for empty expression +func TestCalculateDice_EmptyExpression(t *testing.T) { + _, _, err := CalculateDice("") + if err == nil { + t.Errorf("Empty expression should return error") + } +} + +// TestCalculateDice_InvalidDiceCount tests error handling for invalid dice count +func TestCalculateDice_InvalidDiceCount(t *testing.T) { + _, _, err := CalculateDice("0d6") + if err == nil { + t.Errorf("0d6 should return error") + } +} + +// TestCalculateDice_InvalidDiceSides tests error handling for invalid dice sides +func TestCalculateDice_InvalidDiceSides(t *testing.T) { + _, _, err := CalculateDice("1d0") + if err == nil { + t.Errorf("1d0 should return error") + } +} + +// TestCalculateDice_PlaceholderDice tests error handling for placeholder dice +func TestCalculateDice_PlaceholderDice(t *testing.T) { + _, _, err := CalculateDice("dx") + if err == nil { + t.Errorf("dx should return error") + } +} + +// TestEvaluateMathExpression_SimpleAddition tests simple addition +func TestEvaluateMathExpression_SimpleAddition(t *testing.T) { + result, err := evaluateMathExpression("5+3") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 8 { + t.Errorf("5+3 should be 8, got %v", result) + } +} + +// TestEvaluateMathExpression_Subtraction tests subtraction +func TestEvaluateMathExpression_Subtraction(t *testing.T) { + result, err := evaluateMathExpression("10-3") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 7 { + t.Errorf("10-3 should be 7, got %v", result) + } +} + +// TestEvaluateMathExpression_Multiplication tests multiplication +func TestEvaluateMathExpression_Multiplication(t *testing.T) { + result, err := evaluateMathExpression("4*5") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 20 { + t.Errorf("4*5 should be 20, got %v", result) + } +} + +// TestEvaluateMathExpression_Division tests division +func TestEvaluateMathExpression_Division(t *testing.T) { + result, err := evaluateMathExpression("20/4") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 5 { + t.Errorf("20/4 should be 5, got %v", result) + } +} + +// TestEvaluateMathExpression_OperatorPrecedence tests operator precedence (multiplication before addition) +func TestEvaluateMathExpression_OperatorPrecedence(t *testing.T) { + result, err := evaluateMathExpression("2+3*4") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 14 { + t.Errorf("2+3*4 should be 14, got %v", result) + } +} + +// TestEvaluateMathExpression_Parentheses tests parentheses override precedence +func TestEvaluateMathExpression_Parentheses(t *testing.T) { + result, err := evaluateMathExpression("(2+3)*4") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 20 { + t.Errorf("(2+3)*4 should be 20, got %v", result) + } +} + +// TestEvaluateMathExpression_Exponentiation tests exponentiation +func TestEvaluateMathExpression_Exponentiation(t *testing.T) { + result, err := evaluateMathExpression("2^3") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 8 { + t.Errorf("2^3 should be 8, got %v", result) + } +} + +// TestEvaluateMathExpression_UnaryMinus tests unary minus +func TestEvaluateMathExpression_UnaryMinus(t *testing.T) { + result, err := evaluateMathExpression("-5+10") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 5 { + t.Errorf("-5+10 should be 5, got %v", result) + } +} + +// TestEvaluateMathExpression_FloatingPoint tests floating point numbers +func TestEvaluateMathExpression_FloatingPoint(t *testing.T) { + result, err := evaluateMathExpression("3.5+2.5") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 6 { + t.Errorf("3.5+2.5 should be 6, got %v", result) + } +} + +// TestEvaluateMathExpression_DivisionByZero tests division by zero error +func TestEvaluateMathExpression_DivisionByZero(t *testing.T) { + _, err := evaluateMathExpression("5/0") + if err == nil { + t.Errorf("Division by zero should return error") + } +} + +// TestEvaluateMathExpression_MissingClosingParen tests missing closing parenthesis error +func TestEvaluateMathExpression_MissingClosingParen(t *testing.T) { + _, err := evaluateMathExpression("(2+3") + if err == nil { + t.Errorf("Missing closing parenthesis should return error") + } +} + +// TestEvaluateMathExpression_InvalidCharacter tests invalid character error +func TestEvaluateMathExpression_InvalidCharacter(t *testing.T) { + _, err := evaluateMathExpression("5@3") + if err == nil { + t.Errorf("Invalid character should return error") + } +} + +// TestEvaluateMathExpression_Whitespace tests whitespace handling +func TestEvaluateMathExpression_Whitespace(t *testing.T) { + result, err := evaluateMathExpression(" 5 + 3 ") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 8 { + t.Errorf("' 5 + 3 ' should be 8, got %v", result) + } +} + +// TestEvaluateMathExpression_ComplexExpression tests complex mathematical expressions +func TestEvaluateMathExpression_ComplexExpression(t *testing.T) { + result, err := evaluateMathExpression("((2+3)*4-5)/3") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + // ((2+3)*4-5)/3 = (5*4-5)/3 = (20-5)/3 = 15/3 = 5 + if result != 5 { + t.Errorf("((2+3)*4-5)/3 should be 5, got %v", result) + } +} + +// TestRollDiceSet_Range tests rollDiceSet produces values in correct range +func TestRollDiceSet_Range(t *testing.T) { + rolls := rollDiceSet(10, 20) + if len(rolls) != 10 { + t.Errorf("rollDiceSet(10, 20) should return 10 rolls, got %d", len(rolls)) + } + for i, roll := range rolls { + if roll < 1 || roll > 20 { + t.Errorf("Roll %d is %d, expected 1-20", i, roll) + } + } +} + +// TestRollDiceSet_Variability tests rollDiceSet produces different values +func TestRollDiceSet_Variability(t *testing.T) { + rolls := rollDiceSet(100, 6) + seenValues := make(map[int]bool) + for _, roll := range rolls { + seenValues[roll] = true + } + // With 100 rolls of 1d6, we should see multiple different values + if len(seenValues) < 3 { + t.Errorf("100 rolls of 1d6 should see at least 3 different values, got %d", len(seenValues)) + } +} + +// TestCalculateDice_OutputFormat tests the output format +func TestCalculateDice_OutputFormat(t *testing.T) { + _, diceRolls, err := CalculateDice("2d6") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // Should contain information about the rolls + if !strings.Contains(diceRolls, "2d6") { + t.Errorf("diceRolls should contain '2d6', got %q", diceRolls) + } + if !strings.Contains(diceRolls, "(") || !strings.Contains(diceRolls, ")") { + t.Errorf("diceRolls should be formatted with parentheses, got %q", diceRolls) + } +} + +// TestCalculateDice_PrefixAndSuffixModifiers tests that suffix modifier takes priority +func TestCalculateDice_PrefixAndSuffixModifiers(t *testing.T) { + // When both prefix and suffix modifiers are present, suffix should take priority + result, _, err := CalculateDice("H2d20L") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // Should use L (suffix) modifier, not H + if result < 1 || result > 20 { + t.Errorf("H2d20L should use L modifier and roll 1-20, got %v", result) + } +} + +// TestEvaluateMathExpression_LargeNumbers tests large number handling +func TestEvaluateMathExpression_LargeNumbers(t *testing.T) { + result, err := evaluateMathExpression("1000000+2000000") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 3000000 { + t.Errorf("1000000+2000000 should be 3000000, got %v", result) + } +} + +// TestEvaluateMathExpression_ExponentiationPrecedence tests exponentiation precedence +func TestEvaluateMathExpression_ExponentiationPrecedence(t *testing.T) { + result, err := evaluateMathExpression("2+3^2") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + // 2 + (3^2) = 2 + 9 = 11 + if result != 11 { + t.Errorf("2+3^2 should be 11, got %v", result) + } +} + +// TestEvaluateMathExpression_NestedParentheses tests nested parentheses +func TestEvaluateMathExpression_NestedParentheses(t *testing.T) { + result, err := evaluateMathExpression("((10))") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 10 { + t.Errorf("((10)) should be 10, got %v", result) + } +} + +// TestCalculateDice_NegativeResult tests expressions that can produce negative results +func TestCalculateDice_NegativeResult(t *testing.T) { + result, _, err := CalculateDice("1d4-10") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // 1d4 (1-4) - 10 = -9 to -6 + if result < -9 || result > -6 { + t.Errorf("1d4-10 should roll -9 to -6, got %v", result) + } +} + +// TestExpandDiceNotation_SingleDice tests expansion of single dice notation +func TestExpandDiceNotation_SingleDice(t *testing.T) { + // This test verifies that dice notation expands correctly + // We can't test exact values due to randomness, but we can test the format + expanded, diceRolls, err := expandDiceNotation("1d6") + if err != nil { + t.Fatalf("expandDiceNotation failed: %v", err) + } + // expanded should be a number between 1 and 6 + val, err := evaluateMathExpression(expanded) + if err != nil { + t.Fatalf("expanded result should be valid: %v", err) + } + if val < 1 || val > 6 { + t.Errorf("1d6 should expand to 1-6, got %v", val) + } + if !strings.Contains(diceRolls, "1d6") { + t.Errorf("diceRolls should contain '1d6', got %q", diceRolls) + } +} + +// TestExpandDiceNotation_WithModifier tests expansion with H/L modifier +func TestExpandDiceNotation_WithModifier(t *testing.T) { + expanded, _, err := expandDiceNotation("2d20H") + if err != nil { + t.Fatalf("expandDiceNotation failed: %v", err) + } + val, err := evaluateMathExpression(expanded) + if err != nil { + t.Fatalf("expanded result should be valid: %v", err) + } + if val < 1 || val > 20 { + t.Errorf("2d20H should expand to 1-20, got %v", val) + } +} + +// TestCalculateDice_PowerOperator tests the power/exponentiation operator +func TestCalculateDice_PowerOperator(t *testing.T) { + result, _, err := CalculateDice("2^3") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result != 8 { + t.Errorf("2^3 should be 8, got %v", result) + } +} + +// TestCalculateDice_ZeroDice tests zero values +func TestCalculateDice_ZeroDice(t *testing.T) { + result, _, err := CalculateDice("0") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + if result != 0 { + t.Errorf("0 should be 0, got %v", result) + } +} + +// TestEvaluateMathExpression_NestedExpression tests deeply nested expressions +func TestEvaluateMathExpression_NestedExpression(t *testing.T) { + result, err := evaluateMathExpression("(((2+3)))") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + if result != 5 { + t.Errorf("(((2+3))) should be 5, got %v", result) + } +} + +// TestCalculateDice_MultipleOperations tests multiple different operations +func TestCalculateDice_MultipleOperations(t *testing.T) { + result, _, err := CalculateDice("1d4+2-1") + if err != nil { + t.Fatalf("CalculateDice failed: %v", err) + } + // 1d4 (1-4) + 2 - 1 = 2-5 + if result < 2 || result > 5 { + t.Errorf("1d4+2-1 should roll 2-5, got %v", result) + } +} + +// TestEvaluateMathExpression_ChainedExponentiation tests chained exponentiation +func TestEvaluateMathExpression_ChainedExponentiation(t *testing.T) { + result, err := evaluateMathExpression("2^2^2") + if err != nil { + t.Fatalf("evaluateMathExpression failed: %v", err) + } + // Left-associative: (2^2)^2 = 4^2 = 16 + if result != 16 { + t.Errorf("2^2^2 (left-associative) should be 16, got %v", result) + } +} diff --git a/statistics_test.go b/statistics_test.go new file mode 100644 index 0000000..e9ddd06 --- /dev/null +++ b/statistics_test.go @@ -0,0 +1,706 @@ +package main + +import ( + "testing" +) + +func TestCalculateDiceStatistics_Multiplication(t *testing.T) { + expression := "5d10*3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Errorf("CalculateDiceStatistics(%q) failed: %v", expression, err) + return + } + + // For 5d10, min is 5, max is 50. + // With *3, min should be 15, max should be 150. + if stats.MinValue != 15 { + t.Errorf("Expected MinValue 15, got %d", stats.MinValue) + } + if stats.MaxValue != 150 { + t.Errorf("Expected MaxValue 150, got %d", stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_Basic(t *testing.T) { + // 2d6 -> 2-12 + expression := "2d6" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 2 || stats.MaxValue != 12 { + t.Errorf("Expected 2-12, got %d-%d", stats.MinValue, stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_Constant(t *testing.T) { + // 5 + 3 -> 8 + expression := "5+3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 8 || stats.MaxValue != 8 { + t.Errorf("Expected 8-8, got %d-%d", stats.MinValue, stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_Mixed_ConstantMult(t *testing.T) { + // 1d4 + 2 * 3 -> 1d4 + 6 -> 7-10 + expression := "1d4+2*3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 7 || stats.MaxValue != 10 { + t.Errorf("Expected 7-10, got %d-%d", stats.MinValue, stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_Mixed_DiceMult(t *testing.T) { + // 1d4*2 + 3 -> (1..4)*2 + 3 -> {2,4,6,8} + 3 -> {5,7,9,11} + expression := "1d4*2+3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 5 { + t.Errorf("Expected MinValue 5, got %d", stats.MinValue) + } + if stats.MaxValue != 11 { + t.Errorf("Expected MaxValue 11, got %d", stats.MaxValue) + } + // Check that 6 is NOT a possible outcome (since outcomes are 5, 7, 9, 11) + if _, exists := stats.Results[6]; exists { + t.Errorf("Did not expect outcome 6 to exist") + } +} + +func TestCalculateDiceStatistics_ParenthesisAndDiceMult(t *testing.T) { + // (5d10+3)*d10 + // 5d10 ranges 5-50. +3 ranges 8-53. + // d10 ranges 1-10. + // Min: 8 * 1 = 8. + // Max: 53 * 10 = 530. + expression := "(5d10+3)*d10" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed to calculate %s: %v", expression, err) + } + if stats.MinValue != 8 { + t.Errorf("Expected MinValue 8, got %d", stats.MinValue) + } + if stats.MaxValue != 530 { + t.Errorf("Expected MaxValue 530, got %d", stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_ComplexParentheses(t *testing.T) { + // 2 * (1d4 + 1) -> 2 * {2,3,4,5} -> {4,6,8,10} + expression := "2 * (1d4 + 1)" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 4 { + t.Errorf("Expected MinValue 4, got %d", stats.MinValue) + } + if stats.MaxValue != 10 { + t.Errorf("Expected MaxValue 10, got %d", stats.MaxValue) + } + if _, exists := stats.Results[5]; exists { + t.Errorf("Outcome 5 should not exist") + } +} + +func TestCalculateDiceStatistics_ImplicitMult_Dice(t *testing.T) { + // d203d10 -> 1d203 * 1d10 + // Min: 1 * 1 = 1 + // Max: 203 * 10 = 2030 + expression := "d203d10" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed to calculate %s: %v", expression, err) + } + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 2030 { + t.Errorf("Expected MaxValue 2030, got %d", stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_ImplicitMult_ConstantDice(t *testing.T) { + // 2d10 -> 2 * d10 -> 2,4,6...20? + // NO! 2d10 should be parsed as "2 dice of 10 sides". + // The parser MUST prioritize dice notation over implicit multiplication. + expression := "2d10" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 2d10 is sum of 2 dice. Min 2, Max 20. All values 2..20 possible. + if stats.MinValue != 2 { + t.Errorf("Expected MinValue 2, got %d", stats.MinValue) + } + if stats.MaxValue != 20 { + t.Errorf("Expected MaxValue 20, got %d", stats.MaxValue) + } + if _, exists := stats.Results[3]; !exists { + t.Errorf("Expected outcome 3 to exist for 2d10") + } +} + +func TestCalculateDiceStatistics_ImplicitMult_NumberDice(t *testing.T) { + // 3 d10 -> 3 * d10 + expression := "3 d10" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 3 { + t.Errorf("Expected MinValue 3, got %d", stats.MinValue) + } + if stats.MaxValue != 30 { + t.Errorf("Expected MaxValue 30, got %d", stats.MaxValue) + } + if _, exists := stats.Results[4]; exists { + t.Errorf("Did not expect outcome 4 to exist for 3 * d10") + } +} + +func TestCalculateDiceStatistics_Division(t *testing.T) { + // 1d6 / 2 + // Outcomes: 1/2=0, 2/2=1, 3/2=1, 4/2=2, 5/2=2, 6/2=3 + // Expected: 0, 1, 2, 3 + expression := "1d6/2" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 0 { + t.Errorf("Expected MinValue 0, got %d", stats.MinValue) + } + if stats.MaxValue != 3 { + t.Errorf("Expected MaxValue 3, got %d", stats.MaxValue) + } +} + +func TestCalculateDiceStatistics_Power(t *testing.T) { + // 1d4 ^ 2 + // Outcomes: 1, 4, 9, 16 + expression := "1d4^2" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 16 { + t.Errorf("Expected MaxValue 16, got %d", stats.MaxValue) + } + if _, exists := stats.Results[9]; !exists { + t.Errorf("Expected outcome 9 to exist") + } +} + +func TestCalculateDiceStatistics_Decimal(t *testing.T) { + // 2.5 + 2.5 = 2 + 2 = 4 (floor logic) + expression := "2.5 + 2.5" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 4 { + t.Errorf("Expected MinValue 4, got %d", stats.MinValue) + } +} + +// TestCalculateDiceStatistics_SingleD20 tests a single d20 roll +func TestCalculateDiceStatistics_SingleD20(t *testing.T) { + stats, err := CalculateDiceStatistics("d20") + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 20 { + t.Errorf("Expected MaxValue 20, got %d", stats.MaxValue) + } + // All outcomes should be equally likely (1/20 probability) + expectedProb := 1.0 / 20.0 + for value := 1; value <= 20; value++ { + prob, exists := stats.Results[value] + if !exists { + t.Errorf("Expected outcome %d to exist", value) + } + if prob < expectedProb-0.01 || prob > expectedProb+0.01 { + t.Errorf("d20 outcome %d has probability %f, expected %f", value, prob, expectedProb) + } + } +} + +// TestCalculateDiceStatistics_AverageValue tests average calculation +func TestCalculateDiceStatistics_AverageValue(t *testing.T) { + stats, err := CalculateDiceStatistics("1d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Average of 1d6 should be 3.5 + expectedAvg := 3.5 + if stats.Average < expectedAvg-0.1 || stats.Average > expectedAvg+0.1 { + t.Errorf("Expected average ~3.5, got %f", stats.Average) + } +} + +// TestCalculateDiceStatistics_MostCommonValue tests most common outcome +func TestCalculateDiceStatistics_MostCommonValue(t *testing.T) { + stats, err := CalculateDiceStatistics("2d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Most common roll for 2d6 should be 7 + if stats.MostCommon != 7 { + t.Errorf("Expected MostCommon 7, got %d", stats.MostCommon) + } +} + +// TestCalculateDiceStatistics_SortedOutcomes tests sorted outcomes +func TestCalculateDiceStatistics_SortedOutcomes(t *testing.T) { + stats, err := CalculateDiceStatistics("1d4") + if err != nil { + t.Fatalf("Failed: %v", err) + } + expected := []int{1, 2, 3, 4} + if len(stats.SortedOutcomes) != len(expected) { + t.Errorf("Expected %d outcomes, got %d", len(expected), len(stats.SortedOutcomes)) + } + for i, outcome := range stats.SortedOutcomes { + if outcome != expected[i] { + t.Errorf("Expected outcome[%d] = %d, got %d", i, expected[i], outcome) + } + } +} + +// TestCalculateDiceStatistics_Percentages tests percentage calculations +func TestCalculateDiceStatistics_Percentages(t *testing.T) { + stats, err := CalculateDiceStatistics("d20") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Each outcome should be 5% (1/20) + expectedPercentage := 5.0 + for value := 1; value <= 20; value++ { + pct, exists := stats.Percentages[value] + if !exists { + t.Errorf("Expected percentage for outcome %d", value) + } + if pct < expectedPercentage-0.1 || pct > expectedPercentage+0.1 { + t.Errorf("d20 outcome %d has percentage %f%%, expected %f%%", value, pct, expectedPercentage) + } + } +} + +// TestCalculateDiceStatistics_TotalOutcomes tests total outcomes calculation +func TestCalculateDiceStatistics_TotalOutcomes(t *testing.T) { + stats, err := CalculateDiceStatistics("2d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 2d6 should have 6*6 = 36 total outcomes + if stats.TotalOutcomes.Int64() != 36 { + t.Errorf("Expected 36 total outcomes for 2d6, got %d", stats.TotalOutcomes.Int64()) + } +} + +// TestCalculateDiceStatistics_Subtraction tests subtraction in statistics +func TestCalculateDiceStatistics_Subtraction(t *testing.T) { + expression := "1d6-1d4" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed to calculate %s: %v", expression, err) + } + // 1d6 (1-6) - 1d4 (1-4) gives range -3 to 5 + if stats.MinValue != -3 { + t.Errorf("Expected MinValue -3, got %d", stats.MinValue) + } + if stats.MaxValue != 5 { + t.Errorf("Expected MaxValue 5, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_MultiplyConstants tests multiplying constants +func TestCalculateDiceStatistics_MultiplyConstants(t *testing.T) { + expression := "3 * 4" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 12 || stats.MaxValue != 12 { + t.Errorf("Expected 12-12, got %d-%d", stats.MinValue, stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_DiceMultiplication tests dice multiplication +func TestCalculateDiceStatistics_DiceMultiplication(t *testing.T) { + expression := "1d4 * 1d3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Min: 1*1 = 1, Max: 4*3 = 12 + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 12 { + t.Errorf("Expected MaxValue 12, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_HighestModifier tests highest die modifier +func TestCalculateDiceStatistics_HighestModifier(t *testing.T) { + stats, err := CalculateDiceStatistics("4d6H") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 4d6H returns highest die (1-6) + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 6 { + t.Errorf("Expected MaxValue 6, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_LowestModifier tests lowest die modifier +func TestCalculateDiceStatistics_LowestModifier(t *testing.T) { + stats, err := CalculateDiceStatistics("4d6L") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 4d6L returns lowest die (1-6) + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 6 { + t.Errorf("Expected MaxValue 6, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_EmptyExpression tests error handling +func TestCalculateDiceStatistics_EmptyExpression(t *testing.T) { + _, err := CalculateDiceStatistics("") + if err == nil { + t.Errorf("Empty expression should return error") + } +} + +// TestCalculateDiceStatistics_InvalidDiceCount tests invalid dice count +func TestCalculateDiceStatistics_InvalidDiceCount(t *testing.T) { + _, err := CalculateDiceStatistics("0d6") + if err == nil { + t.Errorf("0d6 should return error") + } +} + +// TestCalculateDiceStatistics_InvalidDiceSides tests invalid dice sides +func TestCalculateDiceStatistics_InvalidDiceSides(t *testing.T) { + _, err := CalculateDiceStatistics("1d0") + if err == nil { + t.Errorf("1d0 should return error") + } +} + +// TestCalculateDiceStatistics_UnexpectedCharacter tests invalid character +func TestCalculateDiceStatistics_UnexpectedCharacter(t *testing.T) { + _, err := CalculateDiceStatistics("1d6@") + if err == nil { + t.Errorf("Invalid character should return error") + } +} + +// TestCalculateDiceStatistics_LargeNumberOfDice tests statistics with many dice +func TestCalculateDiceStatistics_LargeNumberOfDice(t *testing.T) { + stats, err := CalculateDiceStatistics("10d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 10d6 ranges from 10 to 60 + if stats.MinValue != 10 { + t.Errorf("Expected MinValue 10, got %d", stats.MinValue) + } + if stats.MaxValue != 60 { + t.Errorf("Expected MaxValue 60, got %d", stats.MaxValue) + } + // Average of 10d6 should be around 35 (10 * 3.5) + expectedAvg := 35.0 + if stats.Average < expectedAvg-1 || stats.Average > expectedAvg+1 { + t.Errorf("Expected average ~35, got %f", stats.Average) + } +} + +// TestCalculateDiceStatistics_DiceDivision tests dice division +func TestCalculateDiceStatistics_DiceDivision(t *testing.T) { + expression := "1d10 / 2" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 1d10 (1-10) / 2: floor division gives 0-5 + if stats.MinValue != 0 { + t.Errorf("Expected MinValue 0, got %d", stats.MinValue) + } + if stats.MaxValue != 5 { + t.Errorf("Expected MaxValue 5, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_NegativeResults tests expressions with negative results +func TestCalculateDiceStatistics_NegativeResults(t *testing.T) { + expression := "1d4 - 10" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 1d4 (1-4) - 10 = -9 to -6 + if stats.MinValue != -9 { + t.Errorf("Expected MinValue -9, got %d", stats.MinValue) + } + if stats.MaxValue != -6 { + t.Errorf("Expected MaxValue -6, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_Parentheses tests parentheses in expressions +func TestCalculateDiceStatistics_Parentheses(t *testing.T) { + expression := "(1d4 + 2) * 3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // (1d4 + 2) * 3 = (3-6) * 3 = 9-18 + if stats.MinValue != 9 { + t.Errorf("Expected MinValue 9, got %d", stats.MinValue) + } + if stats.MaxValue != 18 { + t.Errorf("Expected MaxValue 18, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_Exponentiation tests exponentiation in expressions +func TestCalculateDiceStatistics_Exponentiation(t *testing.T) { + expression := "1d4 ^ 2" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 1d4 ^ 2: outcomes 1, 4, 9, 16 + if stats.MinValue != 1 { + t.Errorf("Expected MinValue 1, got %d", stats.MinValue) + } + if stats.MaxValue != 16 { + t.Errorf("Expected MaxValue 16, got %d", stats.MaxValue) + } + // Check that 2 is NOT in results + if _, exists := stats.Results[2]; exists { + t.Errorf("Outcome 2 should not exist for 1d4^2") + } +} + +// TestCalculateDiceStatistics_MaxPercentage tests max percentage calculation +func TestCalculateDiceStatistics_MaxPercentage(t *testing.T) { + stats, err := CalculateDiceStatistics("d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // For d6, all outcomes equally likely: 1/6 ≈ 16.67% + expectedMaxPct := (1.0 / 6.0) * 100 + if stats.MaxPercentage < expectedMaxPct-1 || stats.MaxPercentage > expectedMaxPct+1 { + t.Errorf("Expected MaxPercentage ~16.67%%, got %f%%", stats.MaxPercentage) + } +} + +// TestCalculateDiceStatistics_DistributionSum tests that distribution probabilities sum to 1 +func TestCalculateDiceStatistics_DistributionSum(t *testing.T) { + stats, err := CalculateDiceStatistics("2d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + sum := 0.0 + for _, prob := range stats.Results { + sum += prob + } + // Probabilities should sum to ~1.0 + if sum < 0.999 || sum > 1.001 { + t.Errorf("Probabilities should sum to 1.0, got %f", sum) + } +} + +// TestCalculateDiceStatistics_GetSortedOutcomes tests GetSortedOutcomes method +func TestCalculateDiceStatistics_GetSortedOutcomes(t *testing.T) { + stats, err := CalculateDiceStatistics("1d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + outcomes := stats.GetSortedOutcomes() + if len(outcomes) != 6 { + t.Errorf("Expected 6 outcomes, got %d", len(outcomes)) + } + // Check they're sorted + for i := 1; i < len(outcomes); i++ { + if outcomes[i] <= outcomes[i-1] { + t.Errorf("Outcomes not properly sorted: %v", outcomes) + } + } +} + +// TestCalculateDiceStatistics_GetMaxPercentage tests GetMaxPercentage method +func TestCalculateDiceStatistics_GetMaxPercentage(t *testing.T) { + stats, err := CalculateDiceStatistics("d20") + if err != nil { + t.Fatalf("Failed: %v", err) + } + maxPct := stats.GetMaxPercentage() + expectedMaxPct := 5.0 // 1/20 = 0.05 = 5% + if maxPct < expectedMaxPct-0.1 || maxPct > expectedMaxPct+0.1 { + t.Errorf("Expected MaxPercentage 5%%, got %f%%", maxPct) + } +} + +// TestCalculateDiceStatistics_ComplexExpression tests complex expression +func TestCalculateDiceStatistics_ComplexExpression(t *testing.T) { + expression := "2d6 + 1d4 + 3" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // 2d6 (2-12) + 1d4 (1-4) + 3 = 6-19 + if stats.MinValue != 6 { + t.Errorf("Expected MinValue 6, got %d", stats.MinValue) + } + if stats.MaxValue != 19 { + t.Errorf("Expected MaxValue 19, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_ThreeDiceSum tests sum of three different dice +func TestCalculateDiceStatistics_ThreeDiceSum(t *testing.T) { + expression := "1d4 + 1d6 + 1d8" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Min: 1+1+1 = 3, Max: 4+6+8 = 18 + if stats.MinValue != 3 { + t.Errorf("Expected MinValue 3, got %d", stats.MinValue) + } + if stats.MaxValue != 18 { + t.Errorf("Expected MaxValue 18, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_ImplicitMultiplication tests implicit multiplication +func TestCalculateDiceStatistics_ImplicitMultiplication(t *testing.T) { + // 2d10 should be treated as 2 dice of 10 sides, not 2 * d10 + stats, err := CalculateDiceStatistics("2d10") + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 2 { + t.Errorf("Expected MinValue 2, got %d", stats.MinValue) + } + if stats.MaxValue != 20 { + t.Errorf("Expected MaxValue 20, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_OutcomeAtBoundary tests outcomes at boundaries +func TestCalculateDiceStatistics_OutcomeAtBoundary(t *testing.T) { + stats, err := CalculateDiceStatistics("1d20") + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Check minimum boundary + if _, exists := stats.Results[1]; !exists { + t.Errorf("Expected outcome 1 for d20") + } + // Check maximum boundary + if _, exists := stats.Results[20]; !exists { + t.Errorf("Expected outcome 20 for d20") + } + // Check just outside boundaries + if _, exists := stats.Results[0]; exists { + t.Errorf("Outcome 0 should not exist for d20") + } + if _, exists := stats.Results[21]; exists { + t.Errorf("Outcome 21 should not exist for d20") + } +} + +// TestCalculateDiceStatistics_SortedOutcomesAreUnique tests that sorted outcomes are unique +func TestCalculateDiceStatistics_SortedOutcomesAreUnique(t *testing.T) { + stats, err := CalculateDiceStatistics("2d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + seen := make(map[int]bool) + for _, outcome := range stats.SortedOutcomes { + if seen[outcome] { + t.Errorf("Outcome %d appears more than once in SortedOutcomes", outcome) + } + seen[outcome] = true + } +} + +// TestCalculateDiceStatistics_AverageInRange tests that average is within min/max +func TestCalculateDiceStatistics_AverageInRange(t *testing.T) { + stats, err := CalculateDiceStatistics("1d20") + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.Average < float64(stats.MinValue) || stats.Average > float64(stats.MaxValue) { + t.Errorf("Average %f should be between min %d and max %d", stats.Average, stats.MinValue, stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_MostCommonInResults tests that MostCommon is in Results +func TestCalculateDiceStatistics_MostCommonInResults(t *testing.T) { + stats, err := CalculateDiceStatistics("2d6") + if err != nil { + t.Fatalf("Failed: %v", err) + } + if _, exists := stats.Results[stats.MostCommon]; !exists { + t.Errorf("MostCommon value %d should exist in Results", stats.MostCommon) + } +} + +// TestCalculateDiceStatistics_WhitespaceHandling tests whitespace in expressions +func TestCalculateDiceStatistics_WhitespaceHandling(t *testing.T) { + expression := " 1d6 + 2 " + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + if stats.MinValue != 3 { + t.Errorf("Expected MinValue 3, got %d", stats.MinValue) + } + if stats.MaxValue != 8 { + t.Errorf("Expected MaxValue 8, got %d", stats.MaxValue) + } +} + +// TestCalculateDiceStatistics_ZeroOutcome tests expressions that can produce zero +func TestCalculateDiceStatistics_ZeroOutcome(t *testing.T) { + expression := "1d4 - 1d4" + stats, err := CalculateDiceStatistics(expression) + if err != nil { + t.Fatalf("Failed: %v", err) + } + // Should include zero outcome + if _, exists := stats.Results[0]; !exists { + t.Errorf("Expected outcome 0 for 1d4 - 1d4") + } +}