123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
//go:build simulation
|
|
|
|
package smashegg
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
)
|
|
|
|
func TestSimulateNewbiePoolRTP1000Users100000Draws(t *testing.T) {
|
|
const (
|
|
userCount = 1000
|
|
totalDraws = 100000
|
|
pricePerDraw = int64(500)
|
|
targetRTPBasis = 9500
|
|
)
|
|
|
|
option := model.SmashEggDrawOptionConfig{
|
|
ID: 1,
|
|
OptionKey: optionKeyOne,
|
|
Label: "Smash 1",
|
|
Times: 1,
|
|
PriceGold: pricePerDraw,
|
|
Enabled: true,
|
|
}
|
|
rewardTiers := []model.SmashEggRewardConfig{
|
|
{ID: 1, PoolType: poolTypeNewbie, RewardType: rewardTypeGold, RewardGroupName: "50 Gold", RewardValueGold: 50, GoldAmount: 50, Enabled: true},
|
|
{ID: 2, PoolType: poolTypeNewbie, RewardType: rewardTypeGold, RewardGroupName: "100 Gold", RewardValueGold: 100, GoldAmount: 100, Enabled: true},
|
|
{ID: 3, PoolType: poolTypeNewbie, RewardType: rewardTypeGold, RewardGroupName: "500 Gold", RewardValueGold: 500, GoldAmount: 500, Enabled: true},
|
|
{ID: 4, PoolType: poolTypeNewbie, RewardType: rewardTypeGold, RewardGroupName: "5000 Gold", RewardValueGold: 5000, GoldAmount: 5000, Enabled: true},
|
|
}
|
|
rewards, err := rewardsWithAutoProbabilitiesForOption(targetRTPBasis, option, rewardTiers)
|
|
if err != nil {
|
|
t.Fatalf("calculate auto probabilities failed: %v", err)
|
|
}
|
|
|
|
totalProbability := 0
|
|
for _, reward := range rewards {
|
|
totalProbability += reward.Probability
|
|
}
|
|
if totalProbability != probabilityTotal {
|
|
t.Fatalf("total probability = %d, want %d", totalProbability, probabilityTotal)
|
|
}
|
|
theoreticalValue := expectedRewardValue(rewards)
|
|
totalPaid := pricePerDraw * totalDraws
|
|
totalPayout := int64(0)
|
|
userPaid := make([]int64, userCount)
|
|
userPayout := make([]int64, userCount)
|
|
rewardHits := make(map[int64]int, len(rewards))
|
|
|
|
for drawIndex := 0; drawIndex < totalDraws; drawIndex++ {
|
|
userIndex := drawIndex % userCount
|
|
reward, err := pickReward(rewards)
|
|
if err != nil {
|
|
t.Fatalf("pickReward failed: %v", err)
|
|
}
|
|
totalPayout += reward.RewardValueGold
|
|
userPaid[userIndex] += pricePerDraw
|
|
userPayout[userIndex] += reward.RewardValueGold
|
|
rewardHits[reward.ID]++
|
|
}
|
|
|
|
actualRTP := float64(totalPayout) / float64(totalPaid) * 100
|
|
theoreticalRTP := theoreticalValue / float64(pricePerDraw) * 100
|
|
deviation := actualRTP - float64(targetRTPBasis)/100
|
|
minUserRTP, maxUserRTP := userRTPRange(userPaid, userPayout)
|
|
if absFloat64(theoreticalRTP-float64(targetRTPBasis)/100) > 1 {
|
|
t.Fatalf("theoretical RTP %.4f%% is outside target %.2f%% ±1.00%%", theoreticalRTP, float64(targetRTPBasis)/100)
|
|
}
|
|
if absFloat64(deviation) > 1 {
|
|
t.Fatalf("actual RTP %.4f%% is outside target %.2f%% ±1.00%%", actualRTP, float64(targetRTPBasis)/100)
|
|
}
|
|
|
|
t.Logf("users=%d totalDraws=%d pricePerDraw=%d random=crypto/rand no-seed", userCount, totalDraws, pricePerDraw)
|
|
t.Logf("targetRTP=%.2f%% theoreticalRTP=%.2f%% actualRTP=%.4f%% deviation=%+.4f%%", float64(targetRTPBasis)/100, theoreticalRTP, actualRTP, deviation)
|
|
t.Logf("totalPaid=%d totalPayout=%d net=%d", totalPaid, totalPayout, totalPayout-totalPaid)
|
|
t.Logf("perUserDraws=%d minUserRTP=%.2f%% maxUserRTP=%.2f%%", totalDraws/userCount, minUserRTP, maxUserRTP)
|
|
for _, reward := range rewards {
|
|
hits := rewardHits[reward.ID]
|
|
t.Logf(
|
|
"reward=%s value=%d probability=%s%% hits=%d hitRate=%.4f%%",
|
|
reward.RewardGroupName,
|
|
reward.RewardValueGold,
|
|
probabilityPercent(reward.Probability),
|
|
hits,
|
|
float64(hits)/float64(totalDraws)*100,
|
|
)
|
|
}
|
|
}
|
|
|
|
func userRTPRange(paid []int64, payout []int64) (float64, float64) {
|
|
minRTP := 0.0
|
|
maxRTP := 0.0
|
|
for index := range paid {
|
|
if paid[index] <= 0 {
|
|
continue
|
|
}
|
|
rtp := float64(payout[index]) / float64(paid[index]) * 100
|
|
if minRTP == 0 || rtp < minRTP {
|
|
minRTP = rtp
|
|
}
|
|
if rtp > maxRTP {
|
|
maxRTP = rtp
|
|
}
|
|
}
|
|
return minRTP, maxRTP
|
|
}
|
|
|
|
func absFloat64(value float64) float64 {
|
|
if value < 0 {
|
|
return -value
|
|
}
|
|
return value
|
|
}
|
|
|
|
func Example_simulationCommand() {
|
|
fmt.Println("go test -tags simulation ./internal/service/smashegg -run TestSimulateNewbiePoolRTP1000Users100000Draws -count=1 -v")
|
|
// Output:
|
|
// go test -tags simulation ./internal/service/smashegg -run TestSimulateNewbiePoolRTP1000Users100000Draws -count=1 -v
|
|
}
|