244 lines
7.6 KiB
Go
244 lines
7.6 KiB
Go
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
|
|
}
|