144 lines
5.4 KiB
Go
144 lines
5.4 KiB
Go
// Command strategy-sim runs the product matrix against the exact pure kernel used by
|
||
// production. It performs no database or network I/O; fixed seeds make every report
|
||
// reproducible in CI, during review, and after a rule-version change.
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
|
||
domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
|
||
)
|
||
|
||
const defaultSimulationSeed int64 = 20_260_712
|
||
|
||
type simulationOptions struct {
|
||
Seed int64
|
||
MonteCarloN int
|
||
LongRunN int
|
||
GroupLongRunN int
|
||
}
|
||
|
||
type simulationReport struct {
|
||
Seed int64 `json:"seed"`
|
||
MonteCarloDraws int `json:"monte_carlo_draws"`
|
||
LongRunDraws int `json:"long_run_draws"`
|
||
GroupLongRunDraws int `json:"group_long_run_draws"`
|
||
Nominal nominalEVReport `json:"nominal_probability_table"`
|
||
Scenarios []scenarioResult `json:"scenarios"`
|
||
AllPassed bool `json:"all_passed"`
|
||
}
|
||
|
||
type nominalEVReport struct {
|
||
ExpectedMultiplier float64 `json:"expected_multiplier"`
|
||
NominalRTPPercent float64 `json:"nominal_rtp_percent"`
|
||
TargetRTPPercent float64 `json:"target_rtp_percent"`
|
||
PassesTarget bool `json:"passes_target"`
|
||
Conclusion string `json:"conclusion"`
|
||
}
|
||
|
||
type scenarioResult struct {
|
||
Name string `json:"name"`
|
||
Passed bool `json:"passed"`
|
||
Summary string `json:"summary"`
|
||
Data any `json:"data,omitempty"`
|
||
}
|
||
|
||
func main() {
|
||
options := simulationOptions{}
|
||
jsonOut := ""
|
||
flag.Int64Var(&options.Seed, "seed", defaultSimulationSeed, "deterministic simulation seed")
|
||
flag.IntVar(&options.MonteCarloN, "monte-carlo-draws", 100_000, "draws for probability Monte Carlo")
|
||
flag.IntVar(&options.LongRunN, "long-run-draws", 100_000, "draws for funded RTP long run")
|
||
flag.IntVar(&options.GroupLongRunN, "group-long-run-draws", 30_000, "draws per recharge-stage group")
|
||
flag.StringVar(&jsonOut, "json-out", "", "optional path for the same JSON report printed to stdout")
|
||
flag.Parse()
|
||
if options.MonteCarloN <= 0 || options.LongRunN <= 0 || options.GroupLongRunN <= 0 {
|
||
fmt.Fprintln(os.Stderr, "all draw counts must be positive")
|
||
os.Exit(2)
|
||
}
|
||
|
||
report := runSimulation(options)
|
||
encoded, err := json.MarshalIndent(report, "", " ")
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "encode report: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
printHumanSummary(report)
|
||
fmt.Println("\n--- JSON REPORT ---")
|
||
fmt.Println(string(encoded))
|
||
if jsonOut != "" {
|
||
if err := os.WriteFile(jsonOut, append(encoded, '\n'), 0o644); err != nil {
|
||
fmt.Fprintf(os.Stderr, "write JSON report: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
if !report.AllPassed {
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func runSimulation(options simulationOptions) simulationReport {
|
||
// The runtime default remains a safe 98% table. Only this simulator opts into
|
||
// the supplied image's 2650% table so the report can expose why it is unsafe.
|
||
config := imageExampleConfig(domain.DefaultLuckyGiftStrategyConfig())
|
||
nominal := calculateNominalEV(config)
|
||
scenarios := []scenarioResult{
|
||
simulateImage800Redraw(config),
|
||
simulatePWBoundaries(config),
|
||
simulateFundSplitAndColdStart(config),
|
||
simulateBatchCounts(config, options.Seed),
|
||
simulateSettlementWagerWindow(),
|
||
simulateWaterBoundaries(config),
|
||
simulateRechargeWindowBoundaries(config),
|
||
simulateProbabilityMonteCarlo(config, options.Seed+10, options.MonteCarloN),
|
||
simulateFundedRTPLongRun(config, options.Seed+20, options.LongRunN),
|
||
simulateSettledRoundQualification(config, options.Seed+30),
|
||
simulatePoolBlockedQualificationConsumption(config, options.Seed+40),
|
||
simulateJackpotSet(config, options.Seed+50),
|
||
simulateDailyJackpotLimit(config, options.Seed+60),
|
||
simulateSixRiskCapacities(config),
|
||
simulateRechargeStages(config),
|
||
simulateCombinationPriority(config, options.Seed+70),
|
||
simulateConcurrencyAndIdempotency(config, options.Seed+80),
|
||
simulateLongRunGroups(config, options.Seed+90, options.GroupLongRunN),
|
||
}
|
||
allPassed := !nominal.PassesTarget // 2650% must be explicitly rejected, not relabelled as a 98% pass.
|
||
for _, scenario := range scenarios {
|
||
allPassed = allPassed && scenario.Passed
|
||
}
|
||
return simulationReport{
|
||
Seed: options.Seed, MonteCarloDraws: options.MonteCarloN, LongRunDraws: options.LongRunN,
|
||
GroupLongRunDraws: options.GroupLongRunN, Nominal: nominal, Scenarios: scenarios, AllPassed: allPassed,
|
||
}
|
||
}
|
||
|
||
func calculateNominalEV(config domain.StrategyConfig) nominalEVReport {
|
||
var expected float64
|
||
for _, tier := range config.Tiers {
|
||
if tier.Enabled {
|
||
expected += float64(tier.BaseWeightPPM) / float64(domain.StrategyPPMScale) * float64(tier.MultiplierPPM) / float64(domain.StrategyPPMScale)
|
||
}
|
||
}
|
||
return nominalEVReport{
|
||
ExpectedMultiplier: expected, NominalRTPPercent: expected * 100, TargetRTPPercent: 98,
|
||
PassesTarget: expected <= 0.98,
|
||
Conclusion: "图示固定概率表的名义EV为26.5x(2650%),不能作为98% RTP方案;只有P/W资金约束后的有资金长跑口径可验证不透支。",
|
||
}
|
||
}
|
||
|
||
func printHumanSummary(report simulationReport) {
|
||
fmt.Printf("幸运礼物策略模拟 seed=%d\n", report.Seed)
|
||
fmt.Printf("名义概率表: EV=%.2fx, RTP=%.2f%%, 98%%校验=%v(预期必须为false)\n", report.Nominal.ExpectedMultiplier, report.Nominal.NominalRTPPercent, report.Nominal.PassesTarget)
|
||
for _, scenario := range report.Scenarios {
|
||
status := "PASS"
|
||
if !scenario.Passed {
|
||
status = "FAIL"
|
||
}
|
||
fmt.Printf("[%s] %s: %s\n", status, scenario.Name, scenario.Summary)
|
||
}
|
||
fmt.Printf("总结果: all_passed=%v\n", report.AllPassed)
|
||
}
|