yumi-golang/internal/service/smashegg/simulation_test.go
2026-05-15 09:58:01 +08:00

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
}