ai-code-battle/cmd/acb-index-builder/db.go
jedarden f35477dd96 feat(evolution, web): add live match counter per plan §16.18
- Add matches_today and active_bots fields to LiveData Totals (evolver)
- Query matches table for COUNT(*) WHERE completed_at >= today
- Query bots table for COUNT(*) WHERE status = 'active'
- Add fields to index builder EvolutionMeta struct
- Update homepage to render "X matches today · Y bots active · Gen #Z evolving"
- Add CSS styling for .home-live-stats section

Closes: bf-4m8mo
2026-05-26 19:57:57 -04:00

1270 lines
37 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"time"
)
// BotData represents a bot for the index
type BotData struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerID string `json:"owner_id"`
Description string `json:"description,omitempty"`
Rating float64 `json:"rating"`
RatingDeviation float64 `json:"rating_deviation"`
RatingVolatility float64 `json:"rating_volatility"`
MatchesPlayed int `json:"matches_played"`
MatchesWon int `json:"matches_won"`
HealthStatus string `json:"health_status"`
Evolved bool `json:"evolved"`
Island string `json:"island,omitempty"`
Generation int `json:"generation,omitempty"`
Archetype string `json:"archetype,omitempty"`
ParentIDs []string `json:"parent_ids,omitempty"`
DebugPublic bool `json:"debug_public"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// MatchData represents a match for the index
type MatchData struct {
ID string `json:"id"`
MapID string `json:"map_id"`
MapName string `json:"map_name,omitempty"`
WinnerID string `json:"winner_id,omitempty"`
TurnCount int `json:"turn_count"`
EndCondition string `json:"end_condition"`
CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death
Participants []ParticipantData `json:"participants"`
CreatedAt time.Time `json:"created_at"`
CompletedAt time.Time `json:"completed_at"`
PlayedAt time.Time `json:"played_at"`
}
// ParticipantData represents a bot in a match with pre-match rating
type ParticipantData struct {
BotID string `json:"bot_id"`
PlayerSlot int `json:"player_slot"`
Score int `json:"score"`
Won bool `json:"won"`
PreMatchRating float64 `json:"pre_match_rating,omitempty"`
Evolved bool `json:"evolved,omitempty"`
}
// RatingHistoryEntry represents a rating history point
type RatingHistoryEntry struct {
BotID string `json:"bot_id"`
MatchID string `json:"match_id"`
Rating float64 `json:"rating"`
RecordedAt time.Time `json:"recorded_at"`
}
// SeriesGameData represents one game within a series
type SeriesGameData struct {
MatchID string `json:"match_id"`
GameNum int `json:"game_number"`
WinnerID string `json:"winner_id,omitempty"`
WinnerSlot *int `json:"winner_slot"`
Turns int `json:"turns,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
}
// SeriesData represents a series for the index, enriched with bot names and games.
type SeriesData struct {
ID int64 `json:"id"`
BotAID string `json:"bot1_id"`
BotBID string `json:"bot2_id"`
BotAName string `json:"bot1_name"`
BotBName string `json:"bot2_name"`
Format int `json:"best_of"`
AWins int `json:"bot1_wins"`
BWins int `json:"bot2_wins"`
Status string `json:"status"`
WinnerID string `json:"winner_id,omitempty"`
BracketRound string `json:"bracket_round,omitempty"`
BracketPosition int `json:"bracket_position,omitempty"`
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Games []SeriesGameData `json:"games"`
}
// SeasonSnapshotData represents a bot's end-of-season snapshot
type SeasonSnapshotData struct {
BotID string `json:"bot_id"`
BotName string `json:"bot_name"`
Rating float64 `json:"rating"`
Rank int `json:"rank"`
Wins int `json:"wins"`
Losses int `json:"losses"`
}
// ChampionshipSeries is a lightweight series summary for bracket display on the season page.
type ChampionshipSeries struct {
ID int64 `json:"id"`
BotAID string `json:"bot1_id"`
BotBID string `json:"bot2_id"`
BotAName string `json:"bot1_name"`
BotBName string `json:"bot2_name"`
Format int `json:"best_of"`
AWins int `json:"bot1_wins"`
BWins int `json:"bot2_wins"`
Status string `json:"status"`
WinnerID string `json:"winner_id,omitempty"`
Round string `json:"round"`
BracketPosition int `json:"bracket_position"`
Games []SeriesGameData `json:"games"`
}
// SeasonData represents a season for the index, enriched with champion name, match count, and snapshots.
type SeasonData struct {
ID int64 `json:"id"`
Name string `json:"name"`
Theme string `json:"theme,omitempty"`
RulesVer string `json:"rules_version"`
Status string `json:"status"`
ChampionID string `json:"champion_id,omitempty"`
ChampionName string `json:"champion_name,omitempty"`
StartsAt time.Time `json:"starts_at"`
EndsAt time.Time `json:"ends_at,omitempty"`
TotalMatches int `json:"total_matches"`
CreatedAt time.Time `json:"created_at"`
Snapshots []SeasonSnapshotData `json:"final_snapshot"`
ChampionshipBracket []ChampionshipSeries `json:"championship_bracket,omitempty"`
}
// PredictionData represents a prediction for the index
type PredictionData struct {
ID int64 `json:"id"`
MatchID string `json:"match_id"`
PredictorID string `json:"predictor_id"`
PredictedBot string `json:"predicted_bot"`
Correct *bool `json:"correct,omitempty"`
CreatedAt time.Time `json:"created_at"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
}
// PredictorStats represents predictor statistics
type PredictorStats struct {
PredictorID string `json:"predictor_id"`
Correct int `json:"correct"`
Incorrect int `json:"incorrect"`
Streak int `json:"streak"`
BestStreak int `json:"best_streak"`
}
// MapData represents a map for the index
type MapData struct {
MapID string `json:"map_id"`
PlayerCount int `json:"player_count"`
Status string `json:"status"`
Engagement float64 `json:"engagement"`
WallDensity float64 `json:"wall_density"`
EnergyCount int `json:"energy_count"`
GridWidth int `json:"grid_width"`
GridHeight int `json:"grid_height"`
NetVotes int `json:"net_votes"` // Sum of votes from map_votes table
CreatedAt time.Time `json:"created_at"`
RawJSON json.RawMessage `json:"-"`
}
// OpenPredictionMatch represents a pending match open for predictions
type OpenPredictionMatch struct {
MatchID string `json:"match_id"`
BotAID string `json:"bot_a"`
BotBID string `json:"bot_b"`
BotAName string `json:"bot_a_name"`
BotBName string `json:"bot_b_name"`
ARating float64 `json:"a_rating"`
BRating float64 `json:"b_rating"`
AEvolved bool `json:"a_evolved"`
BEvolved bool `json:"b_evolved"`
CreatedAt time.Time `json:"created_at"`
IsSeriesMatch bool `json:"is_series_match"`
HeadToHeadRecord *string `json:"head_to_head_record,omitempty"`
}
// FeedbackEntry represents a community replay annotation from §13.6.
type FeedbackEntry struct {
FeedbackID string `json:"feedback_id"`
MatchID string `json:"match_id"`
Turn int `json:"turn"`
Type string `json:"type"`
Body string `json:"body"`
Author string `json:"author"`
Upvotes int `json:"upvotes"`
CreatedAt time.Time `json:"created_at"`
}
// IndexData contains all data needed for index generation
type IndexData struct {
GeneratedAt time.Time
Bots []BotData
Matches []MatchData
RatingHistory []RatingHistoryEntry
Series []SeriesData
Seasons []SeasonData
Predictions []PredictionData
PredictorStats []PredictorStats
Maps []MapData
TopPredictors []PredictorStats
OpenPredictionMatches []OpenPredictionMatch
Feedback []FeedbackEntry
EvolutionMeta *EvolutionMeta
Lineage []LineageNode
}
// fetchAllData retrieves all data from PostgreSQL for index generation
func fetchAllData(ctx context.Context, db *sql.DB) (*IndexData, error) {
data := &IndexData{
GeneratedAt: time.Now().UTC(),
}
var err error
if data.Bots, err = fetchBots(ctx, db); err != nil {
return nil, err
}
if data.Matches, err = fetchMatches(ctx, db); err != nil {
return nil, err
}
if data.RatingHistory, err = fetchRatingHistory(ctx, db); err != nil {
return nil, err
}
if data.Series, err = fetchSeries(ctx, db); err != nil {
return nil, err
}
if data.Seasons, err = fetchSeasons(ctx, db); err != nil {
return nil, err
}
if data.Predictions, err = fetchPredictions(ctx, db); err != nil {
return nil, err
}
if data.PredictorStats, err = fetchPredictorStats(ctx, db); err != nil {
return nil, err
}
if data.Maps, err = fetchMaps(ctx, db); err != nil {
return nil, err
}
if data.OpenPredictionMatches, err = fetchOpenPredictions(ctx, db); err != nil {
return nil, err
}
if data.Feedback, err = fetchFeedback(ctx, db); err != nil {
return nil, err
}
// Evolution data (may be missing if evolver is not running)
data.EvolutionMeta, _ = fetchEvolutionMeta(ctx, db)
data.Lineage, _ = fetchLineage(ctx, db)
if data.EvolutionMeta != nil && data.EvolutionMeta.Generation > 0 {
slog.Info("Evolution system running",
"generation", data.EvolutionMeta.Generation,
"promoted_today", data.EvolutionMeta.PromotedToday,
"total_promoted", data.EvolutionMeta.TotalPromoted,
"islands", len(data.EvolutionMeta.IslandPopulations))
} else {
slog.Info("Evolution system not initialized or not running")
}
data.TopPredictors = computeTopPredictors(data.PredictorStats)
return data, nil
}
func fetchBots(ctx context.Context, db *sql.DB) ([]BotData, error) {
query := `
SELECT bot_id, name, owner, description,
rating_mu, rating_phi, rating_sigma,
0, 0, status,
evolved, island, generation,
COALESCE(archetype, ''), COALESCE(parent_ids, '[]'::jsonb),
debug_public,
created_at, COALESCE(last_active, created_at)
FROM bots
WHERE status != 'retired'
ORDER BY rating_mu DESC
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var bots []BotData
for rows.Next() {
var b BotData
var desc, island sql.NullString
var gen sql.NullInt64
var parentIDsJSON []byte
err := rows.Scan(
&b.ID, &b.Name, &b.OwnerID, &desc,
&b.Rating, &b.RatingDeviation, &b.RatingVolatility,
&b.MatchesPlayed, &b.MatchesWon, &b.HealthStatus,
&b.Evolved, &island, &gen,
&b.Archetype, &parentIDsJSON,
&b.DebugPublic,
&b.CreatedAt, &b.UpdatedAt,
)
if err != nil {
return nil, err
}
if desc.Valid {
b.Description = desc.String
}
if island.Valid {
b.Island = island.String
}
if gen.Valid {
b.Generation = int(gen.Int64)
}
if len(parentIDsJSON) > 0 {
json.Unmarshal(parentIDsJSON, &b.ParentIDs)
}
bots = append(bots, b)
}
for i := range bots {
mp, mw, err := getBotMatchStats(ctx, db, bots[i].ID)
if err != nil {
return nil, err
}
bots[i].MatchesPlayed = mp
bots[i].MatchesWon = mw
}
return bots, nil
}
func getBotMatchStats(ctx context.Context, db *sql.DB, botID string) (played, won int, err error) {
query := `
SELECT COUNT(*), COUNT(*) FILTER (WHERE mp.player_slot = m.winner)
FROM match_participants mp
JOIN matches m ON mp.match_id = m.match_id
WHERE mp.bot_id = $1 AND m.status = 'completed'
`
err = db.QueryRowContext(ctx, query, botID).Scan(&played, &won)
return
}
func fetchMatches(ctx context.Context, db *sql.DB) ([]MatchData, error) {
query := `
SELECT m.match_id, m.map_id, m.winner, m.turn_count, m.condition,
COALESCE(m.combat_turns, 0),
m.created_at, m.completed_at,
COALESCE(
json_agg(
json_build_object(
'bot_id', mp.bot_id,
'player_slot', mp.player_slot,
'score', mp.score,
'won', mp.player_slot = m.winner,
'pre_match_rating', COALESCE(
(SELECT rh.rating FROM rating_history rh
WHERE rh.bot_id = mp.bot_id AND rh.match_id = m.match_id
LIMIT 1), 0),
'evolved', COALESCE(
(SELECT b.evolved FROM bots b WHERE b.bot_id = mp.bot_id), false)
)
ORDER BY mp.player_slot
) FILTER (WHERE mp.bot_id IS NOT NULL),
'[]'::json
) as participants
FROM matches m
LEFT JOIN match_participants mp ON m.match_id = mp.match_id
WHERE m.status = 'completed'
GROUP BY m.match_id, m.map_id, m.winner, m.turn_count, m.condition,
m.combat_turns, m.created_at, m.completed_at
ORDER BY m.completed_at DESC
LIMIT 1000
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var matches []MatchData
for rows.Next() {
var m MatchData
var winnerID sql.NullString
var participantsJSON []byte
err := rows.Scan(
&m.ID, &m.MapID, &winnerID, &m.TurnCount, &m.EndCondition,
&m.CombatTurns,
&m.CreatedAt, &m.CompletedAt, &participantsJSON,
)
if err != nil {
return nil, err
}
if winnerID.Valid {
m.WinnerID = winnerID.String
}
if err := json.Unmarshal(participantsJSON, &m.Participants); err != nil {
return nil, err
}
// PlayedAt is used for weekly filtering in blog/stats generation.
// CompletedAt is the authoritative timestamp; fall back to CreatedAt.
if !m.CompletedAt.IsZero() {
m.PlayedAt = m.CompletedAt
} else {
m.PlayedAt = m.CreatedAt
}
matches = append(matches, m)
}
return matches, nil
}
func fetchRatingHistory(ctx context.Context, db *sql.DB) ([]RatingHistoryEntry, error) {
query := `
SELECT bot_id, match_id, rating, recorded_at
FROM rating_history
ORDER BY recorded_at DESC
LIMIT 10000
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []RatingHistoryEntry
for rows.Next() {
var e RatingHistoryEntry
if err := rows.Scan(&e.BotID, &e.MatchID, &e.Rating, &e.RecordedAt); err != nil {
return nil, err
}
entries = append(entries, e)
}
return entries, nil
}
func fetchSeries(ctx context.Context, db *sql.DB) ([]SeriesData, error) {
query := `
SELECT s.id, s.bot_a_id, s.bot_b_id,
ba.name, bb.name,
s.format, s.a_wins, s.b_wins, s.status, s.winner_id,
COALESCE(s.bracket_round, ''), COALESCE(s.bracket_position, 0),
s.created_at, s.updated_at
FROM series s
JOIN bots ba ON s.bot_a_id = ba.bot_id
JOIN bots bb ON s.bot_b_id = bb.bot_id
ORDER BY s.created_at DESC
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var series []SeriesData
for rows.Next() {
var s SeriesData
var winnerID sql.NullString
err := rows.Scan(
&s.ID, &s.BotAID, &s.BotBID,
&s.BotAName, &s.BotBName,
&s.Format, &s.AWins, &s.BWins, &s.Status, &winnerID,
&s.BracketRound, &s.BracketPosition,
&s.CreatedAt, &s.UpdatedAt,
)
if err != nil {
return nil, err
}
if winnerID.Valid {
s.WinnerID = winnerID.String
}
series = append(series, s)
}
for i := range series {
games, err := fetchSeriesGames(ctx, db, series[i].ID)
if err != nil {
return nil, err
}
series[i].Games = games
}
return series, nil
}
func fetchSeriesGames(ctx context.Context, db *sql.DB, seriesID int64) ([]SeriesGameData, error) {
rows, err := db.QueryContext(ctx, `
SELECT sg.match_id, sg.game_num, sg.winner_id,
COALESCE(m.turn_count, 0), m.completed_at,
CASE WHEN sg.winner_id IS NOT NULL THEN
(SELECT mp.player_slot FROM match_participants mp
WHERE mp.match_id = sg.match_id AND mp.bot_id = sg.winner_id)
END
FROM series_games sg
LEFT JOIN matches m ON sg.match_id = m.match_id
WHERE sg.series_id = $1
ORDER BY sg.game_num
`, seriesID)
if err != nil {
return nil, err
}
defer rows.Close()
var games []SeriesGameData
for rows.Next() {
var g SeriesGameData
var winnerID sql.NullString
var winnerSlot sql.NullInt64
var turns sql.NullInt64
var completedAt sql.NullTime
err := rows.Scan(&g.MatchID, &g.GameNum, &winnerID, &turns, &completedAt, &winnerSlot)
if err != nil {
return nil, err
}
if winnerID.Valid {
g.WinnerID = winnerID.String
}
if winnerSlot.Valid {
slot := int(winnerSlot.Int64)
g.WinnerSlot = &slot
}
if turns.Valid && turns.Int64 > 0 {
g.Turns = int(turns.Int64)
}
if completedAt.Valid {
g.CompletedAt = &completedAt.Time
}
games = append(games, g)
}
return games, nil
}
func fetchSeasons(ctx context.Context, db *sql.DB) ([]SeasonData, error) {
query := `
SELECT s.id, s.name, s.theme, s.rules_version, s.status,
s.champion_id, b.name,
s.starts_at, s.ends_at, s.created_at
FROM seasons s
LEFT JOIN bots b ON s.champion_id = b.bot_id
ORDER BY s.starts_at DESC
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var seasons []SeasonData
for rows.Next() {
var s SeasonData
var theme, championID, championName sql.NullString
var endsAt sql.NullTime
err := rows.Scan(
&s.ID, &s.Name, &theme, &s.RulesVer, &s.Status,
&championID, &championName,
&s.StartsAt, &endsAt, &s.CreatedAt,
)
if err != nil {
return nil, err
}
if theme.Valid {
s.Theme = theme.String
}
if championID.Valid {
s.ChampionID = championID.String
}
if championName.Valid {
s.ChampionName = championName.String
}
if endsAt.Valid {
s.EndsAt = endsAt.Time
}
seasons = append(seasons, s)
}
// Enrich each season with match count, snapshots, and championship bracket
for i := range seasons {
seasons[i].TotalMatches, _ = getSeasonMatchCount(ctx, db, seasons[i].ID)
snapshots, err := fetchSeasonSnapshots(ctx, db, seasons[i].ID)
if err == nil && len(snapshots) > 0 {
seasons[i].Snapshots = snapshots
}
bracket, err := fetchChampionshipBracket(ctx, db, seasons[i].ID)
if err == nil && len(bracket) > 0 {
seasons[i].ChampionshipBracket = bracket
}
}
return seasons, nil
}
func getSeasonMatchCount(ctx context.Context, db *sql.DB, seasonID int64) (int, error) {
// Count matches from series in this season
var count int
err := db.QueryRowContext(ctx, `
SELECT COUNT(DISTINCT sg.match_id)
FROM series_games sg
JOIN series s ON sg.series_id = s.id
WHERE s.season_id = $1 AND sg.match_id IS NOT NULL
`, seasonID).Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func fetchSeasonSnapshots(ctx context.Context, db *sql.DB, seasonID int64) ([]SeasonSnapshotData, error) {
rows, err := db.QueryContext(ctx, `
SELECT ss.bot_id, b.name, ss.rating, ss.rank, ss.wins, ss.losses
FROM season_snapshots ss
JOIN bots b ON ss.bot_id = b.bot_id
WHERE ss.season_id = $1
ORDER BY ss.rank
`, seasonID)
if err != nil {
return nil, err
}
defer rows.Close()
var snapshots []SeasonSnapshotData
for rows.Next() {
var snap SeasonSnapshotData
if err := rows.Scan(&snap.BotID, &snap.BotName, &snap.Rating, &snap.Rank, &snap.Wins, &snap.Losses); err != nil {
return nil, err
}
snapshots = append(snapshots, snap)
}
return snapshots, nil
}
func fetchChampionshipBracket(ctx context.Context, db *sql.DB, seasonID int64) ([]ChampionshipSeries, error) {
rows, err := db.QueryContext(ctx, `
SELECT s.id, s.bot_a_id, ba.name, s.bot_b_id, bb.name,
s.format, s.a_wins, s.b_wins, s.status, s.winner_id,
COALESCE(s.bracket_round, 'quarterfinal'), COALESCE(s.bracket_position, 0)
FROM series s
JOIN bots ba ON s.bot_a_id = ba.bot_id
JOIN bots bb ON s.bot_b_id = bb.bot_id
WHERE s.season_id = $1 AND s.bracket_round IS NOT NULL
ORDER BY
CASE s.bracket_round
WHEN 'quarterfinal' THEN 0
WHEN 'semifinal' THEN 1
WHEN 'final' THEN 2
END,
s.bracket_position
`, seasonID)
if err != nil {
return nil, err
}
defer rows.Close()
var result []ChampionshipSeries
for rows.Next() {
var cs ChampionshipSeries
var winnerID sql.NullString
if err := rows.Scan(&cs.ID, &cs.BotAID, &cs.BotAName, &cs.BotBID, &cs.BotBName,
&cs.Format, &cs.AWins, &cs.BWins, &cs.Status, &winnerID,
&cs.Round, &cs.BracketPosition); err != nil {
return nil, err
}
if winnerID.Valid {
cs.WinnerID = winnerID.String
}
result = append(result, cs)
}
// Fetch games for each series
for i := range result {
games, err := fetchSeriesGames(ctx, db, result[i].ID)
if err == nil {
result[i].Games = games
}
}
return result, nil
}
func fetchPredictions(ctx context.Context, db *sql.DB) ([]PredictionData, error) {
query := `
SELECT id, match_id, predictor_id, predicted_bot, correct, created_at, resolved_at
FROM predictions
ORDER BY created_at DESC
LIMIT 1000
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var predictions []PredictionData
for rows.Next() {
var p PredictionData
var correct sql.NullBool
var resolvedAt sql.NullTime
err := rows.Scan(
&p.ID, &p.MatchID, &p.PredictorID, &p.PredictedBot,
&correct, &p.CreatedAt, &resolvedAt,
)
if err != nil {
return nil, err
}
if correct.Valid {
p.Correct = &correct.Bool
}
if resolvedAt.Valid {
p.ResolvedAt = &resolvedAt.Time
}
predictions = append(predictions, p)
}
return predictions, nil
}
func fetchPredictorStats(ctx context.Context, db *sql.DB) ([]PredictorStats, error) {
query := `
SELECT predictor_id, correct, incorrect, streak, best_streak
FROM predictor_stats
ORDER BY (correct::float / NULLIF(correct + incorrect, 0)) DESC NULLS LAST
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var stats []PredictorStats
for rows.Next() {
var s PredictorStats
if err := rows.Scan(&s.PredictorID, &s.Correct, &s.Incorrect, &s.Streak, &s.BestStreak); err != nil {
return nil, err
}
stats = append(stats, s)
}
return stats, nil
}
func fetchMaps(ctx context.Context, db *sql.DB) ([]MapData, error) {
query := `
SELECT m.map_id, m.player_count, m.status, m.engagement, m.wall_density,
m.energy_count, m.grid_width, m.grid_height, m.created_at, m.map_json,
COALESCE(v.vote_sum, 0) as net_votes
FROM maps m
LEFT JOIN (
SELECT map_id, SUM(vote)::int as vote_sum
FROM map_votes
GROUP BY map_id
) v ON m.map_id = v.map_id
WHERE m.status IN ('active', 'probation', 'classic')
ORDER BY m.engagement DESC
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var maps []MapData
for rows.Next() {
var m MapData
if err := rows.Scan(
&m.MapID, &m.PlayerCount, &m.Status, &m.Engagement, &m.WallDensity,
&m.EnergyCount, &m.GridWidth, &m.GridHeight, &m.CreatedAt, &m.RawJSON,
&m.NetVotes,
); err != nil {
return nil, err
}
maps = append(maps, m)
}
return maps, nil
}
// fetchOpenPredictions retrieves pending matches that are "predictable":
// - Both bots are in the top 20
// - It's a rivalry match (at least 3 previous h2h matches)
// - It's a series match
// - An evolved bot faces a top-10 human-written bot
func fetchOpenPredictions(ctx context.Context, db *sql.DB) ([]OpenPredictionMatch, error) {
// Get all pending matches with their participants
query := `
SELECT m.match_id, m.created_at,
mp1.bot_id as bot_a_id, b1.name as bot_a_name,
(b1.rating_mu - 2*b1.rating_phi) as bot_a_rating,
b1.evolved as bot_a_evolved,
mp2.bot_id as bot_b_id, b2.name as bot_b_name,
(b2.rating_mu - 2*b2.rating_phi) as bot_b_rating,
b2.evolved as bot_b_evolved,
COALESCE(EXISTS(
SELECT 1 FROM series_games sg
WHERE sg.match_id = m.match_id
), false) as is_series_match
FROM matches m
JOIN match_participants mp1 ON m.match_id = mp1.match_id AND mp1.player_slot = 0
JOIN match_participants mp2 ON m.match_id = mp2.match_id AND mp2.player_slot = 1
JOIN bots b1 ON mp1.bot_id = b1.bot_id
JOIN bots b2 ON mp2.bot_id = b2.bot_id
WHERE m.status = 'pending'
ORDER BY m.created_at ASC
LIMIT 50
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query pending matches: %w", err)
}
defer rows.Close()
var allMatches []OpenPredictionMatch
for rows.Next() {
var m OpenPredictionMatch
var isSeries bool
err := rows.Scan(
&m.MatchID, &m.CreatedAt,
&m.BotAID, &m.BotAName, &m.ARating, &m.AEvolved,
&m.BotBID, &m.BotBName, &m.BRating, &m.BEvolved,
&isSeries,
)
if err != nil {
return nil, fmt.Errorf("scan pending match: %w", err)
}
m.IsSeriesMatch = isSeries
allMatches = append(allMatches, m)
}
if len(allMatches) == 0 {
return []OpenPredictionMatch{}, nil
}
// Get top 20 bot IDs for top-20 vs top-20 check
topBotIDs := make(map[string]bool)
topRows, err := db.QueryContext(ctx, `
SELECT bot_id FROM bots
WHERE status = 'active'
ORDER BY rating_mu DESC
LIMIT 20
`)
if err != nil {
return nil, fmt.Errorf("query top bots: %w", err)
}
defer topRows.Close()
for topRows.Next() {
var botID string
if err := topRows.Scan(&botID); err != nil {
return nil, err
}
topBotIDs[botID] = true
}
// Get top 10 bot IDs for evolved vs top-10 check
top10BotIDs := make(map[string]bool)
top10Rows, err := db.QueryContext(ctx, `
SELECT bot_id FROM bots
WHERE status = 'active' AND evolved = false
ORDER BY rating_mu DESC
LIMIT 10
`)
if err != nil {
return nil, fmt.Errorf("query top 10 bots: %w", err)
}
defer top10Rows.Close()
for top10Rows.Next() {
var botID string
if err := top10Rows.Scan(&botID); err != nil {
return nil, err
}
top10BotIDs[botID] = true
}
// Build pair frequency map for rivalry detection (count completed h2h matches)
pairFrequency := make(map[string]int)
freqRows, err := db.QueryContext(ctx, `
SELECT mp1.bot_id, mp2.bot_id, COUNT(*)
FROM matches m
JOIN match_participants mp1 ON m.match_id = mp1.match_id AND mp1.player_slot = 0
JOIN match_participants mp2 ON m.match_id = mp2.match_id AND mp2.player_slot = 1
WHERE m.status = 'completed'
GROUP BY mp1.bot_id, mp2.bot_id
`)
if err != nil {
return nil, fmt.Errorf("query pair frequency: %w", err)
}
defer freqRows.Close()
for freqRows.Next() {
var botA, botB string
var count int
if err := freqRows.Scan(&botA, &botB, &count); err != nil {
return nil, err
}
pairFrequency[botA+":"+botB] = count
}
// Filter matches that are "predictable"
var predictableMatches []OpenPredictionMatch
for _, m := range allMatches {
isPredictable := false
// Check: both bots in top 20
if topBotIDs[m.BotAID] && topBotIDs[m.BotBID] {
isPredictable = true
}
// Check: rivalry match (at least 3 previous h2h matches)
if freq, ok := pairFrequency[m.BotAID+":"+m.BotBID]; ok && freq >= 3 {
isPredictable = true
}
// Check: series match
if m.IsSeriesMatch {
isPredictable = true
}
// Check: evolved bot vs top-10 human-written bot
if m.AEvolved && top10BotIDs[m.BotBID] {
isPredictable = true
}
if m.BEvolved && top10BotIDs[m.BotAID] {
isPredictable = true
}
if isPredictable {
// Calculate head-to-head record
h2hRecord := computeHeadToHeadRecord(ctx, db, m.BotAID, m.BotBID)
m.HeadToHeadRecord = &h2hRecord
predictableMatches = append(predictableMatches, m)
}
// Limit to next 10 matches
if len(predictableMatches) >= 10 {
break
}
}
return predictableMatches, nil
}
// computeHeadToHeadRecord returns the head-to-head record between two bots
func computeHeadToHeadRecord(ctx context.Context, db *sql.DB, botAID, botBID string) string {
var aWins, bWins int
err := db.QueryRowContext(ctx, `
SELECT
COUNT(*) FILTER (WHERE m.winner = 0) as a_wins,
COUNT(*) FILTER (WHERE m.winner = 1) as b_wins
FROM matches m
JOIN match_participants mp1 ON m.match_id = mp1.match_id AND mp1.player_slot = 0
JOIN match_participants mp2 ON m.match_id = mp2.match_id AND mp2.player_slot = 1
WHERE mp1.bot_id = $1 AND mp2.bot_id = $2 AND m.status = 'completed'
`, botAID, botBID).Scan(&aWins, &bWins)
if err != nil {
return ""
}
return fmt.Sprintf("%d-%d", aWins, bWins)
}
func fetchFeedback(ctx context.Context, db *sql.DB) ([]FeedbackEntry, error) {
query := `
SELECT feedback_id, match_id, turn, type, body, author, upvotes, created_at
FROM replay_feedback
ORDER BY upvotes DESC, created_at DESC
LIMIT 5000
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []FeedbackEntry
for rows.Next() {
var e FeedbackEntry
if err := rows.Scan(&e.FeedbackID, &e.MatchID, &e.Turn, &e.Type, &e.Body, &e.Author, &e.Upvotes, &e.CreatedAt); err != nil {
return nil, err
}
entries = append(entries, e)
}
return entries, nil
}
func computeTopPredictors(stats []PredictorStats) []PredictorStats {
if len(stats) > 50 {
return stats[:50]
}
return stats
}
// ─── Evolution Data (meta.json, lineage.json) ───────────────────────────────────────
// EvolutionMeta represents data/evolution/meta.json
type EvolutionMeta struct {
Generation int `json:"generation"`
PromotedToday int `json:"promoted_today"`
Top10Count int `json:"top_10_count"`
IslandPopulations map[string]int `json:"island_populations"` // island -> program count
BestRatings []EvolvedBotRating `json:"best_ratings"` // top 10 evolved bots
TotalPromoted int `json:"total_promoted"` // all-time promoted count
PromotionRate float64 `json:"promotion_rate"` // promoted/total
UpdatedAt string `json:"updated_at"`
MatchesToday int `json:"matches_today"` // plan §16.18: matches completed today
ActiveBots int `json:"active_bots"` // plan §16.18: active bot count
}
// EvolvedBotRating represents an evolved bot's rating info
type EvolvedBotRating struct {
BotID string `json:"bot_id"`
Name string `json:"name"`
Rating float64 `json:"rating"`
Island string `json:"island"`
Language string `json:"language"`
}
// LineageNode represents a single program in the lineage tree
type LineageNode struct {
ID int64 `json:"id"`
ParentIDs []int64 `json:"parent_ids"`
Generation int `json:"generation"`
Island string `json:"island"`
Fitness float64 `json:"fitness"`
Promoted bool `json:"promoted"`
Language string `json:"language"`
CreatedAt time.Time `json:"created_at"`
}
// fetchEvolutionMeta queries the evolver database for evolution statistics.
// It connects to the evolver database using the same connection parameters.
// Returns empty meta (not an error) if the evolution system is not running.
func fetchEvolutionMeta(ctx context.Context, db *sql.DB) (*EvolutionMeta, error) {
// Query the programs table in the evolver database
// Note: the evolver uses a separate database but same PostgreSQL instance
query := `
SELECT
COALESCE(MAX(generation), 0) as generation,
COALESCE(COUNT(*) FILTER (WHERE promoted AND created_at >= CURRENT_DATE), 0) as promoted_today,
COALESCE(COUNT(*) FILTER (WHERE promoted), 0) as total_promoted,
COALESCE(COUNT(*), 0) as total_programs
FROM programs
`
var meta EvolutionMeta
var updatedAt string = time.Now().UTC().Format(time.RFC3339)
var totalPrograms int
err := db.QueryRowContext(ctx, query).Scan(&meta.Generation, &meta.PromotedToday, &meta.TotalPromoted, &totalPrograms)
if err != nil {
// If evolver tables don't exist or query fails, return empty meta
// This is expected when the evolution system has not been initialized yet
slog.Info("Evolution system not running or programs table empty", "error", err)
return &EvolutionMeta{
Generation: 0,
PromotedToday: 0,
Top10Count: 0,
IslandPopulations: make(map[string]int),
BestRatings: []EvolvedBotRating{},
TotalPromoted: 0,
PromotionRate: 0,
UpdatedAt: updatedAt,
MatchesToday: 0,
ActiveBots: 0,
}, nil
}
// Calculate promotion rate
if totalPrograms > 0 {
meta.PromotionRate = float64(meta.TotalPromoted) / float64(totalPrograms)
}
// Fetch island populations
meta.IslandPopulations = make(map[string]int)
islandRows, err := db.QueryContext(ctx, `
SELECT island, COUNT(*) FROM programs GROUP BY island
`)
if err == nil {
for islandRows.Next() {
var island string
var count int
if islandRows.Scan(&island, &count) == nil {
meta.IslandPopulations[island] = count
}
}
islandRows.Close()
}
// Fetch best ratings (top 10 evolved bots by rating)
// Join with bots table to get current ratings
bestRows, err := db.QueryContext(ctx, `
SELECT
p.bot_id, b.name, b.rating_mu - 2*b.rating_phi as rating,
p.island, p.language
FROM programs p
JOIN bots b ON p.bot_id = b.bot_id
WHERE b.status = 'active'
ORDER BY b.rating_mu DESC
LIMIT 10
`)
if err == nil {
for bestRows.Next() {
var rating EvolvedBotRating
if bestRows.Scan(&rating.BotID, &rating.Name, &rating.Rating, &rating.Island, &rating.Language) == nil {
meta.BestRatings = append(meta.BestRatings, rating)
}
}
bestRows.Close()
}
// Count evolved bots in top 10
meta.Top10Count = len(meta.BestRatings)
// Fetch matches today (plan §16.18: completed matches since midnight UTC)
var matchesToday int
matchErr := db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM matches
WHERE completed_at >= CURRENT_DATE
`).Scan(&matchesToday)
if matchErr != nil {
matchesToday = 0
}
meta.MatchesToday = matchesToday
// Fetch active bots count (plan §16.18: bots with status = 'active')
var activeBots int
botErr := db.QueryRowContext(ctx, `
SELECT COUNT(*) FROM bots
WHERE status = 'active'
`).Scan(&activeBots)
if botErr != nil {
activeBots = 0
}
meta.ActiveBots = activeBots
meta.UpdatedAt = updatedAt
return &meta, nil
}
// fetchLineage queries the evolver database for the full lineage tree.
// Returns all programs with their parent relationships.
func fetchLineage(ctx context.Context, db *sql.DB) ([]LineageNode, error) {
query := `
SELECT id, parent_ids, generation, island, fitness, promoted, language, created_at
FROM programs
ORDER BY generation ASC, id ASC
`
rows, err := db.QueryContext(ctx, query)
if err != nil {
// If evolver tables don't exist, return empty lineage
if err == sql.ErrNoRows {
return []LineageNode{}, nil
}
return nil, fmt.Errorf("fetch lineage: %w", err)
}
defer rows.Close()
var nodes []LineageNode
for rows.Next() {
var node LineageNode
var parentJSON string
err := rows.Scan(&node.ID, &parentJSON, &node.Generation, &node.Island,
&node.Fitness, &node.Promoted, &node.Language, &node.CreatedAt)
if err != nil {
return nil, fmt.Errorf("scan lineage node: %w", err)
}
// Unmarshal parent_ids from JSONB
if err := json.Unmarshal([]byte(parentJSON), &node.ParentIDs); err != nil {
return nil, fmt.Errorf("unmarshal parent_ids: %w", err)
}
nodes = append(nodes, node)
}
return nodes, nil
}
// persistPlaylists writes generated playlist definitions and their match associations
// to the playlists and playlist_matches tables. It uses upsert semantics so playlists
// are updated in place without creating duplicates.
func persistPlaylists(ctx context.Context, db *sql.DB, playlists []persistedPlaylist) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, pl := range playlists {
_, err := tx.ExecContext(ctx, `
INSERT INTO playlists (slug, title, description, category, is_auto, updated_at)
VALUES ($1, $2, $3, $4, TRUE, NOW())
ON CONFLICT (slug) DO UPDATE SET
title = EXCLUDED.title,
description = EXCLUDED.description,
category = EXCLUDED.category,
updated_at = NOW()
`, pl.Slug, pl.Title, pl.Description, pl.Category)
if err != nil {
return fmt.Errorf("persist playlist %s: %w", pl.Slug, err)
}
// Delete old match associations and re-insert
_, err = tx.ExecContext(ctx, `DELETE FROM playlist_matches WHERE playlist_slug = $1`, pl.Slug)
if err != nil {
return fmt.Errorf("clear playlist_matches for %s: %w", pl.Slug, err)
}
for _, pm := range pl.Matches {
_, err := tx.ExecContext(ctx, `
INSERT INTO playlist_matches (playlist_slug, match_id, sort_order, curation_tag)
VALUES ($1, $2, $3, $4)
`, pl.Slug, pm.MatchID, pm.SortOrder, pm.CurationTag)
if err != nil {
return fmt.Errorf("persist playlist_match %s/%s: %w", pl.Slug, pm.MatchID, err)
}
}
}
return tx.Commit()
}
type persistedPlaylist struct {
Slug string
Title string
Description string
Category string
Matches []persistedPlaylistMatch
}
type persistedPlaylistMatch struct {
MatchID string
SortOrder int
CurationTag string
}