go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/experiments/matchmaker/pkg/sim/clustered_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/rand" 12 13 "go.charczuk.com/sdk/iter" 14 ) 15 16 var _ Matchmaker = (*ClusteredMatchmaker)(nil) 17 18 // BalancedMatchmaker will try to create "balanced" teams 19 // from the players currently in the queue according to average ratings. 20 type ClusteredMatchmaker struct { 21 r *rand.Rand 22 q []*Player 23 } 24 25 func (cm *ClusteredMatchmaker) Init(r *rand.Rand) { 26 cm.r = r 27 } 28 29 func (cm *ClusteredMatchmaker) Queue(players ...*Player) { 30 for _, p := range players { 31 cm.q = iter.InsertSortedBy(cm.q, p, playerRatingDesc) 32 } 33 } 34 35 func (cm *ClusteredMatchmaker) QueueLen() int { 36 return len(cm.q) 37 } 38 39 var clusterDistances = []int{100, 200, 500} 40 41 func (cm *ClusteredMatchmaker) FormGames(cfg MatchmakerConfig) (out []MatchmakerGame) { 42 if cfg.MaxGames == 0 { 43 return 44 } 45 46 playersPerGame := cfg.TeamSize * 2 47 if len(cm.q) < playersPerGame { 48 return 49 } 50 51 allPlayers := NewLookup(cm.q) 52 for _, distance := range clusterDistances { 53 if len(out) == cfg.MaxGames { 54 break 55 } 56 if len(allPlayers) == 0 { 57 break 58 } 59 60 clusters := cm.clusterPlayersByDistance(allPlayers, playersPerGame, distance) 61 for _, c := range clusters { 62 game := cm.createGameFromCluster(c) 63 out = append(out, game) 64 if len(out) == cfg.MaxGames { 65 break 66 } 67 } 68 } 69 cm.requeueRemainingPlayers(cm.q, out) 70 return 71 } 72 73 func (cm *ClusteredMatchmaker) playerDistance(p0, p1 *Player) int { 74 if p0.Rating > p1.Rating { 75 return p0.Rating - p1.Rating 76 } 77 return p1.Rating - p0.Rating 78 } 79 80 func (cm *ClusteredMatchmaker) clusterPlayersByDistance(allPlayers PlayerLookup, clusterSize, maxDistance int) (clusters [][]*Player) { 81 for _, p0 := range cm.q { 82 if len(allPlayers) < clusterSize { 83 return 84 } 85 if !allPlayers.Has(p0) { 86 continue 87 } 88 cluster := make(PlayerLookup) 89 cluster.Add(p0) 90 for _, p1 := range allPlayers { 91 if p0.ID == p1.ID { 92 continue 93 } 94 if !allPlayers.Has(p1) { 95 continue 96 } 97 if cm.playerDistance(p0, p1) < maxDistance { 98 cluster.Add(p1) 99 if len(cluster) == clusterSize { 100 break 101 } 102 } 103 } 104 if len(cluster) == clusterSize { 105 var clusterToAdd []*Player 106 for _, p := range cluster { 107 allPlayers.Del(p) 108 clusterToAdd = append(clusterToAdd, p) 109 } 110 clusters = append(clusters, clusterToAdd) 111 } 112 } 113 return 114 } 115 116 func (cm *ClusteredMatchmaker) createGameFromCluster(cluster []*Player) (game MatchmakerGame) { 117 playersLookup := NewLookup(cluster) 118 var alt bool 119 for len(playersLookup) > 0 { 120 p, _ := playersLookup.PopRandom(cm.r) 121 if alt { 122 game.TeamA = append(game.TeamA, p) 123 } else { 124 game.TeamB = append(game.TeamB, p) 125 } 126 alt = !alt 127 } 128 return 129 } 130 131 func (cm *ClusteredMatchmaker) requeueRemainingPlayers(allPlayers []*Player, games []MatchmakerGame) { 132 allPlayersLookup := NewLookup(allPlayers) 133 for _, g := range games { 134 for _, p := range g.TeamA { 135 allPlayersLookup.Del(p) 136 } 137 for _, p := range g.TeamB { 138 allPlayersLookup.Del(p) 139 } 140 } 141 cm.q = nil 142 for _, player := range allPlayersLookup { 143 cm.Queue(player) 144 } 145 }