feat: derive authoritative ranked tiers

This commit is contained in:
Josh Creek
2026-08-31 21:25:53 +01:00
parent 726fe1ce2e
commit 846663e320
4 changed files with 115 additions and 1 deletions
+56
View File
@@ -65,6 +65,62 @@ type RankedProfile struct {
SeasonHistory []string
}
type RankTier string
const (
RankTierProvisional RankTier = "PROVISIONAL"
RankTierBronze RankTier = "BRONZE"
RankTierSilver RankTier = "SILVER"
RankTierGold RankTier = "GOLD"
RankTierPlatinum RankTier = "PLATINUM"
RankTierDiamond RankTier = "DIAMOND"
)
// TierBand is backend configuration, not client input. Bands are evaluated in
// ascending minimum-rating order and the highest matching band wins.
type TierBand struct {
Tier RankTier
MinRating float64
}
type TierPolicy struct {
bands []TierBand
}
func NewTierPolicy(bands []TierBand) (TierPolicy, error) {
if len(bands) == 0 || bands[0].MinRating > 0 {
return TierPolicy{}, fmt.Errorf("tier policy must start at or below zero")
}
copyBands := append([]TierBand(nil), bands...)
for i, band := range copyBands {
if band.Tier == "" || math.IsNaN(band.MinRating) || math.IsInf(band.MinRating, 0) || (i > 0 && band.MinRating <= copyBands[i-1].MinRating) {
return TierPolicy{}, fmt.Errorf("tier bands must have unique ascending finite thresholds")
}
}
return TierPolicy{bands: copyBands}, nil
}
// RankedTier is the only tier derivation entry point. It deliberately accepts
// RankedProfile rather than Rating, so a casual rating cannot be accidentally
// exposed as a ranked tier. The caller serializes this result from the
// authoritative backend response; clients do not reproduce these thresholds.
func RankedTier(profile RankedProfile, policy TierPolicy) (RankTier, error) {
if profile.RankedGames < 0 || len(policy.bands) == 0 || math.IsNaN(profile.Value) || math.IsInf(profile.Value, 0) {
return "", fmt.Errorf("invalid ranked tier input")
}
if RankedIsProvisional(profile) {
return RankTierProvisional, nil
}
tier := policy.bands[0].Tier
for _, band := range policy.bands {
if profile.Value < band.MinRating {
break
}
tier = band.Tier
}
return tier, nil
}
type RankedSeason struct {
SeasonID string
StartsAt time.Time