443 lines
18 KiB
Go
443 lines
18 KiB
Go
package luckygift
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
type scriptedStrategyRandom struct {
|
|
t *testing.T
|
|
indexes []int64
|
|
bounds []int64
|
|
position int
|
|
}
|
|
|
|
func (r *scriptedStrategyRandom) Int63n(n int64) (int64, error) {
|
|
r.t.Helper()
|
|
if r.position >= len(r.indexes) {
|
|
return 0, fmt.Errorf("scripted RNG exhausted at bound %d", n)
|
|
}
|
|
if len(r.bounds) > r.position && r.bounds[r.position] != n {
|
|
r.t.Fatalf("random call %d bound=%d want=%d", r.position, n, r.bounds[r.position])
|
|
}
|
|
value := r.indexes[r.position]
|
|
r.position++
|
|
return value, nil
|
|
}
|
|
|
|
func strategyTestConfig() StrategyConfig {
|
|
config := DefaultLuckyGiftStrategyConfig()
|
|
// The 0/5/50/200/1000 table is the supplied image's deliberately unsafe
|
|
// 2650% example. It is a test fixture only; the exported default stays at 98%.
|
|
config.Tiers = []StrategyTier{
|
|
{ID: "0x", MultiplierPPM: 0, BaseWeightPPM: 600_000, Enabled: true},
|
|
{ID: "5x", MultiplierPPM: 5_000_000, BaseWeightPPM: 200_000, Enabled: true},
|
|
{ID: "50x", MultiplierPPM: 50_000_000, BaseWeightPPM: 150_000, Enabled: true},
|
|
{ID: "200x", MultiplierPPM: 200_000_000, BaseWeightPPM: 40_000, Jackpot: true, JackpotWeight: 4, Enabled: true},
|
|
{ID: "500x", MultiplierPPM: 500_000_000, BaseWeightPPM: 0, Jackpot: true, JackpotWeight: 2, Enabled: true},
|
|
{ID: "1000x", MultiplierPPM: 1_000_000_000, BaseWeightPPM: 10_000, Jackpot: true, JackpotWeight: 1, Enabled: true},
|
|
}
|
|
// Core P/W tests isolate the base distribution; dynamic factors have dedicated
|
|
// boundary tests below and must not silently move scripted intervals here.
|
|
config.LowWaterThresholdCoins = 0
|
|
config.HighWaterThresholdCoins = 1_000_000_000
|
|
config.LowWaterFactorPPM = StrategyPPMScale
|
|
config.HighWaterFactorPPM = StrategyPPMScale
|
|
config.RechargeFactorPPM = StrategyPPMScale
|
|
config.JackpotMechanism1Enabled = false
|
|
config.JackpotMechanism2Enabled = false
|
|
config.DailyJackpotLimit = 1_000_000
|
|
return config
|
|
}
|
|
|
|
func strategyTestInput(price int64) StrategyInput {
|
|
return StrategyInput{GiftPriceCoins: price}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyDeletesUnaffordableTierAndZeroBeforeRedraw(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
random := &scriptedStrategyRandom{
|
|
t: t,
|
|
// 950000 selects 200x from the original 1,000,000 range. After deleting
|
|
// 200x+0x, 359999 selects 1000x; the last zero selects affordable 5x.
|
|
indexes: []int64{950_000, 359_999, 0},
|
|
bounds: []int64{1_000_000, 360_000, 350_000},
|
|
}
|
|
decision, err := DecideLuckyGiftStrategy(config, StrategyState{PoolBalanceCoins: 800}, strategyTestInput(10), random)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.SelectedTier.ID != "5x" || decision.PayoutCoins != 50 || decision.PoolAfterCoins != 750 {
|
|
t.Fatalf("decision=%+v", decision)
|
|
}
|
|
if got, want := decision.Trace.OriginalTierID, "200x"; got != want {
|
|
t.Fatalf("original tier=%s want=%s", got, want)
|
|
}
|
|
if got, want := decision.Trace.RedrawTierIDs, []string{"1000x", "5x"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("redraws=%v want=%v", got, want)
|
|
}
|
|
wantRemoved := []StrategyRemovalTrace{
|
|
{TierID: "200x", Reason: StrategyReasonPoolInsufficient},
|
|
{TierID: "0x", Reason: StrategyRemovalPWRedrawZero},
|
|
{TierID: "1000x", Reason: StrategyReasonPoolInsufficient},
|
|
}
|
|
if !reflect.DeepEqual(decision.Trace.Removed, wantRemoved) {
|
|
t.Fatalf("removed=%+v want=%+v", decision.Trace.Removed, wantRemoved)
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyPWEqualityAndCandidateExhaustion(t *testing.T) {
|
|
t.Run("W equals P is payable", func(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
decision, err := DecideLuckyGiftStrategy(config, StrategyState{PoolBalanceCoins: 2_000}, strategyTestInput(10), &scriptedStrategyRandom{t: t, indexes: []int64{950_000}, bounds: []int64{1_000_000}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.SelectedTier.ID != "200x" || decision.PayoutCoins != 2_000 || decision.PoolAfterCoins != 0 {
|
|
t.Fatalf("P=W decision=%+v", decision)
|
|
}
|
|
})
|
|
|
|
t.Run("all positive tiers impossible falls back to zero", func(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
random := &scriptedStrategyRandom{
|
|
t: t,
|
|
// Exhaust 1000x, 200x, 50x, then 5x. Zero is removed with the first
|
|
// failed P/W comparison, so no extra random call can hide exhaustion.
|
|
indexes: []int64{999_999, 350_000, 349_999, 0},
|
|
bounds: []int64{1_000_000, 390_000, 350_000, 200_000},
|
|
}
|
|
decision, err := DecideLuckyGiftStrategy(config, StrategyState{PoolBalanceCoins: 49}, strategyTestInput(10), random)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.PayoutCoins != 0 || decision.SelectedTier.ID != "0x" || decision.Trace.FinalReason != StrategyReasonNoPayableTier {
|
|
t.Fatalf("exhausted decision=%+v", decision)
|
|
}
|
|
if decision.NextState.PoolBalanceCoins < 0 {
|
|
t.Fatalf("pool became negative: %d", decision.NextState.PoolBalanceCoins)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLuckyGiftStrategyMissProtectionSixthDraw(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
state := StrategyState{PoolBalanceCoins: 800, ConsecutiveZeroDraws: 5}
|
|
decision, err := DecideLuckyGiftStrategy(config, state, strategyTestInput(10), &scriptedStrategyRandom{t: t, indexes: []int64{0}, bounds: []int64{400_000}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.SelectedTier.ID != "5x" || decision.PayoutCoins != 50 || decision.NextState.ConsecutiveZeroDraws != 0 {
|
|
t.Fatalf("protected decision=%+v", decision)
|
|
}
|
|
if len(decision.Trace.Removed) == 0 || decision.Trace.Removed[0].Reason != StrategyRemovalMissProtection {
|
|
t.Fatalf("zero exclusion not traced: %+v", decision.Trace.Removed)
|
|
}
|
|
|
|
// Protection cannot manufacture money: when even 5x exceeds P, the strategy
|
|
// returns zero and leaves an explicit blocked reason instead of overdrawing.
|
|
blocked, err := DecideLuckyGiftStrategy(config, StrategyState{PoolBalanceCoins: 49, ConsecutiveZeroDraws: 5}, strategyTestInput(10), &scriptedStrategyRandom{t: t})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if blocked.PayoutCoins != 0 || blocked.Trace.FinalReason != StrategyReasonMissProtectionBlocked || blocked.NextState.PoolBalanceCoins != 49 {
|
|
t.Fatalf("blocked protection=%+v", blocked)
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyDynamicWeightBoundariesAndMultiplication(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
config.LowWaterThresholdCoins = 1_000
|
|
config.HighWaterThresholdCoins = 2_000
|
|
config.LowWaterFactorPPM = 700_000
|
|
config.HighWaterFactorPPM = 1_300_000
|
|
config.RechargeFactorPPM = 1_100_000
|
|
config.JackpotMechanism1Enabled = false
|
|
config.JackpotMechanism2Enabled = false
|
|
tests := []struct {
|
|
name string
|
|
pool int64
|
|
rechargeAge int64
|
|
hasRecharge bool
|
|
want5x int64
|
|
wantFactorNames []string
|
|
}{
|
|
{name: "low below threshold", pool: 999, want5x: 140_000, wantFactorNames: []string{"low_water"}},
|
|
{name: "low equality is middle", pool: 1_000, want5x: 200_000, wantFactorNames: []string{"identity"}},
|
|
{name: "high equality is middle", pool: 2_000, want5x: 200_000, wantFactorNames: []string{"identity"}},
|
|
{name: "high above threshold", pool: 2_001, want5x: 260_000, wantFactorNames: []string{"high_water"}},
|
|
{name: "recharge starts inclusive and multiplies low", pool: 999, hasRecharge: true, rechargeAge: 0, want5x: 154_000, wantFactorNames: []string{"low_water", "recent_recharge"}},
|
|
{name: "recharge end minus one inclusive", pool: 1_500, hasRecharge: true, rechargeAge: 299_999, want5x: 220_000, wantFactorNames: []string{"recent_recharge"}},
|
|
{name: "recharge end exclusive", pool: 1_500, hasRecharge: true, rechargeAge: 300_000, want5x: 200_000, wantFactorNames: []string{"identity"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
input := strategyTestInput(10)
|
|
input.NowMS = 1_000_000
|
|
input.HasRechargeFact = tt.hasRecharge
|
|
input.LastRechargeAtMS = input.NowMS - tt.rechargeAge
|
|
_, traces, err := PreviewLuckyGiftStrategyWeights(config, StrategyState{PoolBalanceCoins: tt.pool}, input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
trace := findWeightTrace(t, traces, "5x")
|
|
if trace.AdjustedWeightPPM != tt.want5x {
|
|
t.Fatalf("5x adjusted=%d want=%d full=%+v", trace.AdjustedWeightPPM, tt.want5x, trace)
|
|
}
|
|
factorNames := make([]string, 0, len(trace.Factors))
|
|
for _, factor := range trace.Factors {
|
|
factorNames = append(factorNames, factor.Name)
|
|
}
|
|
if !reflect.DeepEqual(factorNames, tt.wantFactorNames) {
|
|
t.Fatalf("factor names=%v want=%v", factorNames, tt.wantFactorNames)
|
|
}
|
|
var total int64
|
|
for _, item := range traces {
|
|
total += item.AdjustedWeightPPM
|
|
}
|
|
if total != StrategyPPMScale {
|
|
t.Fatalf("weights sum=%d", total)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyJackpotMechanismOneRequiresAllNonZeroWindows(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
config.JackpotMechanism1Enabled = true
|
|
config.DailyJackpotLimit = 5
|
|
base := StrategyState{
|
|
PoolBalanceCoins: 10_000,
|
|
GlobalRTP: StrategyRTP{WagerCoins: 100, PayoutCoins: 98},
|
|
UserDayRTP: StrategyRTP{WagerCoins: 100, PayoutCoins: 96},
|
|
User72HourRTP: StrategyRTP{WagerCoins: 100, PayoutCoins: 96},
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*StrategyState)
|
|
wantPrize bool
|
|
}{
|
|
{name: "all inclusive boundaries pass", wantPrize: true},
|
|
{name: "global above 98", mutate: func(s *StrategyState) { s.GlobalRTP.PayoutCoins = 99 }},
|
|
{name: "day above 96", mutate: func(s *StrategyState) { s.UserDayRTP.PayoutCoins = 97 }},
|
|
{name: "72h above 96", mutate: func(s *StrategyState) { s.User72HourRTP.PayoutCoins = 97 }},
|
|
{name: "global denominator zero", mutate: func(s *StrategyState) { s.GlobalRTP = StrategyRTP{} }},
|
|
{name: "day denominator zero", mutate: func(s *StrategyState) { s.UserDayRTP = StrategyRTP{} }},
|
|
{name: "72h denominator zero", mutate: func(s *StrategyState) { s.User72HourRTP = StrategyRTP{} }},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
state := base
|
|
if tt.mutate != nil {
|
|
tt.mutate(&state)
|
|
}
|
|
decision, err := DecideLuckyGiftStrategy(config, state, strategyTestInput(10), NewSeededStrategyRandom(7))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := decision.JackpotMechanism == StrategyJackpotMechanismRTPCompensation
|
|
if got != tt.wantPrize {
|
|
t.Fatalf("mechanism1=%v want=%v trace=%+v", got, tt.wantPrize, decision.Trace)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyMilestoneTokenRetainedThenConsumed(t *testing.T) {
|
|
if got := MilestoneTokensEarned(49, 101, 50); got != 2 {
|
|
t.Fatalf("crossed tokens=%d want=2", got)
|
|
}
|
|
config := strategyTestConfig()
|
|
config.JackpotMechanism2Enabled = true
|
|
config.DailyJackpotLimit = 5
|
|
config.MilestoneSpendCoins = 50
|
|
state := StrategyState{PoolBalanceCoins: 49, PendingMilestoneTokens: 1}
|
|
blocked, err := DecideLuckyGiftStrategy(config, state, strategyTestInput(10), NewSeededStrategyRandom(11))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if blocked.ConsumedMilestoneToken || blocked.NextState.PendingMilestoneTokens != 1 || !blocked.Trace.MilestoneTokenRetained {
|
|
t.Fatalf("blocked token decision=%+v", blocked)
|
|
}
|
|
if len(blocked.Trace.Removed) < 3 {
|
|
t.Fatalf("blocked jackpot removals=%+v want all 200/500/1000 tiers", blocked.Trace.Removed)
|
|
}
|
|
for _, removed := range blocked.Trace.Removed[:3] {
|
|
if removed.Reason != StrategyReasonPoolInsufficient {
|
|
t.Fatalf("blocked jackpot removal=%+v", removed)
|
|
}
|
|
}
|
|
|
|
state = blocked.NextState
|
|
state.PoolBalanceCoins = 10_000
|
|
paid, err := DecideLuckyGiftStrategy(config, state, strategyTestInput(10), NewSeededStrategyRandom(12))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !paid.ConsumedMilestoneToken || paid.NextState.PendingMilestoneTokens != 0 || !paid.Jackpot || paid.PayoutCoins <= 0 {
|
|
t.Fatalf("consumed token decision=%+v", paid)
|
|
}
|
|
|
|
// Crossing happens during finalize, after this draw has already been selected.
|
|
// The token must therefore be persisted for the next request, not consumed now.
|
|
crossingState := StrategyState{PoolBalanceCoins: 1_000, UserDaySpendCoins: 40}
|
|
crossing, err := DecideLuckyGiftStrategy(config, crossingState, strategyTestInput(10), &scriptedStrategyRandom{t: t, indexes: []int64{0}, bounds: []int64{1_000_000}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if crossing.ConsumedMilestoneToken || crossing.NextState.PendingMilestoneTokens != 1 || crossing.Trace.MilestoneTokensEarned != 1 {
|
|
t.Fatalf("crossing decision=%+v", crossing)
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyDailyJackpotLimitAlsoConstrainsBaseDraw(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
config.JackpotMechanism2Enabled = true
|
|
config.DailyJackpotLimit = 5
|
|
state := StrategyState{PoolBalanceCoins: 20_000, PendingMilestoneTokens: 1, UserDailyJackpotWins: 5}
|
|
// Milestone is blocked first; ordinary draw then selects 200x, removes it for
|
|
// the same hard daily limit, removes zero, and redraws an allowed 5x.
|
|
random := &scriptedStrategyRandom{t: t, indexes: []int64{950_000, 600_000}, bounds: []int64{1_000_000, 960_000}}
|
|
decision, err := DecideLuckyGiftStrategy(config, state, strategyTestInput(10), random)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.Jackpot || decision.SelectedTier.ID != "5x" || decision.NextState.PendingMilestoneTokens != 1 || decision.NextState.UserDailyJackpotWins != 5 {
|
|
t.Fatalf("daily-limit decision=%+v", decision)
|
|
}
|
|
if decision.Trace.Draws[0].RemovedReason != StrategyReasonDailyJackpotLimit {
|
|
t.Fatalf("daily-limit removal=%+v", decision.Trace.Draws)
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategySixRiskCapacitiesAreHardInclusiveBounds(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
config.Tiers = []StrategyTier{
|
|
{ID: "0x", MultiplierPPM: 0, BaseWeightPPM: 0, Enabled: true},
|
|
{ID: "5x", MultiplierPPM: 5_000_000, BaseWeightPPM: StrategyPPMScale, Enabled: true},
|
|
}
|
|
setters := []struct {
|
|
name string
|
|
set func(*StrategyRiskCapacity, int64)
|
|
}{
|
|
{name: "single draw", set: func(c *StrategyRiskCapacity, v int64) { c.SingleDrawCoins = v }},
|
|
{name: "user hour", set: func(c *StrategyRiskCapacity, v int64) { c.UserHourCoins = v }},
|
|
{name: "user day", set: func(c *StrategyRiskCapacity, v int64) { c.UserDayCoins = v }},
|
|
{name: "device day", set: func(c *StrategyRiskCapacity, v int64) { c.DeviceDayCoins = v }},
|
|
{name: "room hour", set: func(c *StrategyRiskCapacity, v int64) { c.RoomHourCoins = v }},
|
|
{name: "anchor day", set: func(c *StrategyRiskCapacity, v int64) { c.AnchorDayCoins = v }},
|
|
}
|
|
for _, setter := range setters {
|
|
t.Run(setter.name, func(t *testing.T) {
|
|
capacity := StrategyRiskCapacity{Enabled: true, SingleDrawCoins: 1_000, UserHourCoins: 1_000, UserDayCoins: 1_000, DeviceDayCoins: 1_000, RoomHourCoins: 1_000, AnchorDayCoins: 1_000}
|
|
setter.set(&capacity, 50)
|
|
input := strategyTestInput(10)
|
|
input.RiskCapacity = capacity
|
|
allowed, err := DecideLuckyGiftStrategy(config, StrategyState{PoolBalanceCoins: 50}, input, NewSeededStrategyRandom(1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if allowed.PayoutCoins != 50 || allowed.PoolAfterCoins != 0 {
|
|
t.Fatalf("boundary must pass: %+v", allowed)
|
|
}
|
|
setter.set(&capacity, 49)
|
|
input.RiskCapacity = capacity
|
|
blocked, err := DecideLuckyGiftStrategy(config, StrategyState{PoolBalanceCoins: 50}, input, NewSeededStrategyRandom(1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if blocked.PayoutCoins != 0 || blocked.Trace.FinalReason != StrategyReasonNoPayableTier {
|
|
t.Fatalf("below boundary must block: %+v", blocked)
|
|
}
|
|
if len(blocked.Trace.Removed) != 1 || blocked.Trace.Removed[0].TierID != "5x" || blocked.Trace.Removed[0].Reason != StrategyReasonRiskCapacity {
|
|
t.Fatalf("risk retry must not remove zero: %+v", blocked.Trace.Removed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyRechargeStagesAndFundConservation(t *testing.T) {
|
|
config := DefaultLuckyGiftStrategyConfig()
|
|
stageCases := []struct {
|
|
seven, thirty int64
|
|
want string
|
|
}{
|
|
{seven: 0, thirty: 0, want: StageNovice},
|
|
{seven: 0, thirty: 1, want: StageNormal},
|
|
{seven: 1, thirty: 1, want: StageAdvanced},
|
|
}
|
|
for _, tt := range stageCases {
|
|
if got := SelectLuckyGiftRechargeStage(config, tt.seven, tt.thirty); got != tt.want {
|
|
t.Fatalf("stage(%d,%d)=%s want=%s", tt.seven, tt.thirty, got, tt.want)
|
|
}
|
|
}
|
|
split, err := SplitLuckyGiftStrategyFunds(100, config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if split != (StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 1, AnchorReturnCoins: 1}) {
|
|
t.Fatalf("split=%+v", split)
|
|
}
|
|
if split.PublicPoolCoins+split.ProfitPoolCoins+split.AnchorReturnCoins != 100 {
|
|
t.Fatal("fund split did not conserve money")
|
|
}
|
|
small, err := SplitLuckyGiftStrategyFunds(10, config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if small != (StrategyFundSplit{PublicPoolCoins: 9, ProfitPoolCoins: 1, AnchorReturnCoins: 0}) {
|
|
t.Fatalf("small split=%+v want public floor with residue in profit", small)
|
|
}
|
|
if initial := InitialLuckyGiftStrategyState(config); initial.PoolBalanceCoins != config.ColdStartPoolCoins || initial.PoolBalanceCoins < 0 {
|
|
t.Fatalf("initial=%+v", initial)
|
|
}
|
|
}
|
|
|
|
func TestDefaultLuckyGiftStrategyNominalEVIsExactlyNinetyEightPercent(t *testing.T) {
|
|
config := DefaultLuckyGiftStrategyConfig()
|
|
var weightedMultiplierMicros int64
|
|
for _, tier := range config.Tiers {
|
|
weightedMultiplierMicros += tier.BaseWeightPPM * tier.MultiplierPPM / StrategyPPMScale
|
|
}
|
|
if weightedMultiplierMicros != 980_000 {
|
|
t.Fatalf("default nominal EV=%dppm want=980000ppm", weightedMultiplierMicros)
|
|
}
|
|
for _, tier := range config.Tiers {
|
|
if tier.Jackpot && tier.BaseWeightPPM != 0 {
|
|
t.Fatalf("default jackpot tier %s leaked into ordinary probability", tier.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLuckyGiftStrategyCombinationPriority(t *testing.T) {
|
|
config := strategyTestConfig()
|
|
config.JackpotMechanism1Enabled = true
|
|
config.JackpotMechanism2Enabled = true
|
|
config.DailyJackpotLimit = 5
|
|
state := StrategyState{
|
|
PoolBalanceCoins: 10_000, ConsecutiveZeroDraws: 5, PendingMilestoneTokens: 1,
|
|
GlobalRTP: StrategyRTP{WagerCoins: 100, PayoutCoins: 98},
|
|
UserDayRTP: StrategyRTP{WagerCoins: 100, PayoutCoins: 96},
|
|
User72HourRTP: StrategyRTP{WagerCoins: 100, PayoutCoins: 96},
|
|
}
|
|
decision, err := DecideLuckyGiftStrategy(config, state, strategyTestInput(10), NewSeededStrategyRandom(8))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decision.JackpotMechanism != StrategyJackpotMechanismMilestone || !decision.ConsumedMilestoneToken {
|
|
t.Fatalf("milestone must outrank mechanism1/miss protection: %+v", decision)
|
|
}
|
|
}
|
|
|
|
func findWeightTrace(t *testing.T, traces []StrategyWeightTrace, tierID string) StrategyWeightTrace {
|
|
t.Helper()
|
|
for _, trace := range traces {
|
|
if trace.TierID == tierID {
|
|
return trace
|
|
}
|
|
}
|
|
t.Fatalf("weight trace %s not found", tierID)
|
|
return StrategyWeightTrace{}
|
|
}
|