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  }