ai-code-battle/cmd/acb-index-builder/generator.go
jedarden be3843d9ac fix(index-builder): correct series/season exempt queries, optimize playlist curation
- Fix deploy.go to query actual table names (series_games not series_matches,
  join through series_games for seasons instead of non-existent season_matches)
- Add playlist_matches table to exempt match IDs from R2 pruning
- Pre-build lookup maps for O(1) playlist match filtering instead of O(n²)
- Enhance home page featured replay to prefer AI-enriched matches
- Add enrichment test coverage (shouldEnrich criteria validation)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 16:47:13 -04:00

1288 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// LeaderboardIndex represents the leaderboard.json structure
type LeaderboardIndex struct {
UpdatedAt string `json:"updated_at"`
Entries []LeaderboardEntry `json:"entries"`
}
// LeaderboardEntry represents a single bot on the leaderboard
type LeaderboardEntry struct {
Rank int `json:"rank"`
BotID string `json:"bot_id"`
Name string `json:"name"`
OwnerID string `json:"owner_id"`
Rating int `json:"rating"`
RatingDeviation float64 `json:"rating_deviation"`
MatchesPlayed int `json:"matches_played"`
MatchesWon int `json:"matches_won"`
WinRate float64 `json:"win_rate"`
HealthStatus string `json:"health_status"`
}
// BotDirectory represents bots/index.json
type BotDirectory struct {
UpdatedAt string `json:"updated_at"`
Bots []BotDirectoryEntry `json:"bots"`
}
// BotDirectoryEntry represents a bot in the directory
type BotDirectoryEntry struct {
ID string `json:"id"`
Name string `json:"name"`
Rating int `json:"rating"`
MatchesPlayed int `json:"matches_played"`
WinRate float64 `json:"win_rate"`
}
// BotProfile represents data/bots/{bot_id}.json
type BotProfile struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerID string `json:"owner_id"`
Description string `json:"description,omitempty"`
Rating int `json:"rating"`
RatingDeviation float64 `json:"rating_deviation"`
RatingVolatility float64 `json:"rating_volatility"`
MatchesPlayed int `json:"matches_played"`
MatchesWon int `json:"matches_won"`
WinRate float64 `json:"win_rate"`
HealthStatus string `json:"health_status"`
Evolved bool `json:"evolved"`
Island string `json:"island,omitempty"`
Generation int `json:"generation,omitempty"`
CreatedAt string `json:"created_at"`
RatingHistory []RatingHistoryEntry `json:"rating_history"`
RecentMatches []MatchSummary `json:"recent_matches"`
}
// MatchSummary represents a match in listings
type MatchSummary struct {
ID string `json:"id"`
CompletedAt string `json:"completed_at"`
Participants []MatchParticipantSummary `json:"participants"`
WinnerID string `json:"winner_id,omitempty"`
Turns int `json:"turns"`
EndReason string `json:"end_reason"`
}
// MatchParticipantSummary represents a bot in a match summary
type MatchParticipantSummary struct {
BotID string `json:"bot_id"`
Name string `json:"name"`
Score int `json:"score"`
Won bool `json:"won"`
}
// MatchIndex represents matches/index.json
type MatchIndex struct {
UpdatedAt string `json:"updated_at"`
Matches []MatchSummary `json:"matches"`
}
// generateAllIndexes creates all JSON index files
func generateAllIndexes(data *IndexData, outputDir string, db *sql.DB) error {
botNameMap := make(map[string]string)
for _, bot := range data.Bots {
botNameMap[bot.ID] = bot.Name
}
// Generate leaderboard.json
if err := generateLeaderboard(data, outputDir); err != nil {
return fmt.Errorf("leaderboard: %w", err)
}
// Generate bots/index.json
if err := generateBotDirectory(data, outputDir); err != nil {
return fmt.Errorf("bot directory: %w", err)
}
// Generate individual bot profiles
if err := generateBotProfiles(data, outputDir); err != nil {
return fmt.Errorf("bot profiles: %w", err)
}
// Generate matches/index.json
if err := generateMatchIndex(data, outputDir, botNameMap); err != nil {
return fmt.Errorf("match index: %w", err)
}
// Generate series/index.json
if err := generateSeriesIndex(data, outputDir); err != nil {
return fmt.Errorf("series index: %w", err)
}
// Generate seasons/index.json
if err := generateSeasonsIndex(data, outputDir); err != nil {
return fmt.Errorf("seasons index: %w", err)
}
// Generate predictions/leaderboard.json
if err := generatePredictionsIndex(data, outputDir); err != nil {
return fmt.Errorf("predictions index: %w", err)
}
// Generate playlists
if err := generatePlaylists(data, outputDir, botNameMap); err != nil {
return fmt.Errorf("playlists: %w", err)
}
// Persist playlists to DB for incremental queries and R2 pruning exemptions
if db != nil {
if err := persistGeneratedPlaylists(context.Background(), db, outputDir); err != nil {
// Non-fatal: playlists are still written as JSON files
fmt.Fprintf(os.Stderr, "persist playlists to DB: %v\n", err)
}
}
return nil
}
func generateLeaderboard(data *IndexData, outputDir string) error {
entries := make([]LeaderboardEntry, 0, len(data.Bots))
for i, bot := range data.Bots {
if bot.MatchesPlayed == 0 {
continue
}
winRate := 0.0
if bot.MatchesPlayed > 0 {
winRate = float64(bot.MatchesWon) / float64(bot.MatchesPlayed) * 100
}
entries = append(entries, LeaderboardEntry{
Rank: i + 1,
BotID: bot.ID,
Name: bot.Name,
OwnerID: bot.OwnerID,
Rating: int(bot.Rating),
RatingDeviation: bot.RatingDeviation,
MatchesPlayed: bot.MatchesPlayed,
MatchesWon: bot.MatchesWon,
WinRate: round1(winRate),
HealthStatus: bot.HealthStatus,
})
}
leaderboard := LeaderboardIndex{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Entries: entries,
}
return writeJSON(filepath.Join(outputDir, "data", "leaderboard.json"), leaderboard)
}
func generateBotDirectory(data *IndexData, outputDir string) error {
entries := make([]BotDirectoryEntry, 0, len(data.Bots))
for _, bot := range data.Bots {
winRate := 0.0
if bot.MatchesPlayed > 0 {
winRate = float64(bot.MatchesWon) / float64(bot.MatchesPlayed) * 100
}
entries = append(entries, BotDirectoryEntry{
ID: bot.ID,
Name: bot.Name,
Rating: int(bot.Rating),
MatchesPlayed: bot.MatchesPlayed,
WinRate: round1(winRate),
})
}
dir := BotDirectory{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Bots: entries,
}
return writeJSON(filepath.Join(outputDir, "data", "bots", "index.json"), dir)
}
func generateBotProfiles(data *IndexData, outputDir string) error {
botsDir := filepath.Join(outputDir, "data", "bots")
for _, bot := range data.Bots {
winRate := 0.0
if bot.MatchesPlayed > 0 {
winRate = float64(bot.MatchesWon) / float64(bot.MatchesPlayed) * 100
}
// Get rating history for this bot
history := make([]RatingHistoryEntry, 0)
for _, h := range data.RatingHistory {
if h.BotID == bot.ID {
history = append(history, h)
}
}
// Get recent matches for this bot (last 20)
recentMatches := make([]MatchSummary, 0)
for _, m := range data.Matches {
participated := false
for _, p := range m.Participants {
if p.BotID == bot.ID {
participated = true
break
}
}
if participated {
summary := matchToSummary(m, data)
recentMatches = append(recentMatches, summary)
if len(recentMatches) >= 20 {
break
}
}
}
profile := BotProfile{
ID: bot.ID,
Name: bot.Name,
OwnerID: bot.OwnerID,
Description: bot.Description,
Rating: int(bot.Rating),
RatingDeviation: bot.RatingDeviation,
RatingVolatility: bot.RatingVolatility,
MatchesPlayed: bot.MatchesPlayed,
MatchesWon: bot.MatchesWon,
WinRate: round1(winRate),
HealthStatus: bot.HealthStatus,
Evolved: bot.Evolved,
Island: bot.Island,
Generation: bot.Generation,
CreatedAt: bot.CreatedAt.Format(time.RFC3339),
RatingHistory: history,
RecentMatches: recentMatches,
}
if err := writeJSON(filepath.Join(botsDir, bot.ID+".json"), profile); err != nil {
return err
}
}
return nil
}
func generateMatchIndex(data *IndexData, outputDir string, botNameMap map[string]string) error {
summaries := make([]MatchSummary, 0, len(data.Matches))
for _, m := range data.Matches {
summaries = append(summaries, matchToSummary(m, data))
}
index := MatchIndex{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Matches: summaries,
}
return writeJSON(filepath.Join(outputDir, "data", "matches", "index.json"), index)
}
func matchToSummary(m MatchData, data *IndexData) MatchSummary {
participants := make([]MatchParticipantSummary, 0, len(m.Participants))
for _, p := range m.Participants {
name := "Unknown"
for _, bot := range data.Bots {
if bot.ID == p.BotID {
name = bot.Name
break
}
}
participants = append(participants, MatchParticipantSummary{
BotID: p.BotID,
Name: name,
Score: p.Score,
Won: p.BotID == m.WinnerID,
})
}
return MatchSummary{
ID: m.ID,
CompletedAt: m.CompletedAt.Format(time.RFC3339),
Participants: participants,
WinnerID: m.WinnerID,
Turns: m.TurnCount,
EndReason: m.EndCondition,
}
}
func generateSeriesIndex(data *IndexData, outputDir string) error {
seriesDir := filepath.Join(outputDir, "data", "series")
for _, s := range data.Series {
if err := writeJSON(filepath.Join(seriesDir, fmt.Sprintf("%d.json", s.ID)), s); err != nil {
return err
}
}
type SeriesIndex struct {
UpdatedAt string `json:"updated_at"`
Series []SeriesData `json:"series"`
}
index := SeriesIndex{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Series: data.Series,
}
return writeJSON(filepath.Join(seriesDir, "index.json"), index)
}
func generateSeasonsIndex(data *IndexData, outputDir string) error {
seasonsDir := filepath.Join(outputDir, "data", "seasons")
for _, s := range data.Seasons {
if err := writeJSON(filepath.Join(seasonsDir, fmt.Sprintf("%d.json", s.ID)), s); err != nil {
return err
}
}
var activeSeason *SeasonData
for i := range data.Seasons {
if data.Seasons[i].Status == "active" {
activeSeason = &data.Seasons[i]
break
}
}
type SeasonsIndex struct {
UpdatedAt string `json:"updated_at"`
ActiveSeason *SeasonData `json:"active_season"`
Seasons []SeasonData `json:"seasons"`
}
index := SeasonsIndex{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
ActiveSeason: activeSeason,
Seasons: data.Seasons,
}
return writeJSON(filepath.Join(seasonsDir, "index.json"), index)
}
func generatePredictionsIndex(data *IndexData, outputDir string) error {
type PredictionsLeaderboard struct {
UpdatedAt string `json:"updated_at"`
Entries []PredictorStats `json:"entries"`
}
index := PredictionsLeaderboard{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Entries: data.TopPredictors,
}
return writeJSON(filepath.Join(outputDir, "data", "predictions", "leaderboard.json"), index)
}
func generatePlaylists(data *IndexData, outputDir string, botNameMap map[string]string) error {
playlistsDir := filepath.Join(outputDir, "data", "playlists")
// Pre-build lookup maps for O(1) playlist curation instead of O(n^2) per match.
firstMatchPerBot := buildFirstMatchPerBot(data.Matches)
pairFrequency := buildPairFrequency(data.Matches)
type playlistDef struct {
slug string
title string
description string
category string
filter func(MatchData) bool
sort func([]MatchData)
}
defs := []playlistDef{
{
slug: "closest-finishes",
title: "Closest Finishes",
description: "Matches decided by the thinnest margins — nail-biters to the very end",
category: "close_games",
filter: func(m MatchData) bool {
if len(m.Participants) < 2 || m.WinnerID == "" {
return false
}
return minScoreDiff(m) <= 2
},
sort: func(matches []MatchData) {
sortByScoreDiff(matches)
},
},
{
slug: "biggest-upsets",
title: "Biggest Upsets",
description: "Lower-rated bots triumph against higher-rated opponents",
category: "upsets",
filter: func(m MatchData) bool {
if m.WinnerID == "" || len(m.Participants) < 2 {
return false
}
return ratingUpsetMagnitude(m) >= 100
},
sort: func(matches []MatchData) {
sortByUpsetMagnitude(matches)
},
},
{
slug: "best-comebacks",
title: "Best Comebacks",
description: "Bots that were down but never out — dramatic turnarounds and improbable victories",
category: "comebacks",
filter: func(m MatchData) bool {
return isComeback(m)
},
sort: func(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return turnaroundMagnitude(matches[i]) > turnaroundMagnitude(matches[j])
})
},
},
{
slug: "marathon-matches",
title: "Marathon Matches",
description: "The longest, most grueling matches — endurance-tested battles",
category: "long_games",
filter: func(m MatchData) bool {
return m.TurnCount >= 300
},
sort: func(matches []MatchData) {
sortByTurnCount(matches)
},
},
{
slug: "highest-rated",
title: "Clash of Titans",
description: "Matches between the highest-rated opponents on the ladder",
category: "featured",
filter: func(m MatchData) bool {
if len(m.Participants) < 2 {
return false
}
return combinedRating(m) >= 3200
},
sort: func(matches []MatchData) {
sortByCombinedRating(matches)
},
},
{
slug: "evolution-breakthroughs",
title: "Evolution Breakthroughs",
description: "Evolved bots defeating top-rated opponents — AI strategy milestones",
category: "featured",
filter: func(m MatchData) bool {
return isEvolutionBreakthrough(m, data)
},
sort: func(matches []MatchData) {
sortByUpsetMagnitude(matches)
},
},
{
slug: "rivalry-classics",
title: "Rivalry Classics",
description: "The most closely contested matchups between frequent opponents",
category: "rivalry",
filter: func(m MatchData) bool {
return isRivalryMatchFast(m, pairFrequency)
},
sort: func(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return minScoreDiff(matches[i]) < minScoreDiff(matches[j])
})
},
},
{
slug: "domination",
title: "Total Domination",
description: "One-sided victories where the winner crushed all opposition",
category: "domination",
filter: func(m MatchData) bool {
if m.WinnerID == "" || len(m.Participants) < 2 {
return false
}
return maxScoreDiff(m) >= 5
},
sort: func(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return maxScoreDiff(matches[i]) > maxScoreDiff(matches[j])
})
},
},
{
slug: "new-bot-debuts",
title: "New Bot Debuts",
description: "First matches of newly registered bots — watch their opening games",
category: "tutorial",
filter: func(m MatchData) bool {
return isNewBotDebutFast(m, firstMatchPerBot)
},
sort: func(matches []MatchData) {
// Newest debuts first
sortSlice(matches, func(i, j int) bool {
return matches[i].CompletedAt.After(matches[j].CompletedAt)
})
},
},
{
slug: "season-highlights",
title: "Season Highlights",
description: "Top matches from the current season ranked by excitement",
category: "season",
filter: func(m MatchData) bool {
return isCurrentSeasonMatch(m, data)
},
sort: func(matches []MatchData) {
sortByInterestScore(matches)
},
},
{
slug: "featured",
title: "Featured Matches",
description: "Recent highlights from the ladder",
category: "featured",
filter: func(m MatchData) bool {
return m.WinnerID != ""
},
sort: func(matches []MatchData) {
// Most recent first (already sorted by completed_at DESC from DB)
},
},
{
slug: "best-of-week",
title: "Best of the Week",
description: "This week's top matches ranked by excitement: close finishes, upsets, marathon battles, and elite clashes",
category: "weekly",
filter: func(m MatchData) bool {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
return m.CompletedAt.After(weekAgo) && m.WinnerID != ""
},
sort: func(matches []MatchData) {
sortByInterestScore(matches)
},
},
}
var summaries []PlaylistSummary
for _, def := range defs {
// Special handling for best-of-week: use curated selection with tags
if def.slug == "best-of-week" {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
curated := curateWeeklyHighlights(data.Matches, weekAgo)
curatedMatches := make([]MatchData, 0, len(curated))
tags := make(map[string]string, len(curated))
for _, c := range curated {
curatedMatches = append(curatedMatches, c.Match)
tags[c.Match.ID] = c.Tag
}
if err := writePlaylistWithTags(playlistsDir, def.slug+".json", def.title, def.description, def.category, curatedMatches, tags, data); err != nil {
return err
}
var thumbMatchID string
if len(curatedMatches) > 0 {
thumbMatchID = curatedMatches[0].ID
}
summaries = append(summaries, PlaylistSummary{
Slug: def.slug,
Title: def.title,
Description: def.description,
Category: def.category,
MatchCount: len(curatedMatches),
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
ThumbnailMatchID: thumbMatchID,
})
continue
}
filtered := filterMatches(data.Matches, def.filter)
if def.sort != nil {
def.sort(filtered)
}
filtered = filtered[:min(20, len(filtered))]
if err := writePlaylist(playlistsDir, def.slug+".json", def.title, def.description, def.category, filtered, data); err != nil {
return err
}
var thumbMatchID string
if len(filtered) > 0 {
thumbMatchID = filtered[0].ID
}
summaries = append(summaries, PlaylistSummary{
Slug: def.slug,
Title: def.title,
Description: def.description,
Category: def.category,
MatchCount: len(filtered),
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
ThumbnailMatchID: thumbMatchID,
})
}
index := PlaylistIndex{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Playlists: summaries,
}
return writeJSON(filepath.Join(playlistsDir, "index.json"), index)
}
type PlaylistIndex struct {
UpdatedAt string `json:"updated_at"`
Playlists []PlaylistSummary `json:"playlists"`
}
type PlaylistSummary struct {
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
MatchCount int `json:"match_count"`
UpdatedAt string `json:"updated_at"`
ThumbnailMatchID string `json:"thumbnail_match_id,omitempty"`
}
type Playlist struct {
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
MatchCount int `json:"match_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Matches []PlaylistMatch `json:"matches"`
}
type PlaylistMatch struct {
MatchID string `json:"match_id"`
Order int `json:"order"`
Title string `json:"title,omitempty"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
CurationTag string `json:"curation_tag,omitempty"`
Participants []MatchParticipantSummary `json:"participants,omitempty"`
Score string `json:"score,omitempty"`
Turns int `json:"turns,omitempty"`
EndReason string `json:"end_reason,omitempty"`
CompletedAt string `json:"completed_at,omitempty"`
}
// curatedWeeklyMatch is a match selected by the weekly curation algorithm
// with a tag explaining why it was selected.
type curatedWeeklyMatch struct {
Match MatchData
Tag string
}
// curateWeeklyHighlights selects the best matches from the past 7 days
// using explicit criteria: upsets, elite clashes, marathon battles, closest finishes.
// It processes specific criteria first so distinctive matches aren't consumed
// by generic tags. It returns deduplicated matches tagged with their selection reason.
func curateWeeklyHighlights(matches []MatchData, cutoff time.Time) []curatedWeeklyMatch {
seen := make(map[string]string) // match_id -> tag (first selection reason)
maxPerCriterion := 7
recent := filterMatches(matches, func(m MatchData) bool {
return m.CompletedAt.After(cutoff) && m.WinnerID != "" && len(m.Participants) >= 2
})
// 1. Biggest upsets first (most distinctive — underdog victories)
upsetMatches := make([]MatchData, len(recent))
copy(upsetMatches, recent)
sortByUpsetMagnitude(upsetMatches)
for i, m := range upsetMatches {
if i >= maxPerCriterion {
break
}
mag := ratingUpsetMagnitude(m)
if mag < 50 {
continue
}
if _, exists := seen[m.ID]; !exists {
seen[m.ID] = fmt.Sprintf("Upset victory (underdog by %d rating)", mag)
}
}
// 2. Highest-rated opponents (elite clashes)
ratedMatches := make([]MatchData, len(recent))
copy(ratedMatches, recent)
sortByCombinedRating(ratedMatches)
for i, m := range ratedMatches {
if i >= maxPerCriterion {
break
}
cr := int(combinedRating(m))
if cr < 3000 {
continue
}
if _, exists := seen[m.ID]; !exists {
seen[m.ID] = fmt.Sprintf("Elite clash (combined rating: %d)", cr)
}
}
// 3. Most turns (longest endurance battles)
longMatches := make([]MatchData, len(recent))
copy(longMatches, recent)
sortByTurnCount(longMatches)
for i, m := range longMatches {
if i >= maxPerCriterion {
break
}
if m.TurnCount < 300 {
continue
}
if _, exists := seen[m.ID]; !exists {
seen[m.ID] = fmt.Sprintf("Marathon battle (%d turns)", m.TurnCount)
}
}
// 4. Closest results last (most generic — fills remaining slots)
closeMatches := make([]MatchData, len(recent))
copy(closeMatches, recent)
sortByScoreDiff(closeMatches)
for i, m := range closeMatches {
if i >= maxPerCriterion {
break
}
diff := minScoreDiff(m)
if _, exists := seen[m.ID]; !exists {
seen[m.ID] = fmt.Sprintf("Closest finish (score diff: %d)", diff)
}
}
// Build result in criterion order: upsets, elite, marathon, closest
var result []curatedWeeklyMatch
for _, m := range recent {
if tag, ok := seen[m.ID]; ok {
result = append(result, curatedWeeklyMatch{Match: m, Tag: tag})
}
}
if len(result) > 20 {
result = result[:20]
}
return result
}
func writePlaylist(dir, filename, title, description, category string, matches []MatchData, data *IndexData) error {
slug := filename[:len(filename)-5]
pm := make([]PlaylistMatch, 0, len(matches))
for i, m := range matches {
pm = append(pm, buildPlaylistMatch(m, i, data, ""))
}
playlist := Playlist{
Slug: slug,
Title: title,
Description: description,
Category: category,
MatchCount: len(pm),
CreatedAt: data.GeneratedAt.Format(time.RFC3339),
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Matches: pm,
}
return writeJSON(filepath.Join(dir, filename), playlist)
}
func writePlaylistWithTags(dir, filename, title, description, category string, matches []MatchData, tags map[string]string, data *IndexData) error {
slug := filename[:len(filename)-5]
pm := make([]PlaylistMatch, 0, len(matches))
for i, m := range matches {
pm = append(pm, buildPlaylistMatch(m, i, data, tags[m.ID]))
}
playlist := Playlist{
Slug: slug,
Title: title,
Description: description,
Category: category,
MatchCount: len(pm),
CreatedAt: data.GeneratedAt.Format(time.RFC3339),
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Matches: pm,
}
return writeJSON(filepath.Join(dir, filename), playlist)
}
func formatMatchTitle(m MatchData, data *IndexData) string {
names := make([]string, 0, len(m.Participants))
scores := make([]int, 0, len(m.Participants))
for _, p := range m.Participants {
name := "Unknown"
for _, bot := range data.Bots {
if bot.ID == p.BotID {
name = bot.Name
break
}
}
names = append(names, name)
scores = append(scores, p.Score)
}
if len(names) == 2 {
return fmt.Sprintf("%s %d %d %s", names[0], scores[0], scores[1], names[1])
}
return fmt.Sprintf("%s (%d players)", m.ID[:min(8, len(m.ID))], len(names))
}
func buildPlaylistMatch(m MatchData, order int, data *IndexData, curationTag string) PlaylistMatch {
participants := make([]MatchParticipantSummary, 0, len(m.Participants))
scoreParts := make([]string, 0, len(m.Participants))
for _, p := range m.Participants {
name := "Unknown"
for _, bot := range data.Bots {
if bot.ID == p.BotID {
name = bot.Name
break
}
}
participants = append(participants, MatchParticipantSummary{
BotID: p.BotID,
Name: name,
Score: p.Score,
Won: p.BotID == m.WinnerID,
})
scoreParts = append(scoreParts, fmt.Sprintf("%d", p.Score))
}
title := formatMatchTitle(m, data)
completedAt := ""
if !m.CompletedAt.IsZero() {
completedAt = m.CompletedAt.Format(time.RFC3339)
}
return PlaylistMatch{
MatchID: m.ID,
Order: order,
Title: title,
CurationTag: curationTag,
Participants: participants,
Score: strings.Join(scoreParts, "-"),
Turns: m.TurnCount,
EndReason: m.EndCondition,
CompletedAt: completedAt,
}
}
func ratingUpsetMagnitude(m MatchData) int {
if m.WinnerID == "" || len(m.Participants) < 2 {
return 0
}
var winnerRating, bestLoserRating float64
found := false
for _, p := range m.Participants {
if p.BotID == m.WinnerID {
winnerRating = p.PreMatchRating
found = true
}
}
if !found || winnerRating == 0 {
return 0
}
for _, p := range m.Participants {
if p.BotID != m.WinnerID && p.PreMatchRating > bestLoserRating {
bestLoserRating = p.PreMatchRating
}
}
if bestLoserRating == 0 {
return 0
}
return int(bestLoserRating - winnerRating)
}
func combinedRating(m MatchData) float64 {
total := 0.0
for _, p := range m.Participants {
total += p.PreMatchRating
}
return total
}
func interestScore(m MatchData) float64 {
score := 0.0
// Close finishes are interesting
if len(m.Participants) >= 2 {
minDiff := 999
for i, p1 := range m.Participants {
for _, p2 := range m.Participants[i+1:] {
diff := abs(p1.Score - p2.Score)
if diff < minDiff {
minDiff = diff
}
}
}
if minDiff <= 1 {
score += 3.0
} else if minDiff <= 2 {
score += 2.0
}
}
// Upsets are interesting
upset := ratingUpsetMagnitude(m)
if upset >= 200 {
score += 4.0
} else if upset >= 100 {
score += 2.0
}
// Long matches are interesting
if m.TurnCount >= 400 {
score += 2.0
} else if m.TurnCount >= 300 {
score += 1.0
}
// High-rated opponents
cr := combinedRating(m)
if cr >= 3400 {
score += 2.0
} else if cr >= 3200 {
score += 1.0
}
return score
}
func sortByScoreDiff(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return minScoreDiff(matches[i]) < minScoreDiff(matches[j])
})
}
func sortByUpsetMagnitude(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return ratingUpsetMagnitude(matches[i]) > ratingUpsetMagnitude(matches[j])
})
}
func sortByTurnCount(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return matches[i].TurnCount > matches[j].TurnCount
})
}
func sortByCombinedRating(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return combinedRating(matches[i]) > combinedRating(matches[j])
})
}
func sortByInterestScore(matches []MatchData) {
sortSlice(matches, func(i, j int) bool {
return interestScore(matches[i]) > interestScore(matches[j])
})
}
func minScoreDiff(m MatchData) int {
minDiff := 999
for i, p1 := range m.Participants {
for _, p2 := range m.Participants[i+1:] {
diff := abs(p1.Score - p2.Score)
if diff < minDiff {
minDiff = diff
}
}
}
return minDiff
}
func sortSlice(s []MatchData, less func(i, j int) bool) {
for i := 0; i < len(s)-1; i++ {
for j := i + 1; j < len(s); j++ {
if less(j, i) {
s[i], s[j] = s[j], s[i]
}
}
}
}
func filterMatches(matches []MatchData, pred func(MatchData) bool) []MatchData {
result := make([]MatchData, 0)
for _, m := range matches {
if pred(m) {
result = append(result, m)
}
}
return result
}
// maxScoreDiff returns the maximum score difference between winner and any loser
func maxScoreDiff(m MatchData) int {
if m.WinnerID == "" || len(m.Participants) < 2 {
return 0
}
var winnerScore int
for _, p := range m.Participants {
if p.BotID == m.WinnerID {
winnerScore = p.Score
break
}
}
maxDiff := 0
for _, p := range m.Participants {
if p.BotID != m.WinnerID {
diff := winnerScore - p.Score
if diff > maxDiff {
maxDiff = diff
}
}
}
return maxDiff
}
// isNewBotDebut detects the first match of each bot by finding the earliest
// completed match for each bot.
func isNewBotDebut(m MatchData, data *IndexData) bool {
if m.WinnerID == "" {
return false
}
for _, p := range m.Participants {
earliest := true
for _, other := range data.Matches {
if other.ID == m.ID || other.CompletedAt.IsZero() {
continue
}
for _, op := range other.Participants {
if op.BotID == p.BotID {
if other.CompletedAt.Before(m.CompletedAt) {
earliest = false
}
}
}
}
if earliest {
return true
}
}
return false
}
// isCurrentSeasonMatch checks if a match belongs to the current active season.
func isCurrentSeasonMatch(m MatchData, data *IndexData) bool {
for _, s := range data.Seasons {
if s.Status != "active" {
continue
}
// Check if match falls within season date range
if m.CompletedAt.After(s.StartsAt) || m.CompletedAt.Equal(s.StartsAt) {
if s.EndsAt.IsZero() || m.CompletedAt.Before(s.EndsAt) {
return m.WinnerID != ""
}
}
}
return false
}
// isComeback detects matches where the winner was behind on score at some point
// but rallied to win. Uses a heuristic: winner scored more than loser despite
// having a lower pre-match rating (unlikely comeback) or the match had many turns
// (late-game rally) with a close final score.
func isComeback(m MatchData) bool {
if m.WinnerID == "" || len(m.Participants) < 2 {
return false
}
// An upset with a close score is a comeback
upset := ratingUpsetMagnitude(m)
scoreDiff := minScoreDiff(m)
return upset >= 80 && scoreDiff <= 3
}
// turnaroundMagnitude measures how dramatic a comeback was.
// Higher = more surprising turnaround.
func turnaroundMagnitude(m MatchData) float64 {
upset := float64(ratingUpsetMagnitude(m))
closeFactor := 1.0 / float64(max(minScoreDiff(m), 1))
turnFactor := float64(m.TurnCount) / 500.0
return upset*closeFactor + turnFactor*50
}
// isEvolutionBreakthrough detects matches where an evolved bot beat a high-rated opponent.
func isEvolutionBreakthrough(m MatchData, data *IndexData) bool {
if m.WinnerID == "" || len(m.Participants) < 2 {
return false
}
winnerEvolved := false
for _, bot := range data.Bots {
if bot.ID == m.WinnerID && bot.Evolved {
winnerEvolved = true
}
}
if !winnerEvolved {
return false
}
// Winner must have beaten someone rated >= 1600
for _, p := range m.Participants {
if p.BotID != m.WinnerID && p.PreMatchRating >= 1600 && !p.Won {
return true
}
}
return false
}
// buildFirstMatchPerBot returns a map from botID to the matchID of their earliest
// completed match. O(n*p) where n=matches, p=avg participants.
func buildFirstMatchPerBot(matches []MatchData) map[string]string {
first := make(map[string]string)
firstTime := make(map[string]time.Time)
for _, m := range matches {
if m.CompletedAt.IsZero() || m.WinnerID == "" {
continue
}
for _, p := range m.Participants {
if t, ok := firstTime[p.BotID]; !ok || m.CompletedAt.Before(t) {
firstTime[p.BotID] = m.CompletedAt
first[p.BotID] = m.ID
}
}
}
return first
}
// isNewBotDebutFast checks if any participant's earliest completed match is this one,
// using a pre-built lookup map.
func isNewBotDebutFast(m MatchData, firstMatchPerBot map[string]string) bool {
if m.WinnerID == "" {
return false
}
for _, p := range m.Participants {
if firstMatchPerBot[p.BotID] == m.ID {
return true
}
}
return false
}
// buildPairFrequency returns a map from "botA:botB" (sorted) to the count of
// 2-player matches between them. O(n) where n=matches.
func buildPairFrequency(matches []MatchData) map[string]int {
freq := make(map[string]int)
for _, m := range matches {
if len(m.Participants) != 2 {
continue
}
a, b := m.Participants[0].BotID, m.Participants[1].BotID
if a > b {
a, b = b, a
}
freq[a+":"+b]++
}
return freq
}
// isRivalryMatchFast checks if a 2-player match is between frequent opponents,
// using a pre-built pair frequency map.
func isRivalryMatchFast(m MatchData, pairFrequency map[string]int) bool {
if len(m.Participants) != 2 || m.WinnerID == "" {
return false
}
a, b := m.Participants[0].BotID, m.Participants[1].BotID
if a > b {
a, b = b, a
}
return pairFrequency[a+":"+b] >= 3
}
// isRivalryMatch detects matches between bots that have played each other frequently.
// Builds a frequency map from all matches and checks if this pair qualifies.
func isRivalryMatch(m MatchData, data *IndexData) bool {
if len(m.Participants) != 2 || m.WinnerID == "" {
return false
}
a, b := m.Participants[0].BotID, m.Participants[1].BotID
if a > b {
a, b = b, a
}
pairKey := a + ":" + b
// Count occurrences of this pair across all matches
count := 0
for _, other := range data.Matches {
if len(other.Participants) != 2 {
continue
}
oa, ob := other.Participants[0].BotID, other.Participants[1].BotID
if oa > ob {
oa, ob = ob, oa
}
if oa+":"+ob == pairKey {
count++
}
}
return count >= 3
}
func writeJSON(path string, data interface{}) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(data)
}
// persistGeneratedPlaylists reads the generated playlist JSON files from the output
// directory and writes them to the playlists and playlist_matches DB tables.
func persistGeneratedPlaylists(ctx context.Context, db *sql.DB, outputDir string) error {
playlistsDir := filepath.Join(outputDir, "data", "playlists")
indexContent, err := os.ReadFile(filepath.Join(playlistsDir, "index.json"))
if err != nil {
return fmt.Errorf("read playlist index: %w", err)
}
var index PlaylistIndex
if err := json.Unmarshal(indexContent, &index); err != nil {
return fmt.Errorf("parse playlist index: %w", err)
}
var persisted []persistedPlaylist
for _, summary := range index.Playlists {
plContent, err := os.ReadFile(filepath.Join(playlistsDir, summary.Slug+".json"))
if err != nil {
continue // skip playlists without files
}
var pl Playlist
if err := json.Unmarshal(plContent, &pl); err != nil {
continue
}
matches := make([]persistedPlaylistMatch, 0, len(pl.Matches))
for _, m := range pl.Matches {
matches = append(matches, persistedPlaylistMatch{
MatchID: m.MatchID,
SortOrder: m.Order,
CurationTag: m.CurationTag,
})
}
persisted = append(persisted, persistedPlaylist{
Slug: pl.Slug,
Title: pl.Title,
Description: pl.Description,
Category: pl.Category,
Matches: matches,
})
}
return persistPlaylists(ctx, db, persisted)
}
func round1(v float64) float64 {
return float64(int(v*10+0.5)) / 10
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func min(a, b int) int {
if a < b {
return a
}
return b
}