package smashegg import ( "fmt" "math" "net/http" "sort" "chatapp3-golang/internal/model" ) type probabilityItem struct { index int value int64 } func applyAutoProbabilitiesToInputs(targetRTP int, options []DrawOptionInput, rewards []RewardConfigInput, poolName string) ([]RewardConfigInput, error) { if err := validateAutoProbabilitiesForInputs(targetRTP, options, rewards, poolName); err != nil { return nil, err } baseOption, ok := firstEnabledInputOption(options) if !ok { return rewards, nil } probabilities, err := autoProbabilitiesForInputOption(targetRTP, baseOption, rewards, poolName) if err != nil { return nil, err } for index := range rewards { rewards[index].Probability = probabilities[index] } return rewards, nil } func validateAutoProbabilitiesForInputs(targetRTP int, options []DrawOptionInput, rewards []RewardConfigInput, poolName string) error { for _, option := range options { if !option.Enabled { continue } probabilities, err := autoProbabilitiesForInputOption(targetRTP, option, rewards, poolName) if err != nil { return err } weightedValue := weightedInputRewardValue(rewards, probabilities) rtp := drawOptionRTPBasisPointsFromWeightedValue(weightedValue, option.Times, option.PriceGold) if rtp < targetRTP-rtpToleranceBasisPoints || rtp > targetRTP+rtpToleranceBasisPoints { return NewAppError( http.StatusBadRequest, "invalid_rtp", fmt.Sprintf("%s pool option %s auto RTP %s%% is outside target %s%% ±1.00%%", poolName, option.Label, basisPointsPercent(rtp), basisPointsPercent(targetRTP)), ) } } return nil } func rewardsWithAutoProbabilitiesForOption(targetRTP int, option model.SmashEggDrawOptionConfig, rewards []model.SmashEggRewardConfig) ([]model.SmashEggRewardConfig, error) { probabilities, err := autoProbabilitiesForModelOption(targetRTP, option, rewards, normalizePoolType(firstPoolType(rewards))) if err != nil { return nil, err } next := append([]model.SmashEggRewardConfig(nil), rewards...) for index := range next { next[index].Probability = probabilities[index] } return next, nil } func firstPoolType(rewards []model.SmashEggRewardConfig) string { for _, reward := range rewards { if reward.PoolType != "" { return reward.PoolType } } return poolTypeGeneral } func expectedRewardValueForOption(targetRTP int, option model.SmashEggDrawOptionConfig, rewards []model.SmashEggRewardConfig) float64 { next, err := rewardsWithAutoProbabilitiesForOption(targetRTP, option, rewards) if err != nil { return expectedRewardValue(rewards) } return expectedRewardValue(next) } func autoProbabilitiesForInputOption(targetRTP int, option DrawOptionInput, rewards []RewardConfigInput, poolName string) ([]int, error) { items := make([]probabilityItem, 0, len(rewards)) for index, reward := range rewards { if reward.Enabled { items = append(items, probabilityItem{index: index, value: reward.RewardValueGold}) } } return calculateAutoProbabilities(targetWeightedValue(option.PriceGold, option.Times, targetRTP), len(rewards), items, poolName, option.Label) } func autoProbabilitiesForModelOption(targetRTP int, option model.SmashEggDrawOptionConfig, rewards []model.SmashEggRewardConfig, poolName string) ([]int, error) { items := make([]probabilityItem, 0, len(rewards)) for index, reward := range rewards { if reward.Enabled { items = append(items, probabilityItem{index: index, value: reward.RewardValueGold}) } } return calculateAutoProbabilities(targetWeightedValue(option.PriceGold, option.Times, targetRTP), len(rewards), items, poolName, option.Label) } func calculateAutoProbabilities(targetWeighted int64, rewardCount int, items []probabilityItem, poolName string, optionLabel string) ([]int, error) { probabilities := make([]int, rewardCount) if len(items) == 0 { return probabilities, NewAppError(http.StatusBadRequest, "empty_rewards", poolName+" pool requires at least one enabled reward") } if len(items) > probabilityTotal { return probabilities, NewAppError(http.StatusBadRequest, "too_many_rewards", fmt.Sprintf("%s pool enabled reward count cannot exceed %d", poolName, probabilityTotal)) } for _, item := range items { if item.value <= 0 { return probabilities, NewAppError(http.StatusBadRequest, "invalid_reward_value", poolName+" pool enabled reward value must be greater than 0") } } sort.SliceStable(items, func(i, j int) bool { if items[i].value == items[j].value { return items[i].index < items[j].index } return items[i].value < items[j].value }) var baseWeighted int64 for _, item := range items { probabilities[item.index] = 1 baseWeighted += item.value } remaining := probabilityTotal - len(items) if remaining == 0 { return probabilities, nil } residualTarget := targetWeighted - baseWeighted minResidual := items[0].value * int64(remaining) maxResidual := items[len(items)-1].value * int64(remaining) if residualTarget < minResidual { probabilities[items[0].index] += remaining return probabilities, nil } if residualTarget > maxResidual { probabilities[items[len(items)-1].index] += remaining return probabilities, nil } low := items[0] high := items[len(items)-1] for index, item := range items { if item.value*int64(remaining) <= residualTarget { low = item if index+1 < len(items) { high = items[index+1] } else { high = item } } } if low.value == high.value { probabilities[low.index] += remaining return probabilities, nil } spread := high.value - low.value highCount := roundDiv(residualTarget-low.value*int64(remaining), spread) if highCount < 0 { highCount = 0 } if highCount > int64(remaining) { highCount = int64(remaining) } highCount = bestHighCount(residualTarget, low.value, high.value, int64(remaining), highCount) lowCount := int64(remaining) - highCount probabilities[low.index] += int(lowCount) probabilities[high.index] += int(highCount) return probabilities, nil } func bestHighCount(target int64, lowValue int64, highValue int64, remaining int64, seed int64) int64 { best := seed bestDiff := weightedDiff(target, lowValue, highValue, remaining, seed) for _, candidate := range []int64{seed - 1, seed + 1} { if candidate < 0 || candidate > remaining { continue } diff := weightedDiff(target, lowValue, highValue, remaining, candidate) if diff < bestDiff { best = candidate bestDiff = diff } } return best } func weightedDiff(target int64, lowValue int64, highValue int64, remaining int64, highCount int64) int64 { value := lowValue*remaining + (highValue-lowValue)*highCount return absInt64(target - value) } func weightedInputRewardValue(rewards []RewardConfigInput, probabilities []int) int64 { var total int64 for index, reward := range rewards { if index >= len(probabilities) || !reward.Enabled || probabilities[index] <= 0 { continue } total += reward.RewardValueGold * int64(probabilities[index]) } return total } func targetWeightedValue(priceGold int64, times int, targetRTP int) int64 { if priceGold <= 0 || times <= 0 || targetRTP <= 0 { return 0 } return roundDiv(priceGold*int64(targetRTP)*int64(probabilityTotal), int64(times)*10000) } func firstEnabledInputOption(options []DrawOptionInput) (DrawOptionInput, bool) { for _, option := range options { if option.Enabled { return option, true } } return DrawOptionInput{}, false } func roundDiv(numerator int64, denominator int64) int64 { if denominator == 0 { return 0 } if numerator >= 0 { return (numerator + denominator/2) / denominator } return -int64(math.Round(float64(-numerator) / float64(denominator))) } func absInt64(value int64) int64 { if value < 0 { return -value } return value }