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  }