go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/experiments/matchmaker/pkg/sim/balanced_mm.go (about) 1 /* 2 3 Copyright (c) 2024 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package sim 9 10 import ( 11 "math" 12 "math/rand" 13 14 "go.charczuk.com/sdk/iter" 15 ) 16 17 var _ Matchmaker = (*BalancedMatchmaker)(nil) 18 19 // BalancedMatchmaker will try to create "balanced" teams 20 // from the players currently in the queue according to average ratings. 21 type BalancedMatchmaker struct { 22 r *rand.Rand 23 q []*Player 24 } 25 26 func (bm *BalancedMatchmaker) Init(r *rand.Rand) { 27 bm.r = r 28 } 29 30 func playerRatingDesc(p *Player) int { 31 return -p.Rating 32 } 33 34 func (bm *BalancedMatchmaker) Queue(players ...*Player) { 35 for _, p := range players { 36 bm.q = iter.InsertSortedBy(bm.q, p, playerRatingDesc) 37 } 38 } 39 40 func (bm *BalancedMatchmaker) QueueLen() int { 41 if bm.q == nil { 42 return 0 43 } 44 return len(bm.q) 45 } 46 47 type rankTier struct { 48 Name string 49 MaxRating int 50 MeanRating int 51 MinRating int 52 } 53 54 func (rt rankTier) Key() string { return rt.Name } 55 56 var rankTiers = []rankTier{ 57 {Name: "t500", MaxRating: math.MaxInt, MeanRating: 3550, MinRating: 3000}, 58 {Name: "grand master", MaxRating: 3000, MeanRating: 2750, MinRating: 2500}, 59 {Name: "master", MaxRating: 2500, MeanRating: 2250, MinRating: 2000}, 60 {Name: "diamond", MaxRating: 2000, MeanRating: 1875, MinRating: 1750}, 61 {Name: "platinum", MaxRating: 1750, MeanRating: 1675, MinRating: 1500}, 62 {Name: "gold", MaxRating: 1500, MeanRating: 1250, MinRating: 1000}, 63 {Name: "silver", MaxRating: 1000, MeanRating: 750, MinRating: 500}, 64 {Name: "bronze", MaxRating: 500, MeanRating: 250, MinRating: 0}, 65 } 66 67 func (bm *BalancedMatchmaker) FormGames(cfg MatchmakerConfig) (out []MatchmakerGame) { 68 if cfg.MaxGames == 0 { 69 return 70 } 71 72 playersPerGame := cfg.TeamSize * 2 73 if len(bm.q) < playersPerGame { 74 return 75 } 76 77 maxGames := cfg.MaxGames 78 79 // do a first pass for new players 80 games := bm.formGamesNewPlayers(playersPerGame, maxGames, bm.q) 81 out = append(out, games...) 82 maxGames = maxGames - len(games) 83 84 rankTierBag := NewLookup[string, rankTier](rankTiers) 85 for _, tierCutoff := range rankTierBag { 86 if maxGames == 0 { 87 break 88 } 89 games := bm.formGamesForRankTier(playersPerGame, maxGames, bm.q, tierCutoff) 90 out = append(out, games...) 91 maxGames = maxGames - len(games) 92 } 93 bm.requeueRemainingPlayers(bm.q, out) 94 return 95 } 96 97 func (bm *BalancedMatchmaker) formGamesNewPlayers(playersPerGame, maxGames int, sortedPlayers []*Player) (games []MatchmakerGame) { 98 var newPlayers []*Player 99 for _, p := range sortedPlayers { 100 if p.Games <= 5 { 101 newPlayers = append(newPlayers, p) 102 } 103 } 104 if len(newPlayers) < playersPerGame { 105 return 106 } 107 108 newPlayersLookup := NewLookup(newPlayers) 109 for len(newPlayersLookup) >= playersPerGame && len(games) < maxGames { 110 var g MatchmakerGame 111 var alt bool 112 for x := 0; x < playersPerGame; x++ { 113 p, _ := newPlayersLookup.PopRandom(bm.r) 114 if alt { 115 g.TeamA = append(g.TeamA, p) 116 } else { 117 g.TeamB = append(g.TeamB, p) 118 } 119 alt = !alt 120 } 121 games = append(games, g) 122 } 123 return 124 } 125 126 func (bm *BalancedMatchmaker) formGamesForRankTier(playersPerGame, maxGames int, sortedPlayers []*Player, tier rankTier) (games []MatchmakerGame) { 127 var playersInTier []*Player 128 for _, p := range sortedPlayers { 129 if p.Rating > tier.MaxRating { 130 continue 131 } else if p.Rating <= tier.MinRating { 132 break 133 } 134 if p.Games > 5 { 135 playersInTier = append(playersInTier, p) 136 } 137 } 138 if len(playersInTier) < playersPerGame { 139 return 140 } 141 142 playersInTierLookup := NewLookup(playersInTier) 143 for len(playersInTierLookup) >= playersPerGame && len(games) < maxGames { 144 var g MatchmakerGame 145 var alt bool 146 for x := 0; x < playersPerGame; x++ { 147 p, _ := playersInTierLookup.PopRandom(bm.r) 148 if alt { 149 g.TeamA = append(g.TeamA, p) 150 } else { 151 g.TeamB = append(g.TeamB, p) 152 } 153 alt = !alt 154 } 155 games = append(games, g) 156 } 157 return 158 } 159 160 func (bm *BalancedMatchmaker) requeueRemainingPlayers(allPlayers []*Player, games []MatchmakerGame) { 161 allPlayersLookup := NewLookup(allPlayers) 162 for _, g := range games { 163 for _, p := range g.TeamA { 164 allPlayersLookup.Del(p) 165 } 166 for _, p := range g.TeamB { 167 allPlayersLookup.Del(p) 168 } 169 } 170 171 // PRETTY IMPORANT STEP HERE. 172 bm.q = nil 173 for _, player := range allPlayersLookup { 174 bm.Queue(player) 175 } 176 }