389 lines
11 KiB
Go
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)
|
|
}
|
|
}
|