Skip to content

Commit

Permalink
Refactor random tier function for clarity. Add simulations. (#2615)
Browse files Browse the repository at this point in the history
* Refactor random tier function for clarity. Add simulations.

* Properly configured test code

* Removed Printf statements.
Added comments & modified variable names for clarity
  • Loading branch information
DrGrooveDev authored Apr 19, 2024
1 parent c588827 commit 2ef861d
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,45 @@ import (
"math/rand"
)

// PayoutChances to pick the random number between
// PayoutChances to pick the random number between 0 and this value
const PayoutChances = 1_000_000_000

const (
// PayoutTier1 that the random number must fall below to win
PayoutTier1 = 400_000_000

// PayoutTier2 that the payout must be below, but above PayoutTier1, to win
PayoutTier2 = 500_000_000
// The probabilities of a Tier being drawn (out of PayoutChances draws)
Tier1Prob = 400_000_000 // this is 400_000_000 / 1_000_000_000
Tier2Prob = 100_000_000 // this is 100_000_000 / 1_000_000_000
Tier3Prob = 50_000_000 //etc...
Tier4Prob = 10_000_000
Tier5Prob = 100
//The probability of NoBottle is: (PayoutChances - (The Sum of all TierXProb values above)) / PayoutChances

// Thresholds that the random number must fall below to win a tier
PayoutTier1 = Tier1Prob
PayoutTier2 = Tier2Prob + PayoutTier1
PayoutTier3 = Tier3Prob + PayoutTier2
PayoutTier4 = Tier4Prob + PayoutTier3
PayoutTier5 = Tier5Prob + PayoutTier4
//Anything above PayoutTier5 is NoBottle

// PayoutTier3
PayoutTier3 = 550_000_000

// PayoutTier4
PayoutTier4 = 560_000_000

// PayoutNoBottle is awarded when no bottle is sent to the user
PayoutNoBottle = 999_999_000

// PayoutTier5 (very low probability)
PayoutTier5 = 1_000_000_000
)

// pickRandomReward, 0 being no rewards.
// pickRandomReward. Returns Lootbox Tier 0-5, 0 being no rewards.
func pickRandomNumber() int {
n := rand.Int31n(PayoutChances)
switch {
case n >= PayoutNoBottle && n <= PayoutTier5:
return 5
case n >= PayoutTier4 && n <= PayoutNoBottle:
return 0
case n >= PayoutTier3 && n <= PayoutTier4:
return 4
case n >= PayoutTier2 && n <= PayoutTier3:
return 3
case n >= PayoutTier1 &&n <= PayoutTier2:
return 2
case n <= PayoutTier1:
return 1
case n <= PayoutTier2:
return 2
case n <= PayoutTier3:
return 3
case n <= PayoutTier4:
return 4
case n <= PayoutTier5:
return 5
case n > PayoutTier5 && n <= PayoutChances:
return 0 //NoBottle
default:
panic(fmt.Sprintf("bad pickRandomNumber impl: %v", n))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

// TestPickRandomTier. Simulate a large number of draws and compare to the expected probabilities.
func TestPickRandomTier(t *testing.T) {
simIterations := 10_000_000

expectedResults := getExpectedTierPcts()

//Prepare result data structure
simResults := make(map[int]int)
for i := 0; i < 6; i++ {
simResults[i] = 0
}

//Run Simulations
for i := 0; i < simIterations; i++ {
lootboxRewardTier := pickRandomNumber()
simResults[lootboxRewardTier] += 1
}

//Calculate the Simulated Result Outcome Percentages for each Tier
simResultPcts := make(map[int]float64)
for i := 0; i < 6; i++ {
simResultPcts[i] = (float64(simResults[i]) / float64(simIterations)) * 100
}

//This is "how close" the simulated outcome percentages must be to the expected probabilities to pass the test
testTolerancePct := float64(0.5) //Percentage, so 0.5 = 0.5% or 1 = 1%

//Actual testing & asserts.
//Compare the Simulated Result Outcome Pcts to an "acceptable" value range around the Expected Result
for i := 0; i < 6; i++ {

//Define an acceptable range of [Expected Result Pct] + or - TestTolerancePct
acceptableHigh := expectedResults[i] + testTolerancePct //expectedResults[i] is a pct. so we just +/- the tolerance
acceptableLow := expectedResults[i] - testTolerancePct
if acceptableLow < 0 {
acceptableLow = 0
}

thisSimResultPct := simResultPcts[i] // The simulated outcome pct

//Assert: Simulated Output Pct must be within the range of acceptable expected values.
assert.True(t, thisSimResultPct >= float64(acceptableLow) && thisSimResultPct <= float64(acceptableHigh), fmt.Sprintf("Tier %v value is not within expected range", i))
}
}

// getExpectedTierPcts. Returns the expected percentage chance of being drawn for all tiers
func getExpectedTierPcts() map[int]float64 {
expectedMap := make(map[int]float64)
expectedMap[0] = float64((PayoutChances - PayoutTier5)) / float64(PayoutChances) * 100
expectedMap[1] = float64(Tier1Prob) / float64(PayoutChances) * 100
expectedMap[2] = float64(Tier2Prob) / float64(PayoutChances) * 100
expectedMap[3] = float64(Tier3Prob) / float64(PayoutChances) * 100
expectedMap[4] = float64(Tier4Prob) / float64(PayoutChances) * 100
expectedMap[5] = float64(Tier5Prob) / float64(PayoutChances) * 100
return expectedMap
}

0 comments on commit 2ef861d

Please sign in to comment.