Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor random tier function for clarity. Add simulations. #2615

Merged
merged 5 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading