144 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.5x2650%不能作为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)
}