feat: add ranked season rollover policy

This commit is contained in:
Josh Creek
2026-08-31 20:29:11 +01:00
parent 1793eb7621
commit cc2cd80a01
16 changed files with 688 additions and 249 deletions
+57 -26
View File
@@ -7,29 +7,29 @@ import (
)
const (
MaxPlacementRTT = 100.0
MaxPlacementRTT = 100.0
MinRatingTolerance = 100.0
MaxRatingTolerance = 400.0
RatingWidenStep = 25.0
RatingWidenPeriod = 30.0
RatingWidenStep = 25.0
RatingWidenPeriod = 30.0
)
// Candidate is the server-side projection of a verified, live queue ticket.
// RTT values come from backend probes, never from the client request body.
type Candidate struct {
TicketID string
PlayerID string
Rating float64
EnqueuedAt time.Time
TicketID string
PlayerID string
Rating float64
EnqueuedAt time.Time
PredictedRTT map[string]float64
}
type Selection struct {
Players []Candidate
Region string
WorstRTT float64
TotalRTT float64
RatingRange float64
Players []Candidate
Region string
WorstRTT float64
TotalRTT float64
RatingRange float64
TotalWaitSeconds float64
}
@@ -153,38 +153,69 @@ func scoreSelection(players []Candidate, now time.Time) (Selection, bool) {
wait := 0.0
for _, player := range players {
rtt := player.PredictedRTT[region]
if rtt > worst { worst = rtt }
if rtt > worst {
worst = rtt
}
total += rtt
if player.Rating < minRating { minRating = player.Rating }
if player.Rating > maxRating { maxRating = player.Rating }
if seconds := now.Sub(player.EnqueuedAt).Seconds(); seconds > 0 { wait += seconds }
if player.Rating < minRating {
minRating = player.Rating
}
if player.Rating > maxRating {
maxRating = player.Rating
}
if seconds := now.Sub(player.EnqueuedAt).Seconds(); seconds > 0 {
wait += seconds
}
}
candidate := Selection{Players: append([]Candidate(nil), players...), Region: region, WorstRTT: worst, TotalRTT: total, RatingRange: maxRating - minRating, TotalWaitSeconds: wait}
if best.Players == nil || betterSelection(candidate, best) {
best = candidate
}
candidate := Selection{Players: append([]Candidate(nil), players...), Region: region, WorstRTT: worst, TotalRTT: total, RatingRange: maxRating-minRating, TotalWaitSeconds: wait}
if best.Players == nil || betterSelection(candidate, best) { best = candidate }
}
return best, true
}
func betterSelection(a, b Selection) bool {
if a.WorstRTT != b.WorstRTT { return a.WorstRTT < b.WorstRTT }
if a.TotalRTT != b.TotalRTT { return a.TotalRTT < b.TotalRTT }
if a.RatingRange != b.RatingRange { return a.RatingRange < b.RatingRange }
if a.TotalWaitSeconds != b.TotalWaitSeconds { return a.TotalWaitSeconds > b.TotalWaitSeconds }
if a.WorstRTT != b.WorstRTT {
return a.WorstRTT < b.WorstRTT
}
if a.TotalRTT != b.TotalRTT {
return a.TotalRTT < b.TotalRTT
}
if a.RatingRange != b.RatingRange {
return a.RatingRange < b.RatingRange
}
if a.TotalWaitSeconds != b.TotalWaitSeconds {
return a.TotalWaitSeconds > b.TotalWaitSeconds
}
return ticketIDs(a.Players) < ticketIDs(b.Players)
}
func containsTicket(players []Candidate, ticketID string) bool {
for _, player := range players { if player.TicketID == ticketID { return true } }
for _, player := range players {
if player.TicketID == ticketID {
return true
}
}
return false
}
func ticketIDs(players []Candidate) string {
ids := make([]string, 0, len(players))
for _, player := range players { ids = append(ids, player.TicketID) }
for _, player := range players {
ids = append(ids, player.TicketID)
}
sort.Strings(ids)
result := ""
for _, id := range ids { result += id + "\x00" }
for _, id := range ids {
result += id + "\x00"
}
return result
}
func abs(value float64) float64 { if value < 0 { return -value }; return value }
func abs(value float64) float64 {
if value < 0 {
return -value
}
return value
}