package logic import ( "math/rand" "sort" "time" ) // RunMonteCarlo runs a Monte Carlo simulation of the remaining matches. // Returns a slice of TeamProbability sorted by Group and then by TotalQualProb descending. func RunMonteCarlo(teams []Team, matches []Match, scoreOutcomes []ScoreOutcome, numSimulations int) []TeamProbability { if numSimulations <= 0 { numSimulations = 50000 } r := rand.New(rand.NewSource(time.Now().UnixNano())) sm := NewScoreModel(scoreOutcomes) // Map teams to groups teamGroups := make(map[string]string) groupsTeams := make(map[string][]string) for _, t := range teams { teamGroups[t.Name] = t.Group groupsTeams[t.Group] = append(groupsTeams[t.Group], t.Name) } directCounts := make(map[string]int) thirdCounts := make(map[string]int) // Separate completed and uncompleted matches var completed []Match var uncompleted []Match for _, m := range matches { if m.IsCompleted { completed = append(completed, m) } else { uncompleted = append(uncompleted, m) } } for sim := 0; sim < numSimulations; sim++ { // Clone completed matches and simulate uncompleted ones simMatches := make([]Match, len(completed)+len(uncompleted)) copy(simMatches, completed) for i, m := range uncompleted { outcome := sm.RandomScore(r) simMatches[len(completed)+i] = Match{ TeamA: m.TeamA, TeamB: m.TeamB, IsCompleted: true, ScoreA: outcome.ScoreA, ScoreB: outcome.ScoreB, } } // Calculate standings for each group thirdPlaced := make([]*TeamStats, 0, 12) for grp, grpTeams := range groupsTeams { var grpMatches []Match for _, m := range simMatches { if teamGroups[m.TeamA] == grp && teamGroups[m.TeamB] == grp { grpMatches = append(grpMatches, m) } } // Shuffle group teams to randomize tie-breaks in SortGroupStandings shuffledGrpTeams := make([]string, len(grpTeams)) copy(shuffledGrpTeams, grpTeams) r.Shuffle(len(shuffledGrpTeams), func(i, j int) { shuffledGrpTeams[i], shuffledGrpTeams[j] = shuffledGrpTeams[j], shuffledGrpTeams[i] }) standings := CalculateStandings(shuffledGrpTeams, grpMatches) // Top 2 qualify directly if len(standings) > 0 { directCounts[standings[0].TeamName]++ } if len(standings) > 1 { directCounts[standings[1].TeamName]++ } // 3rd placed team goes to the pool if len(standings) > 2 { thirdPlaced = append(thirdPlaced, standings[2]) } } // Shuffle third-placed list to randomize cross-group tie-breaks in CompareThirdPlaced r.Shuffle(len(thirdPlaced), func(i, j int) { thirdPlaced[i], thirdPlaced[j] = thirdPlaced[j], thirdPlaced[i] }) CompareThirdPlaced(thirdPlaced) limit := 8 if len(thirdPlaced) < limit { limit = len(thirdPlaced) } for i := 0; i < limit; i++ { thirdCounts[thirdPlaced[i].TeamName]++ } } // Build result slice res := make([]TeamProbability, 0, len(teams)) for _, t := range teams { dQual := float64(directCounts[t.Name]) / float64(numSimulations) tQual := float64(thirdCounts[t.Name]) / float64(numSimulations) res = append(res, TeamProbability{ TeamName: t.Name, Group: t.Group, DirectQualProb: dQual, ThirdQualProb: tQual, TotalQualProb: dQual + tQual, }) } // Sort results by Group name first, then by TotalQualProb descending sort.Slice(res, func(i, j int) bool { if res[i].Group != res[j].Group { return res[i].Group < res[j].Group } return res[i].TotalQualProb > res[j].TotalQualProb }) return res }