//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) } }