feat(multiplayer): expose ranked season countdown

This commit is contained in:
Josh Creek
2026-09-01 21:49:48 +01:00
parent 9240cd4b27
commit 51f6e19339
9 changed files with 47 additions and 20 deletions
+13 -8
View File
@@ -931,13 +931,14 @@ func assignmentChangedEvent(view AssignmentView, now time.Time) ControlPlaneEven
}
type rankedProfileResponse struct {
Rating float64 `json:"rating"`
RD float64 `json:"rd"`
Volatility float64 `json:"volatility"`
RankedGames int `json:"ranked_games"`
Tier string `json:"tier"`
Provisional bool `json:"provisional"`
SeasonID string `json:"season_id,omitempty"`
Rating float64 `json:"rating"`
RD float64 `json:"rd"`
Volatility float64 `json:"volatility"`
RankedGames int `json:"ranked_games"`
Tier string `json:"tier"`
Provisional bool `json:"provisional"`
SeasonID string `json:"season_id,omitempty"`
SeasonEndsAt string `json:"season_ends_at,omitempty"`
}
func (s *Service) profile(w http.ResponseWriter, r *http.Request) {
@@ -1000,7 +1001,11 @@ func (s *Service) rankedProfile(w http.ResponseWriter, r *http.Request) {
if seasonID == "" {
seasonID = profile.LastSeasonID
}
writeJSON(w, http.StatusOK, rankedProfileResponse{Rating: profile.Value, RD: profile.RD, Volatility: profile.Volatility, RankedGames: profile.RankedGames, Tier: string(tier), Provisional: domain.RankedIsProvisional(profile), SeasonID: seasonID})
seasonEndsAt := ""
if profile.CurrentSeasonID != "" && !profile.CurrentSeasonEndsAt.IsZero() {
seasonEndsAt = profile.CurrentSeasonEndsAt.UTC().Format(time.RFC3339)
}
writeJSON(w, http.StatusOK, rankedProfileResponse{Rating: profile.Value, RD: profile.RD, Volatility: profile.Volatility, RankedGames: profile.RankedGames, Tier: string(tier), Provisional: domain.RankedIsProvisional(profile), SeasonID: seasonID, SeasonEndsAt: seasonEndsAt})
}
type probeRequest struct {
+2 -2
View File
@@ -1079,7 +1079,7 @@ func TestRankedProfileAPIReturnsBackendTierAndHidesCasualData(t *testing.T) {
}
service := &Service{
Sessions: sessions,
RankedProfiles: map[string]domain.RankedProfile{"player-a": {Rating: domain.Rating{Value: 1600, RD: 200, Volatility: 0.06}, RankedGames: 10, CurrentSeasonID: "season-current", LastSeasonID: "season-1"}},
RankedProfiles: map[string]domain.RankedProfile{"player-a": {Rating: domain.Rating{Value: 1600, RD: 200, Volatility: 0.06}, RankedGames: 10, CurrentSeasonID: "season-current", CurrentSeasonEndsAt: now.Add(48 * time.Hour), LastSeasonID: "season-1"}},
TierPolicy: policy,
Now: func() time.Time { return now },
}
@@ -1099,7 +1099,7 @@ func TestRankedProfileAPIReturnsBackendTierAndHidesCasualData(t *testing.T) {
if err := json.NewDecoder(response.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Tier != string(domain.RankTierGold) || body.Provisional || body.RankedGames != 10 || body.SeasonID != "season-current" {
if body.Tier != string(domain.RankTierGold) || body.Provisional || body.RankedGames != 10 || body.SeasonID != "season-current" || body.SeasonEndsAt != "1970-01-03T00:16:40Z" {
t.Fatalf("ranked profile response = %+v", body)
}
}
+5 -4
View File
@@ -60,10 +60,11 @@ func ScoreForPlayer(outcome MatchOutcome, playerID string, team int) (float64, e
type RankedProfile struct {
Rating
RankedGames int
CurrentSeasonID string
LastSeasonID string
SeasonHistory []string
RankedGames int
CurrentSeasonID string
CurrentSeasonEndsAt time.Time
LastSeasonID string
SeasonHistory []string
}
type RankTier string
+1 -1
View File
@@ -1290,7 +1290,7 @@ func TestPostgreSQLRankedProfileProjectsActiveSeason(t *testing.T) {
t.Fatal(err)
}
profile, found, err := (PostgresRankedProfiles{DB: db}).Get(ctx, "profile-season-player")
if err != nil || !found || profile.CurrentSeasonID != "profile-season-current" {
if err != nil || !found || profile.CurrentSeasonID != "profile-season-current" || !profile.CurrentSeasonEndsAt.Equal(now.Add(time.Hour)) {
t.Fatalf("profile=%+v found=%t err=%v", profile, found, err)
}
}
+5 -2
View File
@@ -11,7 +11,10 @@ import (
const RankedProfileSelectSQL = `SELECT rating, deviation, volatility, ranked_games, updated_at,
COALESCE((SELECT season_id FROM seasons
WHERE playlist = 'ranked' AND starts_at <= CURRENT_TIMESTAMP AND ends_at > CURRENT_TIMESTAMP
ORDER BY starts_at DESC, season_id DESC LIMIT 1), '')
ORDER BY starts_at DESC, season_id DESC LIMIT 1), ''),
COALESCE((SELECT ends_at FROM seasons
WHERE playlist = 'ranked' AND starts_at <= CURRENT_TIMESTAMP AND ends_at > CURRENT_TIMESTAMP
ORDER BY starts_at DESC, season_id DESC LIMIT 1), TIMESTAMP 'epoch')
FROM ratings
WHERE player_id = $1`
@@ -34,7 +37,7 @@ func (p PostgresRankedProfiles) Get(ctx context.Context, playerID string) (domai
}
var profile domain.RankedProfile
err := p.DB.QueryRowContext(ctx, RankedProfileSelectSQL, playerID).
Scan(&profile.Value, &profile.RD, &profile.Volatility, &profile.RankedGames, &profile.LastRatedAt, &profile.CurrentSeasonID)
Scan(&profile.Value, &profile.RD, &profile.Volatility, &profile.RankedGames, &profile.LastRatedAt, &profile.CurrentSeasonID, &profile.CurrentSeasonEndsAt)
if err == sql.ErrNoRows {
return domain.RankedProfile{}, false, nil
}
+1 -1
View File
@@ -3,7 +3,7 @@ package store
import "testing"
func TestRankedProfileQueryProjectsOnlyTheActiveRankedSeason(t *testing.T) {
for _, fragment := range []string{"playlist = 'ranked'", "starts_at <= CURRENT_TIMESTAMP", "ends_at > CURRENT_TIMESTAMP", "ORDER BY starts_at DESC", "LIMIT 1"} {
for _, fragment := range []string{"playlist = 'ranked'", "starts_at <= CURRENT_TIMESTAMP", "ends_at > CURRENT_TIMESTAMP", "ORDER BY starts_at DESC", "LIMIT 1", "TIMESTAMP 'epoch'"} {
if !contains(RankedProfileSelectSQL, fragment) {
t.Fatalf("ranked profile query missing %q", fragment)
}