yumi-golang/scripts/wheel_pure_random_simulate.go
2026-05-19 19:50:50 +08:00

389 lines
11 KiB
Go

//go:build ignore
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"math/rand"
"os"
"sort"
"strconv"
"strings"
"time"
"chatapp3-golang/internal/service/prizepool"
)
const (
probabilityTotal = 1000000
defaultTarget = 100000
defaultUsers = 5000
)
type categorySpec struct {
Category string
RewardCount int
GoldBase int64
}
type rewardConfig struct {
ID int64
Category string
Sort int
GoldAmount int64
Probability int
TotalLimit int64
UserLimit int64
IssuedCount int64
}
type rewardSummary struct {
Category string `json:"category"`
Sort int `json:"sort"`
RewardID int64 `json:"rewardId"`
GoldAmount int64 `json:"goldAmount"`
Probability int `json:"probability"`
TotalLimit int64 `json:"totalLimit,omitempty"`
UserLimit int64 `json:"userLimit,omitempty"`
IssuedCount int64 `json:"issuedCount"`
}
type summary struct {
Seed int64 `json:"seed"`
TargetRewardItems int `json:"targetRewardItems"`
ActualRewardItems int64 `json:"actualRewardItems"`
DrawRequests int64 `json:"drawRequests"`
RandomUsers int `json:"randomUsers"`
UniqueUsersDrawn int `json:"uniqueUsersDrawn"`
DrawTimesRequests map[string]int64 `json:"drawTimesRequests"`
DrawTimesRewardItems map[string]int64 `json:"drawTimesRewardItems"`
CategoryRewardItems map[string]int64 `json:"categoryRewardItems"`
LimitedRewards int `json:"limitedRewards"`
ExhaustedLimitedRewards int `json:"exhaustedLimitedRewards"`
TotalLimitViolations int `json:"totalLimitViolations"`
UserLimitViolations int `json:"userLimitViolations"`
NoAvailableHits int64 `json:"noAvailableHits"`
LimitedRewardDetails []rewardSummary `json:"limitedRewardDetails"`
TopRewards []rewardSummary `json:"topRewards"`
ElapsedMillis int64 `json:"elapsedMillis"`
}
type userRewardKey struct {
UserID int64
RewardID int64
}
func main() {
ctx := context.Background()
startedAt := time.Now()
seed := envInt64("WHEEL_SIM_SEED", time.Now().UnixNano())
target := envInt("WHEEL_SIM_TARGET_REWARDS", defaultTarget)
userCount := envInt("WHEEL_SIM_USERS", defaultUsers)
rng := rand.New(rand.NewSource(seed))
rewardsByCategory, allRewards := buildRandomConfig(rng)
userRewardCounts := map[userRewardKey]int64{}
drawTimesRequests := map[string]int64{"1": 0, "10": 0, "50": 0}
drawTimesRewardItems := map[string]int64{"1": 0, "10": 0, "50": 0}
categoryRewardItems := map[string]int64{}
uniqueUsers := map[int64]struct{}{}
var drawRequests int64
var rewardItems int64
var noAvailableHits int64
picker := prizepool.Picker{RandomIntn: func(max int) (int, error) {
return rng.Intn(max), nil
}}
for rewardItems < int64(target) {
userID := int64(1 + rng.Intn(userCount))
category := specs()[rng.Intn(len(specs()))].Category
categoryRewards := rewardsByCategory[category]
times := randomDrawTimes(rng)
drawRequests++
uniqueUsers[userID] = struct{}{}
timesKey := strconv.Itoa(times)
drawTimesRequests[timesKey]++
for i := 0; i < times; i++ {
picked, err := picker.PickAvailable(ctx, prizePoolItems(categoryRewards), userID, prizepool.UserIssueCounterFunc(func(_ context.Context, itemID int64, userID int64) (int64, error) {
return userRewardCounts[userRewardKey{UserID: userID, RewardID: itemID}], nil
}))
if errors.Is(err, prizepool.ErrNoAvailableItem) {
noAvailableHits++
continue
}
if err != nil {
log.Fatal(err)
}
reward := categoryRewards[picked.Index]
reward.IssuedCount++
if reward.UserLimit > 0 {
userRewardCounts[userRewardKey{UserID: userID, RewardID: reward.ID}]++
}
rewardItems++
drawTimesRewardItems[timesKey]++
categoryRewardItems[category]++
}
}
limitedRewards, exhaustedLimitedRewards := countLimitedRewards(allRewards)
totalLimitViolations := countTotalLimitViolations(allRewards)
userLimitViolations := countUserLimitViolations(allRewards, userRewardCounts)
limitedRewardDetails := limitedRewardSummaries(allRewards)
topRewards := topRewardSummaries(allRewards, 14)
assert(totalLimitViolations == 0, fmt.Sprintf("total limit violations = %d", totalLimitViolations))
assert(userLimitViolations == 0, fmt.Sprintf("user limit violations = %d", userLimitViolations))
assert(noAvailableHits == 0, fmt.Sprintf("no available hits = %d", noAvailableHits))
result := summary{
Seed: seed,
TargetRewardItems: target,
ActualRewardItems: rewardItems,
DrawRequests: drawRequests,
RandomUsers: userCount,
UniqueUsersDrawn: len(uniqueUsers),
DrawTimesRequests: drawTimesRequests,
DrawTimesRewardItems: drawTimesRewardItems,
CategoryRewardItems: categoryRewardItems,
LimitedRewards: limitedRewards,
ExhaustedLimitedRewards: exhaustedLimitedRewards,
TotalLimitViolations: totalLimitViolations,
UserLimitViolations: userLimitViolations,
NoAvailableHits: noAvailableHits,
LimitedRewardDetails: limitedRewardDetails,
TopRewards: topRewards,
ElapsedMillis: time.Since(startedAt).Milliseconds(),
}
out, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
}
func specs() []categorySpec {
return []categorySpec{
{Category: "CLASSIC", RewardCount: 8, GoldBase: 10},
{Category: "LUXURY", RewardCount: 8, GoldBase: 100},
{Category: "ADVANCED", RewardCount: 12, GoldBase: 500},
}
}
func buildRandomConfig(rng *rand.Rand) (map[string][]*rewardConfig, []*rewardConfig) {
byCategory := map[string][]*rewardConfig{}
all := make([]*rewardConfig, 0, 28)
var rewardID int64 = 1
for _, spec := range specs() {
probabilities := randomProbabilities(spec.RewardCount, rng)
rewards := make([]*rewardConfig, 0, spec.RewardCount)
for index := 0; index < spec.RewardCount; index++ {
totalLimit := int64(0)
userLimit := int64(0)
if index == 0 {
totalLimit = int64(120 + rng.Intn(81))
userLimit = 1
}
if index == 1 {
totalLimit = int64(260 + rng.Intn(121))
userLimit = 2
}
reward := &rewardConfig{
ID: rewardID,
Category: spec.Category,
Sort: index + 1,
GoldAmount: spec.GoldBase * int64(spec.RewardCount-index),
Probability: probabilities[index],
TotalLimit: totalLimit,
UserLimit: userLimit,
}
rewardID++
rewards = append(rewards, reward)
all = append(all, reward)
}
byCategory[spec.Category] = rewards
}
return byCategory, all
}
func randomProbabilities(count int, rng *rand.Rand) []int {
raw := make([]int, count)
for i := 0; i < count; i++ {
raw[i] = rng.Intn(1000) + 1
}
raw[0] += 7000 + rng.Intn(3000)
raw[1] += 3500 + rng.Intn(2000)
rawTotal := 0
for _, value := range raw {
rawTotal += value
}
result := make([]int, count)
assigned := 0
for i, value := range raw {
probability := value * probabilityTotal / rawTotal
if probability <= 0 {
probability = 1
}
result[i] = probability
assigned += probability
}
result[0] += probabilityTotal - assigned
return result
}
func randomDrawTimes(rng *rand.Rand) int {
switch rng.Intn(3) {
case 0:
return 1
case 1:
return 10
default:
return 50
}
}
func prizePoolItems(rewards []*rewardConfig) []prizepool.Item {
items := make([]prizepool.Item, 0, len(rewards))
for _, reward := range rewards {
items = append(items, prizepool.Item{
ID: reward.ID,
Enabled: true,
Weight: reward.Probability,
TotalLimit: reward.TotalLimit,
UserLimit: reward.UserLimit,
IssuedCount: reward.IssuedCount,
})
}
return items
}
func countLimitedRewards(rewards []*rewardConfig) (int, int) {
limited := 0
exhausted := 0
for _, reward := range rewards {
if reward.TotalLimit <= 0 {
continue
}
limited++
if reward.IssuedCount >= reward.TotalLimit {
exhausted++
}
}
return limited, exhausted
}
func countTotalLimitViolations(rewards []*rewardConfig) int {
violations := 0
for _, reward := range rewards {
if reward.TotalLimit > 0 && reward.IssuedCount > reward.TotalLimit {
violations++
}
}
return violations
}
func countUserLimitViolations(rewards []*rewardConfig, userRewardCounts map[userRewardKey]int64) int {
userLimits := map[int64]int64{}
for _, reward := range rewards {
if reward.UserLimit > 0 {
userLimits[reward.ID] = reward.UserLimit
}
}
violations := 0
for key, count := range userRewardCounts {
if limit := userLimits[key.RewardID]; limit > 0 && count > limit {
violations++
}
}
return violations
}
func limitedRewardSummaries(rewards []*rewardConfig) []rewardSummary {
result := make([]rewardSummary, 0)
for _, reward := range rewards {
if reward.TotalLimit <= 0 {
continue
}
result = append(result, rewardSummary{
Category: reward.Category,
Sort: reward.Sort,
RewardID: reward.ID,
GoldAmount: reward.GoldAmount,
Probability: reward.Probability,
TotalLimit: reward.TotalLimit,
UserLimit: reward.UserLimit,
IssuedCount: reward.IssuedCount,
})
}
sort.SliceStable(result, func(i, j int) bool {
if result[i].Category == result[j].Category {
return result[i].Sort < result[j].Sort
}
return result[i].Category < result[j].Category
})
return result
}
func topRewardSummaries(rewards []*rewardConfig, limit int) []rewardSummary {
copied := append([]*rewardConfig(nil), rewards...)
sort.SliceStable(copied, func(i, j int) bool {
if copied[i].IssuedCount == copied[j].IssuedCount {
return copied[i].ID < copied[j].ID
}
return copied[i].IssuedCount > copied[j].IssuedCount
})
if len(copied) > limit {
copied = copied[:limit]
}
result := make([]rewardSummary, 0, len(copied))
for _, reward := range copied {
result = append(result, rewardSummary{
Category: reward.Category,
Sort: reward.Sort,
RewardID: reward.ID,
GoldAmount: reward.GoldAmount,
Probability: reward.Probability,
TotalLimit: reward.TotalLimit,
UserLimit: reward.UserLimit,
IssuedCount: reward.IssuedCount,
})
}
return result
}
func envInt(name string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := strconv.Atoi(raw)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}
func envInt64(name string, fallback int64) int64 {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}
func assert(ok bool, message string) {
if !ok {
log.Fatal(message)
}
}