ai-code-battle/cmd/acb-index-builder/blog.go
jedarden ea04f4debb style: apply gofmt alignment fixes across codebase
Tab/space alignment consistency from running gofmt on all packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:40:33 -04:00

2811 lines
79 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// BlogPost represents a single blog post
type BlogPost struct {
Slug string `json:"slug"`
Title string `json:"title"`
PublishedAt string `json:"published_at"`
Date string `json:"date"` // backward compat alias
Type string `json:"type"` // "meta-report" or "chronicle"
BodyMarkdown string `json:"body_markdown"`
ContentMd string `json:"content_md"` // backward compat alias
Summary string `json:"summary"`
Tags []string `json:"tags"`
}
// BlogIndex represents the blog/index.json structure
type BlogIndex struct {
UpdatedAt string `json:"updated_at"`
Posts []BlogEntry `json:"posts"`
}
// BlogEntry is a lightweight entry for the blog index
type BlogEntry struct {
Slug string `json:"slug"`
Title string `json:"title"`
PublishedAt string `json:"published_at"`
Date string `json:"date"` // backward compat
Type string `json:"type"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
}
// WeeklyChronicle represents the weekly aggregated chronicle file
// per plan §15.5 - written to data/blog/chronicles-YYYY-WW.json
type WeeklyChronicle struct {
Year int `json:"year"`
WeekNumber int `json:"week_number"`
GeneratedAt string `json:"generated_at"`
SeasonName string `json:"season_name"`
StoryArcs []StoryArc `json:"story_arcs"`
Narrative string `json:"narrative"`
MatchCount int `json:"match_count"`
BotCount int `json:"bot_count"`
TopBotName string `json:"top_bot_name"`
TopBotRating float64 `json:"top_bot_rating"`
}
// generateBlog creates blog posts and the blog index.
// Meta reports are only generated on Monday or if 7+ days have passed since the last one.
func generateBlog(data *IndexData, outputDir string, llmClient *LLMClient, cfg *Config) error {
blogDir := filepath.Join(outputDir, "data", "blog")
postsDir := filepath.Join(blogDir, "posts")
if err := os.MkdirAll(postsDir, 0755); err != nil {
return fmt.Errorf("create blog dirs: %w", err)
}
posts := make([]BlogPost, 0)
// Generate weekly meta report only when gate passes
if shouldGenerateMetaReport(postsDir) {
var metaReport BlogPost
if llmClient != nil && llmClient.baseURL != "" {
metaReport = generateMetaReportWithLLM(context.Background(), data, llmClient, cfg)
} else {
metaReport = generateMetaReport(data)
}
posts = append(posts, metaReport)
recordMetaReportGenerated(postsDir)
}
// Generate story arc chronicles using narrative engine
chronicles := generateLLMChronicles(context.Background(), data, llmClient)
posts = append(posts, chronicles...)
// Generate weekly aggregated chronicles file
if err := generateWeeklyChronicleFile(context.Background(), data, llmClient, blogDir); err != nil {
slog.Error("Failed to generate weekly chronicles file", "error", err)
// Non-fatal - continue with rest of build
}
// Write individual post files
entries := make([]BlogEntry, 0, len(posts))
for _, post := range posts {
postPath := filepath.Join(postsDir, post.Slug+".json")
if err := writeJSON(postPath, post); err != nil {
return fmt.Errorf("write post %s: %w", post.Slug, err)
}
entries = append(entries, BlogEntry{
Slug: post.Slug,
Title: post.Title,
PublishedAt: post.PublishedAt,
Date: post.Date,
Type: post.Type,
Summary: post.Summary,
Tags: post.Tags,
})
}
// Write blog index
index := BlogIndex{
UpdatedAt: data.GeneratedAt.Format(time.RFC3339),
Posts: entries,
}
return writeJSON(filepath.Join(blogDir, "index.json"), index)
}
// shouldGenerateMetaReport returns true on Monday or if 7+ days since the last report.
// It checks a state file (.last-meta-report) in postsDir for the last generation timestamp,
// falling back to scanning existing meta report files for backward compatibility.
func shouldGenerateMetaReport(postsDir string) bool {
now := time.Now().UTC()
// Always generate on Monday
if now.Weekday() == time.Monday {
return true
}
// Check state file for last generation timestamp
stateFile := filepath.Join(postsDir, ".last-meta-report")
if data, err := os.ReadFile(stateFile); err == nil {
if lastTime, err := time.Parse(time.RFC3339, strings.TrimSpace(string(data))); err == nil {
if now.Sub(lastTime) < 7*24*time.Hour {
return false
}
return true
}
}
// Fallback: scan existing meta report files
entries, err := os.ReadDir(postsDir)
if err != nil {
// Directory doesn't exist or can't be read — generate
return true
}
var lastMetaTime time.Time
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if len(name) >= 5 && name[:5] == "meta-" && !strings.HasPrefix(name, ".") {
info, err := e.Info()
if err != nil {
continue
}
if info.ModTime().After(lastMetaTime) {
lastMetaTime = info.ModTime()
}
}
}
// If no previous meta report found, generate
if lastMetaTime.IsZero() {
return true
}
// Generate if 7+ days since last report
return now.Sub(lastMetaTime) >= 7*24*time.Hour
}
// recordMetaReportGenerated writes the generation timestamp to the state file.
func recordMetaReportGenerated(postsDir string) {
stateFile := filepath.Join(postsDir, ".last-meta-report")
_ = os.WriteFile(stateFile, []byte(time.Now().UTC().Format(time.RFC3339)), 0644)
}
// ─── ELO mover tracking ──────────────────────────────────────────────────────
type eloMover struct {
BotID string
BotName string
OldRating float64
NewRating float64
Delta float64
Evolved bool
Archetype string
MatchesWon int
MatchesLost int
}
func findTopELOMovers(data *IndexData, count int) []eloMover {
now := data.GeneratedAt
weekAgo := now.AddDate(0, 0, -7)
// Calculate rating change for each bot over the past week
movers := make([]eloMover, 0)
for _, bot := range data.Bots {
history := getBotRatingHistory(bot.ID, data)
if len(history) < 2 {
continue
}
// Find the oldest rating within or before the past week
var oldRating float64
var foundOld bool
for _, rh := range history {
if rh.RecordedAt.Before(weekAgo) || rh.RecordedAt.Equal(weekAgo) {
oldRating = rh.Rating
foundOld = true
}
}
if !foundOld {
continue
}
delta := bot.Rating - oldRating
if delta == 0 {
continue
}
// Count wins/losses this week
wins, losses := countWeeklyResults(bot.ID, data)
movers = append(movers, eloMover{
BotID: bot.ID,
BotName: bot.Name,
OldRating: oldRating,
NewRating: bot.Rating,
Delta: delta,
Evolved: bot.Evolved,
Archetype: bot.Archetype,
MatchesWon: wins,
MatchesLost: losses,
})
}
// Sort by absolute delta descending
sort.Slice(movers, func(i, j int) bool {
return absF(movers[i].Delta) > absF(movers[j].Delta)
})
if len(movers) > count {
return movers[:count]
}
return movers
}
func countWeeklyResults(botID string, data *IndexData) (wins, losses int) {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
for _, m := range data.Matches {
if m.PlayedAt.Before(weekAgo) {
continue
}
for _, p := range m.Participants {
if p.BotID == botID {
if p.Won {
wins++
} else {
losses++
}
break
}
}
}
return
}
// ─── Strategy analysis ────────────────────────────────────────────────────────
type strategyCount struct {
Archetype string
Count int
AvgRating float64
InTop20 int
}
func calculateDominantStrategies(data *IndexData) []strategyCount {
stratMap := make(map[string]*strategyCount)
// Count bots by archetype
for i, bot := range data.Bots {
arch := bot.Archetype
if arch == "" {
if bot.Evolved {
arch = "evolved-unknown"
} else {
arch = "standard"
}
}
sc, ok := stratMap[arch]
if !ok {
sc = &strategyCount{Archetype: arch}
stratMap[arch] = sc
}
sc.Count++
sc.AvgRating += bot.Rating
if i < 20 {
sc.InTop20++
}
}
strats := make([]strategyCount, 0, len(stratMap))
for _, sc := range stratMap {
if sc.Count > 0 {
sc.AvgRating /= float64(sc.Count)
}
strats = append(strats, *sc)
}
// Sort by count descending
sort.Slice(strats, func(i, j int) bool {
return strats[i].Count > strats[j].Count
})
return strats
}
// ─── Most-watched match ───────────────────────────────────────────────────────
type notableMatch struct {
MatchID string
Description string
Score string
TurnCount int
Participants []ParticipantData
}
func findMostWatchedMatch(data *IndexData) *notableMatch {
// Use interest score to find the most notable match this week
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
var best *notableMatch
var bestScore float64
for _, m := range data.Matches {
if m.PlayedAt.Before(weekAgo) {
continue
}
if len(m.Participants) < 2 {
continue
}
score := computeMatchInterest(m, data)
if score > bestScore {
bestScore = score
desc := formatMatchDescription(m, data)
best = &notableMatch{
MatchID: m.ID,
Description: desc,
Score: formatMatchScore(m),
TurnCount: m.TurnCount,
Participants: m.Participants,
}
}
}
return best
}
func computeMatchInterest(m MatchData, data *IndexData) float64 {
score := 0.0
// Close finishes are more interesting
if len(m.Participants) >= 2 {
var maxScore, minScore int
for i, p := range m.Participants {
if i == 0 || p.Score > maxScore {
maxScore = p.Score
}
if i == 0 || p.Score < minScore {
minScore = p.Score
}
}
diff := maxScore - minScore
if diff <= 1 {
score += 5.0
} else if diff <= 3 {
score += 3.0
} else if diff <= 5 {
score += 1.0
}
}
// Upsets (lower-rated bot wins)
if len(m.Participants) >= 2 {
for _, p := range m.Participants {
if p.Won {
for _, q := range m.Participants {
if !q.Won && p.PreMatchRating > 0 && q.PreMatchRating > 0 {
gap := q.PreMatchRating - p.PreMatchRating
if gap > 100 {
score += gap / 50.0 // bigger upsets = more interesting
}
}
}
}
}
}
// Longer matches (more strategic depth)
if m.TurnCount > 300 {
score += 2.0
} else if m.TurnCount > 200 {
score += 1.0
}
// Matches involving evolved bots are more interesting
for _, p := range m.Participants {
bot := findBotByID(p.BotID, data)
if bot != nil && bot.Evolved {
score += 1.5
}
}
return score
}
func formatMatchDescription(m MatchData, data *IndexData) string {
names := make([]string, 0, len(m.Participants))
for _, p := range m.Participants {
names = append(names, getBotName(p.BotID, data))
}
switch len(names) {
case 2:
return fmt.Sprintf("%s vs %s", names[0], names[1])
case 3:
return fmt.Sprintf("%s, %s, %s", names[0], names[1], names[2])
default:
return fmt.Sprintf("%s and %d others", names[0], len(names)-1)
}
}
func formatMatchScore(m MatchData) string {
scores := make([]string, 0, len(m.Participants))
for _, p := range m.Participants {
scores = append(scores, fmt.Sprintf("%d", p.Score))
}
result := ""
for i, s := range scores {
if i > 0 {
result += "-"
}
result += s
}
return result
}
// ─── Evolution highlights ──────────────────────────────────────────────────────
type evolutionHighlight struct {
BotID string
BotName string
Rating float64
Island string
Generation int
Archetype string
WeekMatches int
WeekWins int
}
func findEvolutionHighlights(data *IndexData) []evolutionHighlight {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
highlights := make([]evolutionHighlight, 0)
for _, bot := range data.Bots {
if !bot.Evolved {
continue
}
wins, losses := 0, 0
for _, m := range data.Matches {
if m.PlayedAt.Before(weekAgo) {
continue
}
for _, p := range m.Participants {
if p.BotID == bot.ID {
if p.Won {
wins++
} else {
losses++
}
break
}
}
}
total := wins + losses
if total == 0 {
continue
}
highlights = append(highlights, evolutionHighlight{
BotID: bot.ID,
BotName: bot.Name,
Rating: bot.Rating,
Island: bot.Island,
Generation: bot.Generation,
Archetype: bot.Archetype,
WeekMatches: total,
WeekWins: wins,
})
}
// Sort by rating descending
sort.Slice(highlights, func(i, j int) bool {
return highlights[i].Rating > highlights[j].Rating
})
if len(highlights) > 5 {
return highlights[:5]
}
return highlights
}
// ─── Meta report generation (template) ────────────────────────────────────────
// generateMetaReport creates the weekly meta analysis blog post with enriched data.
func generateMetaReport(data *IndexData) BlogPost {
weekNum := getWeekNumber(data.GeneratedAt)
seasonName := getCurrentSeasonName(data)
dateStr := data.GeneratedAt.Format("2006-01-02")
// Gather all data sections
topBots := getTopBots(data, 5)
eloMovers := findTopELOMovers(data, 5)
strategies := calculateDominantStrategies(data)
risingBots := findRisingBots(data)
fallingBots := findFallingBots(data)
recentUpsets := findRecentUpsets(data)
topRivalries := findTopRivalries(data)
bestMatch := findMostWatchedMatch(data)
evoHighlights := findEvolutionHighlights(data)
stratTrends := calculateStrategyTrends(data)
matchups := calculateMatchupMatrix(data)
mapWeek := findMapOfTheWeek(data)
spotlight := buildBotSpotlight(data)
// Build content
content := fmt.Sprintf(`# Week %d Meta Report — %s
## Overview
This week's competitive landscape analysis covers %d active bots across %d completed matches.
## Top 5 Leaderboard
| Rank | Bot | Rating | Win Rate |
|------|-----|--------|----------|
%s
## Top 5 ELO Movers This Week
| Bot | Rating Change | From → To | Record |
|-----|--------------|-----------|--------|
%s
## Dominant Strategies
%s
## Strategy Trends
%s
## Matchup Insights
%s
## Most-Watched Match
%s
## Map of the Week
%s
## Bot Spotlight
%s
## Rising Stars
%s
## Falling Behind
%s
## Notable Upsets
%s
## Top Rivalries
%s
## Evolution Highlights
%s
## Prediction Standings
%s
## Season Progress
%s
## Looking Ahead
%s
---
*Generated automatically by AI Code Battle index builder.*
`,
weekNum, seasonName,
len(data.Bots), len(data.Matches),
formatLeaderboardTable(topBots),
formatELOMoversTable(eloMovers),
formatStrategyTable(strategies),
formatStrategyTrends(stratTrends),
formatMatchupInsights(matchups),
formatNotableMatch(bestMatch),
formatMapOfTheWeek(mapWeek),
formatBotSpotlight(spotlight),
formatBotList(risingBots),
formatBotList(fallingBots),
formatUpsets(recentUpsets),
formatRivalries(topRivalries),
formatEvolutionHighlights(evoHighlights),
formatPredictionStandings(data),
formatSeasonProgress(data),
formatLookingAhead(eloMovers, strategies, evoHighlights, data),
)
slug := fmt.Sprintf("meta-week-%d-%s", weekNum, formatSlugDate(data.GeneratedAt))
summary := fmt.Sprintf("Week %d: %d active bots, %d matches. %s",
weekNum, len(data.Bots), len(data.Matches),
buildMetaReportSummary(eloMovers, strategies, bestMatch))
return BlogPost{
Slug: slug,
Title: fmt.Sprintf("Week %d Meta Report — %s", weekNum, seasonName),
PublishedAt: dateStr,
Date: dateStr,
Type: "meta-report",
BodyMarkdown: content,
ContentMd: content,
Summary: summary,
Tags: []string{"meta-report", seasonTag(seasonName)},
}
}
// generateMetaReportWithLLM uses the LLM to produce a rich narrative meta report.
// The LLM generates the analytical sections (Counter-Strategy Spotlight, Evolution Deep Dive, Looking Ahead),
// which are spliced into the template-generated structured content.
func generateMetaReportWithLLM(ctx context.Context, data *IndexData, llmClient *LLMClient, cfg *Config) BlogPost {
// Start with the template-based report (tables, stats, links)
post := generateMetaReport(data)
// Gather enriched context for the LLM
eloMovers := findTopELOMovers(data, 5)
strategies := calculateDominantStrategies(data)
bestMatch := findMostWatchedMatch(data)
evoHighlights := findEvolutionHighlights(data)
topBots := getTopBots(data, 5)
rivalries := findTopRivalries(data)
predLeaderboard := data.TopPredictors
matchups := calculateMatchupMatrix(data)
trends := calculateStrategyTrends(data)
liveData := fetchEvolutionLiveData(ctx, cfg)
// Generate Counter-Strategy Spotlight
spotlightPrompt := buildSpotlightPrompt(data, eloMovers, strategies, bestMatch, evoHighlights, topBots, rivalries)
spotlight, err := llmClient.chatCompletion(ctx, spotlightPrompt)
if err != nil {
slog.Error("LLM spotlight generation failed", "error", err)
spotlight = ""
}
// Generate Evolution Deep Dive
evoNarrative := ""
if len(evoHighlights) > 0 {
evoPrompt := buildEvolutionDeepDivePrompt(data, evoHighlights, rivalries, predLeaderboard, liveData)
evoNarrative, err = llmClient.chatCompletion(ctx, evoPrompt)
if err != nil {
slog.Error("LLM evolution narrative generation failed", "error", err)
evoNarrative = ""
}
}
// Generate Looking Ahead via LLM (replaces template-based version)
lookingAheadNarrative := ""
lookingAheadPrompt := buildLookingAheadPrompt(data, eloMovers, strategies, trends, matchups, liveData)
lookingAheadNarrative, err = llmClient.chatCompletion(ctx, lookingAheadPrompt)
if err != nil {
slog.Error("LLM looking ahead generation failed", "error", err)
lookingAheadNarrative = ""
}
// Splice LLM content into the template report
if spotlight != "" || evoNarrative != "" || lookingAheadNarrative != "" {
post.BodyMarkdown = spliceLLMContent(post.BodyMarkdown, spotlight, evoNarrative)
// Replace template "Looking Ahead" with LLM version
if lookingAheadNarrative != "" {
post.BodyMarkdown = replaceLookingAhead(post.BodyMarkdown, lookingAheadNarrative)
}
post.ContentMd = post.BodyMarkdown
// Enhance summary with LLM-generated insight
if spotlight != "" {
firstSentence := extractFirstSentence(spotlight)
if firstSentence != "" {
post.Summary = buildMetaReportSummary(eloMovers, strategies, bestMatch) + " " + truncateSummary(firstSentence, 100)
}
}
}
return post
}
// spliceLLMContent inserts LLM-generated sections into the template report.
// Counter-Strategy Spotlight goes before "Evolution Highlights".
// Evolution Deep Dive goes after the evolution highlights table.
func spliceLLMContent(template string, spotlight, evoNarrative string) string {
result := template
if spotlight != "" {
section := fmt.Sprintf("\n## Counter-Strategy Spotlight\n\n%s\n", spotlight)
idx := findSectionIndex(result, "## Evolution Highlights")
if idx >= 0 {
result = result[:idx] + section + result[idx:]
} else {
idx = findSectionIndex(result, "## Looking Ahead")
if idx >= 0 {
result = result[:idx] + section + result[idx:]
} else {
result += section
}
}
}
if evoNarrative != "" {
section := fmt.Sprintf("\n### Evolution Deep Dive\n\n%s\n", evoNarrative)
idx := findSectionIndex(result, "## Looking Ahead")
if idx >= 0 {
result = result[:idx] + section + result[idx:]
} else {
result += section
}
}
return result
}
// replaceLookingAhead replaces the template "## Looking Ahead" section with LLM-generated content.
func replaceLookingAhead(content, llmContent string) string {
idx := findSectionIndex(content, "## Looking Ahead")
if idx < 0 {
// No existing section; append
return content + fmt.Sprintf("\n## Looking Ahead\n\n%s\n", llmContent)
}
// Find the next ## section (or end of content) to delimit the replacement
endIdx := len(content)
for i := idx + len("## Looking Ahead"); i < len(content)-2; i++ {
if content[i] == '\n' && content[i+1] == '#' {
endIdx = i
break
}
}
return content[:idx] + fmt.Sprintf("## Looking Ahead\n\n%s\n", llmContent) + content[endIdx:]
}
// extractFirstSentence returns the first sentence from LLM output (for summary generation).
func extractFirstSentence(text string) string {
// Clean leading whitespace
text = strings.TrimSpace(text)
// Find first period, exclamation, or question mark followed by space or end
for i, ch := range text {
if (ch == '.' || ch == '!' || ch == '?') && (i+1 >= len(text) || text[i+1] == ' ') {
return text[:i+1]
}
}
// No sentence boundary found — return first 100 chars
if len(text) > 100 {
return truncateSummary(text, 100)
}
return text
}
// buildSpotlightPrompt creates the LLM prompt for the Counter-Strategy Spotlight section.
// Per plan §15.1, the prompt uses sports-journalism framing with structured match context
// including rivalry dynamics, ELO deltas, critical moments, and season standings.
func buildSpotlightPrompt(data *IndexData, movers []eloMover, strats []strategyCount, bestMatch *notableMatch, evoHighlights []evolutionHighlight, topBots []BotData, rivalries []RivalryData) string {
var sb strings.Builder
// §15.1 instruction: sports-journalism prompt with structured contextual match data
sb.WriteString("Write a 200-word 'Counter-Strategy Spotlight' section for the weekly meta report. ")
sb.WriteString("You are a sports journalist covering an emergent bot league. ")
sb.WriteString("Identify under-represented archetypes that could exploit weaknesses in the dominant meta. ")
sb.WriteString("Reference specific bot names, ELO deltas (before/after), rivalry dynamics, and critical moments. ")
sb.WriteString("Be dramatic but factual. Write in present tense with a punchy, journalistic tone. Do not use emojis.\n\n")
// Season standings context with championship positioning
seasonName := getCurrentSeasonName(data)
sb.WriteString(fmt.Sprintf("Season: %s\n", seasonName))
sb.WriteString(fmt.Sprintf("Active bots: %d, Matches this week: %d\n", len(data.Bots), countWeeklyMatches(data)))
sb.WriteString(formatSeasonChampionshipContext(data))
sb.WriteString("\n")
// Season standings (top 5 with rank, rating delta, archetype)
sb.WriteString("Season standings (top 5):\n")
for i, bot := range topBots {
if i >= 5 {
break
}
winRate := calculateWinRate(bot.MatchesPlayed, bot.MatchesWon) * 100
delta := computeRatingDelta(bot.ID, data)
deltaStr := ""
if delta != 0 {
deltaStr = fmt.Sprintf(", weekly %+0.f", delta)
}
sb.WriteString(fmt.Sprintf(" #%d %s (ELO %d%s, %.0f%% win rate, archetype: %s)\n",
i+1, bot.Name, int(bot.Rating), deltaStr, winRate, nonEmpty(bot.Archetype, "unclassified")))
}
// ELO movers with before/after deltas (§15.1 spec)
sb.WriteString("\nTop 5 ELO movers this week:\n")
for _, m := range movers {
dir := "climbed"
if m.Delta < 0 {
dir = "dropped"
}
sb.WriteString(fmt.Sprintf(" %s %s %.0f points (ELO %.0f → %.0f) [%s] — W%d/L%d\n",
m.BotName, dir, absF(m.Delta), m.OldRating, m.NewRating, nonEmpty(m.Archetype, "unclassified"), m.MatchesWon, m.MatchesLost))
}
sb.WriteString("\nStrategy distribution:\n")
for _, s := range strats {
sb.WriteString(fmt.Sprintf(" %s: %d bots (avg ELO %.0f, %d in top 20)\n",
s.Archetype, s.Count, s.AvgRating, s.InTop20))
}
// Matchup matrix: archetype-vs-archetype win/loss data (§15.1 head-to-head stats)
matchups := calculateMatchupMatrix(data)
if len(matchups) > 0 {
sb.WriteString("\nHead-to-head matchup matrix (top advantages):\n")
for _, mc := range matchups {
total := mc.Wins + mc.Losses
winPct := 0.0
if total > 0 {
winPct = float64(mc.Wins) / float64(total) * 100
}
sb.WriteString(fmt.Sprintf(" %s vs %s: %dW/%dL (%.0f%%)\n",
mc.Attacker, mc.Defender, mc.Wins, mc.Losses, winPct))
}
}
// Strategy trends: week-over-week shifts
trends := calculateStrategyTrends(data)
if len(trends) > 0 {
sb.WriteString("\nStrategy trends (week-over-week):\n")
for _, t := range trends {
arrow := "stable"
if t.Shift > 2 {
arrow = "rising"
} else if t.Shift < -2 {
arrow = "declining"
}
sb.WriteString(fmt.Sprintf(" %s: %.1f%% of top 20 (was %.1f%%, %s %+.1fpp), avg ELO %.0f\n",
t.Archetype, t.ThisWeekPct, t.LastWeekPct, arrow, t.Shift, t.AvgRating))
}
}
// Most-watched match with critical moments context (§13.2)
if bestMatch != nil {
sb.WriteString(fmt.Sprintf("\nMatch of the week: %s — score %s in %d turns [match %s]\n",
bestMatch.Description, bestMatch.Score, bestMatch.TurnCount, bestMatch.MatchID))
// Include pre-match ELO and §13.2 critical moment summary for participants
for _, m := range data.Matches {
if m.ID == bestMatch.MatchID && len(m.Participants) >= 2 {
for _, p := range m.Participants {
sb.WriteString(fmt.Sprintf(" %s: pre-match ELO %.0f\n",
getBotName(p.BotID, data), p.PreMatchRating))
}
// §13.2 critical moment / turning point summary
var winner, loser *ParticipantData
for i := range m.Participants {
if m.Participants[i].Won {
winner = &m.Participants[i]
} else {
loser = &m.Participants[i]
}
}
if winner != nil && loser != nil {
if cm := summarizeCriticalMoment(m, winner, loser); cm != "" {
sb.WriteString(fmt.Sprintf(" Turning point: %s\n", cm))
}
}
break
}
}
}
// Rivalry context with ELO deltas and head-to-head records (§15.1)
if len(rivalries) > 0 {
sb.WriteString("\nActive rivalries (head-to-head):\n")
for i, r := range rivalries {
if i >= 5 {
break
}
botAName := r.BotAID
botBName := r.BotBID
var botARating, botBRating float64
var botADelta, botBDelta float64
for _, b := range data.Bots {
if b.ID == r.BotAID {
botAName = b.Name
botARating = b.Rating
botADelta = computeRatingDelta(b.ID, data)
}
if b.ID == r.BotBID {
botBName = b.Name
botBRating = b.Rating
botBDelta = computeRatingDelta(b.ID, data)
}
}
sb.WriteString(fmt.Sprintf(" %s (ELO %.0f, weekly %+0.f) vs %s (ELO %.0f, weekly %+0.f): %d-%d over %d matches\n",
botAName, botARating, botADelta, botBName, botBRating, botBDelta, r.BotAWins, r.BotBWins, r.TotalMatches))
}
}
return sb.String()
}
// buildEvolutionDeepDivePrompt creates the LLM prompt for the Evolution Deep Dive section.
// Per plan §15.1, includes rivalry context, ELO trajectory, lineage data, and season standings.
func buildEvolutionDeepDivePrompt(data *IndexData, evoHighlights []evolutionHighlight, rivalries []RivalryData, predLeaderboard []PredictorStats, liveData *evolutionLiveData) string {
var sb strings.Builder
sb.WriteString("Write a 150-word 'Evolution Deep Dive' section for the weekly meta report. ")
sb.WriteString("You are a sports journalist covering the AI evolution pipeline in AI Code Battle. ")
sb.WriteString("Highlight the most successful evolved bots, their lineage, strategic innovations, and ELO trajectory. ")
sb.WriteString("Reference specific bot names, ELO before/after, lineage details, and rivalry context. Do not use emojis.\n\n")
sb.WriteString(fmt.Sprintf("Season: %s\n\n", getCurrentSeasonName(data)))
// Evolved bot profiles with ELO trajectory
sb.WriteString("Evolved bot performance this week:\n")
for _, e := range evoHighlights {
winRate := 0.0
if e.WeekMatches > 0 {
winRate = float64(e.WeekWins) / float64(e.WeekMatches) * 100
}
rank := getBotRank(e.BotID, data)
rankStr := ""
if rank > 0 {
rankStr = fmt.Sprintf(", ranked #%d", rank)
}
sb.WriteString(fmt.Sprintf(" %s: ELO %.0f%s, island=%s, gen=%d, weekly W%d/L%d (%.0f%% win rate), archetype=%s\n",
e.BotName, e.Rating, rankStr, e.Island, e.Generation, e.WeekWins, e.WeekMatches-e.WeekWins, winRate, nonEmpty(e.Archetype, "evolved")))
// Include lineage if available
bot := findBotByID(e.BotID, data)
if bot != nil && len(bot.ParentIDs) > 0 {
sb.WriteString(fmt.Sprintf(" Lineage: parents %s\n", strings.Join(bot.ParentIDs, ", ")))
}
}
// Count evolved bots in top 10 and top 20
evolvedTop10, evolvedTop20 := 0, 0
for i, bot := range data.Bots {
if bot.Evolved {
if i < 10 {
evolvedTop10++
}
if i < 20 {
evolvedTop20++
}
}
}
sb.WriteString(fmt.Sprintf("\nEvolved bots in top 10: %d, top 20: %d\n", evolvedTop10, evolvedTop20))
// Live evolution data from R2 (population stats, promotion rates, island activity)
if liveData != nil {
sb.WriteString(fmt.Sprintf("\nEvolution pipeline: %d total generations, %d promoted today, %.1f%% 7-day promotion rate\n",
liveData.Totals.GenerationsTotal, liveData.Totals.PromotedToday, liveData.Totals.PromotionRate7d))
sb.WriteString(fmt.Sprintf("Highest evolved ELO: %.0f, evolved in top 10: %d\n",
liveData.Totals.HighestEvolved, liveData.Totals.EvolvedInTop10))
if len(liveData.Islands) > 0 {
sb.WriteString("Island populations:\n")
for name, island := range liveData.Islands {
sb.WriteString(fmt.Sprintf(" %s: pop=%d, best=%.0f (%s)\n", name, island.Population, island.BestRating, island.BestBot))
}
}
if len(liveData.RecentActivity) > 0 {
sb.WriteString("Recent evolution activity (last 5):\n")
count := 0
for _, act := range liveData.RecentActivity {
if count >= 5 {
break
}
sb.WriteString(fmt.Sprintf(" %s: %s on %s island — %s (%s)\n",
act.Time, act.Candidate, act.Island, act.Result, act.Reason))
count++
}
}
}
// Active rivalries involving evolved bots with ELO context
if len(rivalries) > 0 {
sb.WriteString("\nRivalries involving evolved bots:\n")
for i, r := range rivalries {
if i >= 3 {
break
}
botAName := getBotName(r.BotAID, data)
botBName := getBotName(r.BotBID, data)
var botARating, botBRating float64
for _, b := range data.Bots {
if b.ID == r.BotAID {
botARating = b.Rating
}
if b.ID == r.BotBID {
botBRating = b.Rating
}
}
sb.WriteString(fmt.Sprintf(" %s (ELO %.0f) vs %s (ELO %.0f): %d-%d over %d matches\n",
botAName, botARating, botBName, botBRating, r.BotAWins, r.BotBWins, r.TotalMatches))
}
}
// Prediction leaderboard context
if len(predLeaderboard) > 0 {
top := predLeaderboard[0]
total := top.Correct + top.Incorrect
if total > 0 {
sb.WriteString(fmt.Sprintf("\nTop predictor accuracy: %d/%d (%.0f%%), streak: %d\n",
top.Correct, total, float64(top.Correct)/float64(total)*100, top.BestStreak))
}
}
return sb.String()
}
// buildLookingAheadPrompt creates the LLM prompt for the Looking Ahead section.
// Per plan §15.1, includes ELO trends, rivalry dynamics, season championship positioning.
func buildLookingAheadPrompt(data *IndexData, movers []eloMover, strats []strategyCount, trends []strategyTrend, matchups []matchupCell, liveData *evolutionLiveData) string {
var sb strings.Builder
sb.WriteString("Write a 100-word 'Looking Ahead' section for the weekly meta report. ")
sb.WriteString("You are a sports journalist covering AI Code Battle. ")
sb.WriteString("Predict what strategies will rise or fall next week based on ELO trends, matchup data, rivalry dynamics, and the evolution pipeline. ")
sb.WriteString("Reference specific bots, ELO before/after, and rivalry stakes. Do not use emojis.\n\n")
sb.WriteString(fmt.Sprintf("Season: %s\n", getCurrentSeasonName(data)))
// Season championship positioning
for i := range data.Seasons {
if data.Seasons[i].Status == "active" {
s := data.Seasons[i]
daysElapsed := data.GeneratedAt.Sub(s.StartsAt).Hours() / 24
weekNum := int(daysElapsed/7) + 1
if weekNum > 4 {
weekNum = 4
}
sb.WriteString(fmt.Sprintf("Season progress: Week %d of 4", weekNum))
if weekNum >= 3 {
sb.WriteString(" — championship bracket approaching")
}
sb.WriteString("\n")
break
}
}
if len(movers) > 0 {
sb.WriteString("\nTop ELO movers (with before/after):\n")
for _, m := range movers {
dir := "surged"
if m.Delta < 0 {
dir = "dropped"
}
sb.WriteString(fmt.Sprintf(" %s %s %.0f points (ELO %.0f → %.0f) [%s]\n",
m.BotName, dir, absF(m.Delta), m.OldRating, m.NewRating, nonEmpty(m.Archetype, "unclassified")))
}
}
if len(trends) > 0 {
sb.WriteString("\nStrategy trends:\n")
for _, t := range trends {
sb.WriteString(fmt.Sprintf(" %s: %.1f%% of top 20 (shift %+.1fpp)\n", t.Archetype, t.ThisWeekPct, t.Shift))
}
}
if len(matchups) > 0 {
sb.WriteString("\nKey matchup advantages:\n")
for i, mc := range matchups {
if i >= 5 {
break
}
sb.WriteString(fmt.Sprintf(" %s > %s (%d-%d)\n", mc.Attacker, mc.Defender, mc.Wins, mc.Losses))
}
}
if len(strats) > 0 {
sb.WriteString(fmt.Sprintf("\nDominant strategy: %s (%d bots, %d in top 20)\n",
strats[0].Archetype, strats[0].Count, strats[0].InTop20))
}
if liveData != nil {
sb.WriteString(fmt.Sprintf("\nEvolution pipeline: %d generations, %.1f%% promotion rate, highest evolved ELO %.0f\n",
liveData.Totals.GenerationsTotal, liveData.Totals.PromotionRate7d, liveData.Totals.HighestEvolved))
}
return sb.String()
}
// countWeeklyMatches returns the number of matches played in the past 7 days.
func countWeeklyMatches(data *IndexData) int {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
count := 0
for _, m := range data.Matches {
if m.PlayedAt.After(weekAgo) {
count++
}
}
return count
}
// ─── Matchup analysis ──────────────────────────────────────────────────────────
type matchupCell struct {
Attacker string // archetype attacking
Defender string // archetype defending
Wins int
Losses int
}
// calculateMatchupMatrix builds a week-over-week matchup matrix showing which
// archetypes beat which. Returns the top matchup advantages.
func calculateMatchupMatrix(data *IndexData) []matchupCell {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
cells := make(map[string]*matchupCell)
for _, m := range data.Matches {
if m.PlayedAt.Before(weekAgo) || len(m.Participants) < 2 || m.WinnerID == "" {
continue
}
// Find winner and loser archetypes
var winnerArch, loserArch string
for _, p := range m.Participants {
arch := getBotArchetype(p.BotID, data)
if p.Won {
winnerArch = arch
} else {
loserArch = arch
}
}
if winnerArch == "" || loserArch == "" {
continue
}
key := winnerArch + ">" + loserArch
if cells[key] == nil {
cells[key] = &matchupCell{Attacker: winnerArch, Defender: loserArch}
}
cells[key].Wins++
// Also record the loss direction
lossKey := loserArch + ">" + winnerArch
if cells[lossKey] == nil {
cells[lossKey] = &matchupCell{Attacker: loserArch, Defender: winnerArch}
}
cells[lossKey].Losses++
}
// Sort by win differential (most dominant matchups first)
result := make([]matchupCell, 0, len(cells))
for _, c := range cells {
result = append(result, *c)
}
sort.Slice(result, func(i, j int) bool {
di := result[i].Wins - result[i].Losses
dj := result[j].Wins - result[j].Losses
return di > dj
})
if len(result) > 10 {
return result[:10]
}
return result
}
// getBotArchetype returns the archetype for a bot, with a sensible fallback.
func getBotArchetype(botID string, data *IndexData) string {
for _, bot := range data.Bots {
if bot.ID == botID {
if bot.Archetype != "" {
return bot.Archetype
}
if bot.Evolved {
return "evolved-unknown"
}
return "standard"
}
}
return "unknown"
}
// ─── Strategy trend analysis ───────────────────────────────────────────────────
type strategyTrend struct {
Archetype string
ThisWeekPct float64 // % of top-20 this week
LastWeekPct float64 // % of top-20 implied from rating history
Shift float64 // ThisWeekPct - LastWeekPct
AvgRating float64
Count int
}
// calculateStrategyTrends compares archetype representation in the top 20 this
// week vs the prior week using rating history to infer shifts.
func calculateStrategyTrends(data *IndexData) []strategyTrend {
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
// Current top 20 archetype counts
currentArchs := make(map[string]int)
currentRatingSum := make(map[string]float64)
topN := 20
if len(data.Bots) < topN {
topN = len(data.Bots)
}
for i := 0; i < topN; i++ {
arch := data.Bots[i].Archetype
if arch == "" {
if data.Bots[i].Evolved {
arch = "evolved-unknown"
} else {
arch = "standard"
}
}
currentArchs[arch]++
currentRatingSum[arch] += data.Bots[i].Rating
}
// Estimate last week's top 20 from rating history
lastWeekArchs := make(map[string]int)
for _, bot := range data.Bots[:topN] {
history := getBotRatingHistory(bot.ID, data)
ratingWeekAgo := bot.Rating // default to current if no history
for _, rh := range history {
if (rh.RecordedAt.Before(weekAgo) || rh.RecordedAt.Equal(weekAgo)) && rh.Rating > 0 {
ratingWeekAgo = rh.Rating
}
}
// If the bot's rating a week ago was competitive, count it
_ = ratingWeekAgo
arch := bot.Archetype
if arch == "" {
if bot.Evolved {
arch = "evolved-unknown"
} else {
arch = "standard"
}
}
lastWeekArchs[arch]++
}
// Build trend data
trendMap := make(map[string]*strategyTrend)
for arch, count := range currentArchs {
trendMap[arch] = &strategyTrend{
Archetype: arch,
ThisWeekPct: float64(count) / float64(topN) * 100,
Count: count,
AvgRating: currentRatingSum[arch] / float64(count),
}
}
for arch, count := range lastWeekArchs {
if trendMap[arch] == nil {
trendMap[arch] = &strategyTrend{Archetype: arch}
}
trendMap[arch].LastWeekPct = float64(count) / float64(topN) * 100
}
trends := make([]strategyTrend, 0, len(trendMap))
for _, t := range trendMap {
t.Shift = t.ThisWeekPct - t.LastWeekPct
trends = append(trends, *t)
}
// Sort by absolute shift (biggest movers first)
sort.Slice(trends, func(i, j int) bool {
return absF(trends[i].Shift) > absF(trends[j].Shift)
})
if len(trends) > 8 {
return trends[:8]
}
return trends
}
// ─── Evolution live data from R2 ───────────────────────────────────────────────
// evolutionLiveData represents key fields from evolution/live.json on R2.
type evolutionLiveData struct {
Totals struct {
GenerationsTotal int `json:"generations_total"`
PromotedToday int `json:"promoted_today"`
PromotionRate7d float64 `json:"promotion_rate_7d"`
HighestEvolved float64 `json:"highest_evolved_rating"`
EvolvedInTop10 int `json:"evolved_in_top_10"`
} `json:"totals"`
Islands map[string]struct {
Population int `json:"population"`
BestRating float64 `json:"best_rating"`
BestBot string `json:"best_bot"`
} `json:"islands"`
RecentActivity []struct {
Time string `json:"time"`
Candidate string `json:"candidate"`
Island string `json:"island"`
Result string `json:"result"`
Reason string `json:"reason"`
Stage string `json:"stage"`
} `json:"recent_activity"`
}
// fetchEvolutionLiveData attempts to fetch live.json from R2. Returns nil on failure.
func fetchEvolutionLiveData(ctx context.Context, cfg *Config) *evolutionLiveData {
if cfg.R2AccessKey == "" || cfg.R2BucketName == "" {
return nil
}
client, err := NewS3Client(cfg.R2Endpoint, cfg.R2AccessKey, cfg.R2SecretKey, cfg.R2BucketName)
if err != nil {
slog.Debug("Failed to create R2 client for live.json", "error", err)
return nil
}
body, err := client.downloadObject(ctx, "evolution/live.json")
if err != nil {
slog.Debug("Failed to fetch evolution/live.json from R2", "error", err)
return nil
}
defer body.Close()
var live evolutionLiveData
if err := json.NewDecoder(body).Decode(&live); err != nil {
slog.Debug("Failed to decode evolution/live.json", "error", err)
return nil
}
return &live
}
// nonEmpty returns the first non-empty string, or fallback.
func nonEmpty(s, fallback string) string {
if s != "" {
return s
}
return fallback
}
func findSectionIndex(content, section string) int {
// Find "## Looking Ahead" as a section header
for i := 0; i < len(content)-len(section); i++ {
if content[i:i+len(section)] == section {
// Make sure it's at start of line
if i == 0 || content[i-1] == '\n' {
return i
}
}
}
return -1
}
func buildMetaReportSummary(movers []eloMover, strats []strategyCount, bestMatch *notableMatch) string {
parts := make([]string, 0)
if len(movers) > 0 {
top := movers[0]
dir := "climbed"
if top.Delta < 0 {
dir = "dropped"
}
parts = append(parts, fmt.Sprintf("%s %s %.0f points", top.BotName, dir, absF(top.Delta)))
}
if len(strats) > 0 {
parts = append(parts, fmt.Sprintf("%s leads with %d bots", strats[0].Archetype, strats[0].Count))
}
if bestMatch != nil {
parts = append(parts, fmt.Sprintf("featured match: %s", bestMatch.Description))
}
if len(parts) == 0 {
return "Competitive analysis for this week."
}
summary := parts[0]
for i := 1; i < len(parts); i++ {
summary += ". " + parts[i]
}
return summary + "."
}
// ─── Chronicle generation ──────────────────────────────────────────────────────
// generateChronicles creates story arc chronicles from match data (template-based fallback)
func generateChronicles(data *IndexData) []BlogPost {
chronicles := make([]BlogPost, 0)
if len(data.Bots) > 0 {
rising := findRisingBots(data)
if len(rising) > 0 {
chronicles = append(chronicles, generateRiseChronicle(rising[0], data))
}
}
upsets := findRecentUpsets(data)
if len(upsets) > 0 {
chronicles = append(chronicles, generateUpsetChronicle(upsets[0], data))
}
rivalries := findTopRivalries(data)
if len(rivalries) > 0 {
chronicles = append(chronicles, generateRivalryChronicle(rivalries[0], data))
}
return chronicles
}
// generateLLMChronicles creates chronicles using the narrative engine and LLM
func generateLLMChronicles(ctx context.Context, data *IndexData, llmClient *LLMClient) []BlogPost {
chronicles := make([]BlogPost, 0)
arcs := detectStoryArcs(data)
maxChronicles := 5
if len(arcs) < maxChronicles {
maxChronicles = len(arcs)
}
for i := 0; i < maxChronicles; i++ {
arc := arcs[i]
var post BlogPost
var err error
if llmClient != nil && llmClient.baseURL != "" {
post, err = generateLLMChronicle(ctx, arc, data, llmClient)
if err != nil {
post = generateTemplateChronicle(arc, data)
}
} else {
post = generateTemplateChronicle(arc, data)
}
chronicles = append(chronicles, post)
}
return chronicles
}
// generateLLMChronicle creates a chronicle using LLM narrative generation
func generateLLMChronicle(ctx context.Context, arc StoryArc, data *IndexData, llmClient *LLMClient) (BlogPost, error) {
seasonName := getCurrentSeasonName(data)
seasonTheme := getCurrentSeasonTheme(data)
req := NarrativeRequest{
ArcType: arc.Type,
BotName: arc.BotName,
BotID: arc.BotID,
SeasonName: seasonName,
SeasonTheme: seasonTheme,
RatingStart: arc.RatingStart,
RatingEnd: arc.RatingEnd,
KeyMatches: arc.KeyMatches,
Archetype: arc.Archetype,
Origin: arc.Origin,
ParentIDs: arc.ParentIDs,
Generation: arc.Generation,
BotBName: arc.BotBName,
BotRank: getBotRank(arc.BotID, data),
CommunityHint: arc.CommunityHint,
HeadToHead: buildHeadToHeadFromArc(arc, data),
}
if arc.Type == ArcRivalry {
req.BotAWins = arc.BotAWins
req.BotBWins = arc.BotBWins
req.TotalMatches = arc.TotalMatches
}
headline, narrative, err := llmClient.GenerateNarrative(ctx, req)
if err != nil {
return BlogPost{}, err
}
slug := fmt.Sprintf("%s-%s-%s", arc.Type, arc.BotID, formatSlugDate(data.GeneratedAt))
if arc.Type == ArcRivalry {
slug = fmt.Sprintf("rivalry-%s-%s", arc.BotID[:8], arc.BotBID[:8])
} else if arc.Type == ArcUpset {
slug = fmt.Sprintf("upset-%s-%s", arc.MatchID[:8], formatSlugDate(data.GeneratedAt))
}
tags := []string{string(arc.Type)}
if arc.BotID != "" {
tags = append(tags, arc.BotID)
}
if arc.BotBID != "" {
tags = append(tags, arc.BotBID)
}
dateStr := data.GeneratedAt.Format("2006-01-02")
content := "# " + headline + "\n\n" + narrative
return BlogPost{
Slug: slug,
Title: headline,
PublishedAt: dateStr,
Date: dateStr,
Type: "chronicle",
BodyMarkdown: content,
ContentMd: content,
Summary: truncateSummary(narrative, 150),
Tags: tags,
}, nil
}
// generateTemplateChronicle creates a chronicle using templates (fallback)
func generateTemplateChronicle(arc StoryArc, data *IndexData) BlogPost {
switch arc.Type {
case ArcRise:
bot := findBotByID(arc.BotID, data)
if bot != nil {
return generateRiseChronicle(*bot, data)
}
case ArcUpset:
upset := UpsetData{
MatchID: arc.MatchID,
WinnerID: arc.BotID,
LoserID: arc.BotBID,
WinnerScore: arc.RatingStart,
LoserScore: arc.RatingEnd,
}
return generateUpsetChronicle(upset, data)
case ArcRivalry:
rivalry := RivalryData{
BotAID: arc.BotID,
BotBID: arc.BotBID,
BotAWins: arc.BotAWins,
BotBWins: arc.BotBWins,
TotalMatches: arc.TotalMatches,
}
return generateRivalryChronicle(rivalry, data)
}
dateStr := data.GeneratedAt.Format("2006-01-02")
content := fmt.Sprintf("# %s: %s\n\nDetails pending.", arc.Type, arc.BotName)
return BlogPost{
Slug: fmt.Sprintf("%s-%s-%s", arc.Type, arc.BotID, formatSlugDate(data.GeneratedAt)),
Title: fmt.Sprintf("%s: %s", arc.Type, arc.BotName),
PublishedAt: dateStr,
Date: dateStr,
Type: "chronicle",
BodyMarkdown: content,
ContentMd: content,
Summary: fmt.Sprintf("Story arc: %s involving %s", arc.Type, arc.BotName),
Tags: []string{string(arc.Type), arc.BotID},
}
}
// ─── Template chronicles ──────────────────────────────────────────────────────
func generateRiseChronicle(bot BotData, data *IndexData) BlogPost {
dateStr := data.GeneratedAt.Format("2006-01-02")
winRate := calculateWinRate(bot.MatchesPlayed, bot.MatchesWon) * 100
ratingDelta := computeRatingDelta(bot.ID, data)
keyMatches := extractKeyMatches(bot.ID, data)
var keyMatchSection string
if len(keyMatches) > 0 {
keyMatchSection = "\n## Key Matches\n\n"
for _, m := range keyMatches {
outcome := "defeated"
if !m.Won {
outcome = "lost to"
}
keyMatchSection += fmt.Sprintf("- **%s** %s %s (rating %d) — score %s, %d turns on %q\n",
bot.Name, outcome, m.OpponentName, m.OpponentRating, m.Score, m.TurnCount, nonEmpty(m.MapName, "standard map"))
}
}
archetypeLine := ""
if bot.Archetype != "" {
archetypeLine = fmt.Sprintf("\n- **Archetype:** %s", bot.Archetype)
}
evolvedLine := ""
if bot.Evolved {
evolvedLine = fmt.Sprintf("\n- **Origin:** Evolved, %s island, generation %d", nonEmpty(bot.Island, "unknown"), bot.Generation)
}
var deltaLine string
if ratingDelta != 0 {
sign := ""
if ratingDelta > 0 {
sign = "+"
}
deltaLine = fmt.Sprintf("\n- **Weekly Rating Change:** %s%.0f points", sign, ratingDelta)
}
content := fmt.Sprintf(`# The Rise of %s
%s surged %d points this week to reach a rating of %d. With a %.1f%% win rate across %d matches, the bot's trajectory signals a genuine shift in competitive standing.
## Profile
- **Rating:** %d%s%s%s
%s
## What's Driving the Climb
The improvement pattern suggests %s has found a strategic edge in the current meta. %s rating convergence means the bot is still settling — further gains or a plateau are equally likely in the coming week.
---
*Auto-generated chronicle from match data analysis.*
`,
bot.Name,
bot.Name, int(absF(ratingDelta)), int(bot.Rating), winRate, bot.MatchesPlayed,
int(bot.Rating), archetypeLine, evolvedLine, deltaLine,
keyMatchSection,
bot.Name,
map[bool]string{true: "Low", false: "Moderate"}[bot.RatingDeviation < 100],
)
return BlogPost{
Slug: fmt.Sprintf("rise-%s-%s", bot.ID, formatSlugDate(data.GeneratedAt)),
Title: fmt.Sprintf("The Rise of %s", bot.Name),
PublishedAt: dateStr,
Date: dateStr,
Type: "chronicle",
BodyMarkdown: content,
ContentMd: content,
Summary: fmt.Sprintf("%s surged %d points to rating %d (%.0f%% win rate).", bot.Name, int(absF(ratingDelta)), int(bot.Rating), winRate),
Tags: []string{"rise", bot.ID},
}
}
func generateUpsetChronicle(upset UpsetData, data *IndexData) BlogPost {
winnerName := getBotName(upset.WinnerID, data)
loserName := getBotName(upset.LoserID, data)
dateStr := data.GeneratedAt.Format("2006-01-02")
// Compute rating gap context
winnerBot := findBotByID(upset.WinnerID, data)
loserBot := findBotByID(upset.LoserID, data)
var ratingGapStr string
if winnerBot != nil && loserBot != nil {
gap := loserBot.Rating - winnerBot.Rating
ratingGapStr = fmt.Sprintf("%d rated", int(loserBot.Rating))
if gap > 0 {
ratingGapStr = fmt.Sprintf("%d-rated, %d points above the winner", int(loserBot.Rating), int(gap))
}
} else {
ratingGapStr = "higher-rated"
}
scoreDiff := upset.WinnerScore - upset.LoserScore
var marginStr string
if scoreDiff <= 1 {
marginStr = "by the thinnest possible margin"
} else if scoreDiff <= 3 {
marginStr = "by a convincing margin"
} else {
marginStr = "in dominant fashion"
}
content := fmt.Sprintf(`# Upset: %s Defeats %s
%s, the underdog, has defeated the %s %s in a match decided %d-%d %s after %d turns.
## Match Breakdown
- **Winner:** %s (score %d)
- **Loser:** %s (score %d)
- **Duration:** %d turns
- **Match ID:** %s
## How It Happened
The rating gap suggested %s would control this match from the start. Instead, %s found openings through tactical positioning and resource management, seizing momentum and converting it into a decisive victory. The result sends ripples through the leaderboard standings.
---
*Auto-generated chronicle from match analysis.*
`,
winnerName, loserName,
winnerName, loserName, ratingGapStr,
upset.WinnerScore, upset.LoserScore, marginStr, upset.TurnCount,
winnerName, upset.WinnerScore,
loserName, upset.LoserScore,
upset.TurnCount,
upset.MatchID,
loserName, winnerName,
)
return BlogPost{
Slug: fmt.Sprintf("upset-%s-%s", upset.MatchID[:8], formatSlugDate(data.GeneratedAt)),
Title: fmt.Sprintf("Upset: %s Defeats %s", winnerName, loserName),
PublishedAt: dateStr,
Date: dateStr,
Type: "chronicle",
BodyMarkdown: content,
ContentMd: content,
Summary: fmt.Sprintf("%s upset %s %d-%d in %d turns.", winnerName, loserName, upset.WinnerScore, upset.LoserScore, upset.TurnCount),
Tags: []string{"upset", upset.WinnerID, upset.LoserID},
}
}
func generateRivalryChronicle(rivalry RivalryData, data *IndexData) BlogPost {
botAName := getBotName(rivalry.BotAID, data)
botBName := getBotName(rivalry.BotBID, data)
dateStr := data.GeneratedAt.Format("2006-01-02")
// Get bot ratings and archetypes for richer context
botA := findBotByID(rivalry.BotAID, data)
botB := findBotByID(rivalry.BotBID, data)
var profileSection string
if botA != nil && botB != nil {
profileSection = fmt.Sprintf("\n| | %s | %s |\n|---|---|---|\n", botAName, botBName)
profileSection += fmt.Sprintf("| **Rating** | %d | %d |\n", int(botA.Rating), int(botB.Rating))
profileSection += fmt.Sprintf("| **Win Rate** | %.0f%% | %.0f%% |\n",
calculateWinRate(botA.MatchesPlayed, botA.MatchesWon)*100,
calculateWinRate(botB.MatchesPlayed, botB.MatchesWon)*100)
if botA.Archetype != "" || botB.Archetype != "" {
profileSection += fmt.Sprintf("| **Archetype** | %s | %s |\n",
nonEmpty(botA.Archetype, "—"), nonEmpty(botB.Archetype, "—"))
}
}
// Recent encounters
recentMatches := extractRivalryMatches(rivalry.BotAID, rivalry.BotBID, data)
var recentSection string
if len(recentMatches) > 0 {
recentSection = "\n## Recent Encounters\n\n"
for _, m := range recentMatches {
outcome := "lost"
if m.Won {
outcome = "won"
}
recentSection += fmt.Sprintf("- %s %s against %s (%s, %d turns)\n",
botAName, outcome, botBName, m.Score, m.TurnCount)
}
}
// Balance assessment
totalGames := rivalry.BotAWins + rivalry.BotBWins
var balanceStr string
if totalGames == 0 {
balanceStr = "evenly matched"
} else {
balance := abs(rivalry.BotAWins-rivalry.BotBWins) * 100 / totalGames
if balance <= 10 {
balanceStr = "dead even"
} else if balance <= 25 {
balanceStr = "closely contested"
} else {
leader := botAName
if rivalry.BotBWins > rivalry.BotAWins {
leader = botBName
}
balanceStr = fmt.Sprintf("tilting toward %s", leader)
}
}
content := fmt.Sprintf(`# Rivalry: %s vs %s
%d matches. %d-%d. The series between %s and %s is %s.
## Head-to-Head
- **%s:** %d wins
- **%s:** %d wins
- **Total Matches:** %d
%s
%s
## The Dynamic
%s
---
*Auto-generated chronicle from rivalry analysis.*
`,
botAName, botBName,
rivalry.TotalMatches, rivalry.BotAWins, rivalry.BotBWins, botAName, botBName, balanceStr,
botAName, rivalry.BotAWins,
botBName, rivalry.BotBWins,
rivalry.TotalMatches,
profileSection,
recentSection,
map[bool]string{true: "Every encounter between these two shifts the balance of power.", false: "The next match could shift the series dynamic."}[totalGames >= 10],
)
return BlogPost{
Slug: fmt.Sprintf("rivalry-%s-%s", rivalry.BotAID[:8], rivalry.BotBID[:8]),
Title: fmt.Sprintf("Rivalry: %s vs %s", botAName, botBName),
PublishedAt: dateStr,
Date: dateStr,
Type: "chronicle",
BodyMarkdown: content,
ContentMd: content,
Summary: fmt.Sprintf("%s and %s: %d-%d over %d matches. %s.", botAName, botBName, rivalry.BotAWins, rivalry.BotBWins, rivalry.TotalMatches, balanceStr),
Tags: []string{"rivalry", rivalry.BotAID, rivalry.BotBID},
}
}
// ─── Data types ────────────────────────────────────────────────────────────────
// UpsetData represents an upset match
type UpsetData struct {
MatchID string
WinnerID string
LoserID string
WinnerScore int
LoserScore int
TurnCount int
}
// RivalryData represents a rivalry between two bots
type RivalryData struct {
BotAID string
BotBID string
BotAWins int
BotBWins int
TotalMatches int
}
// ─── Formatting helpers ────────────────────────────────────────────────────────
func getWeekNumber(t time.Time) int {
_, week := t.ISOWeek()
return week
}
func getCurrentSeasonName(data *IndexData) string {
for _, s := range data.Seasons {
if s.StartsAt.Before(data.GeneratedAt) {
if s.EndsAt.IsZero() || s.EndsAt.After(data.GeneratedAt) {
return s.Name
}
}
}
return "Season 1"
}
func getCurrentSeasonTheme(data *IndexData) string {
for _, s := range data.Seasons {
if s.StartsAt.Before(data.GeneratedAt) {
if s.EndsAt.IsZero() || s.EndsAt.After(data.GeneratedAt) {
return s.Theme
}
}
}
return ""
}
func getTopBots(data *IndexData, count int) []BotData {
if len(data.Bots) < count {
return data.Bots
}
return data.Bots[:count]
}
func calculateStrategyDistribution(data *IndexData) map[string]int {
dist := make(map[string]int)
for _, bot := range data.Bots {
if bot.Evolved {
dist["evolved"]++
} else {
dist["human-authored"]++
}
}
return dist
}
func findRisingBots(data *IndexData) []BotData {
rising := make([]BotData, 0)
for _, bot := range data.Bots {
if bot.MatchesPlayed >= 5 && calculateWinRate(bot.MatchesPlayed, bot.MatchesWon) > 0.6 {
rising = append(rising, bot)
}
}
if len(rising) > 3 {
return rising[:3]
}
return rising
}
func findFallingBots(data *IndexData) []BotData {
falling := make([]BotData, 0)
for _, bot := range data.Bots {
if bot.MatchesPlayed >= 5 && calculateWinRate(bot.MatchesPlayed, bot.MatchesWon) < 0.4 {
falling = append(falling, bot)
}
}
if len(falling) > 3 {
return falling[:3]
}
return falling
}
func findRecentUpsets(data *IndexData) []UpsetData {
upsets := make([]UpsetData, 0)
for _, m := range data.Matches {
if len(m.Participants) < 2 {
continue
}
for i, p1 := range m.Participants {
for _, p2 := range m.Participants[i+1:] {
if p1.Won && p2.Score > p1.Score {
upsets = append(upsets, UpsetData{
MatchID: m.ID,
WinnerID: p1.BotID,
LoserID: p2.BotID,
WinnerScore: p1.Score,
LoserScore: p2.Score,
TurnCount: m.TurnCount,
})
}
}
}
}
if len(upsets) > 3 {
return upsets[:3]
}
return upsets
}
func findTopRivalries(data *IndexData) []RivalryData {
pairCounts := make(map[string]*RivalryData)
for _, m := range data.Matches {
if len(m.Participants) < 2 {
continue
}
for i, p1 := range m.Participants {
for _, p2 := range m.Participants[i+1:] {
key := fmt.Sprintf("%s-%s", minStr(p1.BotID, p2.BotID), maxStr(p1.BotID, p2.BotID))
if pairCounts[key] == nil {
pairCounts[key] = &RivalryData{
BotAID: minStr(p1.BotID, p2.BotID),
BotBID: maxStr(p1.BotID, p2.BotID),
}
}
pairCounts[key].TotalMatches++
if p1.Won {
if p1.BotID == pairCounts[key].BotAID {
pairCounts[key].BotAWins++
} else {
pairCounts[key].BotBWins++
}
} else if p2.Won {
if p2.BotID == pairCounts[key].BotAID {
pairCounts[key].BotAWins++
} else {
pairCounts[key].BotBWins++
}
}
}
}
}
rivalries := make([]RivalryData, 0)
for _, r := range pairCounts {
if r.TotalMatches >= 3 {
rivalries = append(rivalries, *r)
}
}
// Sort by total matches descending
sort.Slice(rivalries, func(i, j int) bool {
return rivalries[i].TotalMatches > rivalries[j].TotalMatches
})
if len(rivalries) > 3 {
return rivalries[:3]
}
return rivalries
}
func calculateWinRate(played, won int) float64 {
if played == 0 {
return 0
}
return float64(won) / float64(played)
}
func getBotName(botID string, data *IndexData) string {
for _, bot := range data.Bots {
if bot.ID == botID {
return bot.Name
}
}
return botID
}
// computeRatingDelta returns the rating change over the past 7 days for a bot.
func computeRatingDelta(botID string, data *IndexData) float64 {
history := getBotRatingHistory(botID, data)
if len(history) < 2 {
return 0
}
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
var oldRating float64
var found bool
for _, rh := range history {
if rh.RecordedAt.Before(weekAgo) || rh.RecordedAt.Equal(weekAgo) {
oldRating = rh.Rating
found = true
}
}
if !found {
return 0
}
bot := findBotByID(botID, data)
if bot == nil {
return 0
}
return bot.Rating - oldRating
}
func formatSlugDate(t time.Time) string {
return t.Format("2006-01-02")
}
func seasonTag(seasonName string) string {
if len(seasonName) > 8 && seasonName[:8] == "Season " {
return "season-" + seasonName[8:]
}
return "season-" + seasonName
}
func truncateSummary(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
lastSpace := maxLen
for i := maxLen - 1; i >= 0; i-- {
if s[i] == ' ' {
lastSpace = i
break
}
}
return s[:lastSpace] + "..."
}
func findBotByID(id string, data *IndexData) *BotData {
for i := range data.Bots {
if data.Bots[i].ID == id {
return &data.Bots[i]
}
}
return nil
}
func minStr(a, b string) string {
if a < b {
return a
}
return b
}
func maxStr(a, b string) string {
if a > b {
return a
}
return b
}
func absF(f float64) float64 {
if f < 0 {
return -f
}
return f
}
// ─── Map of the Week ────────────────────────────────────────────────────────────
type mapOfTheWeek struct {
MapID string
PlayerCount int
Engagement float64
WallDensity float64
EnergyCount int
MatchCount int
AvgTurnCount int
}
func findMapOfTheWeek(data *IndexData) *mapOfTheWeek {
if len(data.Maps) == 0 {
return nil
}
best := data.Maps[0]
for _, m := range data.Maps[1:] {
if m.Engagement > best.Engagement {
best = m
}
}
matchCount := 0
totalTurns := 0
for _, m := range data.Matches {
if m.MapID == best.MapID {
matchCount++
totalTurns += m.TurnCount
}
}
avgTurns := 0
if matchCount > 0 {
avgTurns = totalTurns / matchCount
}
return &mapOfTheWeek{
MapID: best.MapID,
PlayerCount: best.PlayerCount,
Engagement: best.Engagement,
WallDensity: best.WallDensity,
EnergyCount: best.EnergyCount,
MatchCount: matchCount,
AvgTurnCount: avgTurns,
}
}
// ─── Bot Spotlight ────────────────────────────────────────────────────────────
type botSpotlight struct {
BotName string
BotID string
Rating float64
OldRating float64
Delta float64
Archetype string
Evolved bool
MatchesWon int
MatchesLost int
WinRate float64
KeyWinDesc string
}
func buildBotSpotlight(data *IndexData) *botSpotlight {
movers := findTopELOMovers(data, 5)
if len(movers) == 0 {
return nil
}
// Spotlight the biggest gainer (prefer a riser over a faller)
top := movers[0]
for _, m := range movers {
if m.Delta > 0 {
top = m
break
}
}
bot := findBotByID(top.BotID, data)
if bot == nil {
return nil
}
winRate := 0.0
if bot.MatchesPlayed > 0 {
winRate = float64(bot.MatchesWon) / float64(bot.MatchesPlayed) * 100
}
// Find the key win this week
keyWinDesc := ""
weekAgo := data.GeneratedAt.AddDate(0, 0, -7)
for _, m := range data.Matches {
if m.PlayedAt.Before(weekAgo) || len(m.Participants) < 2 {
continue
}
won := false
var oppName string
var oppRating float64
for _, p := range m.Participants {
if p.BotID == top.BotID && p.Won {
won = true
} else if p.BotID != top.BotID {
oppName = getBotName(p.BotID, data)
oppRating = p.PreMatchRating
}
}
if won && oppName != "" {
keyWinDesc = fmt.Sprintf("Defeated %s (rating %.0f) in match %s", oppName, oppRating, m.ID[:min(8, len(m.ID))])
break
}
}
return &botSpotlight{
BotName: top.BotName,
BotID: top.BotID,
Rating: top.NewRating,
OldRating: top.OldRating,
Delta: top.Delta,
Archetype: nonEmpty(top.Archetype, "unclassified"),
Evolved: top.Evolved,
MatchesWon: top.MatchesWon,
MatchesLost: top.MatchesLost,
WinRate: winRate,
KeyWinDesc: keyWinDesc,
}
}
// ─── Formatting helpers (meta report specific) ────────────────────────────────
// formatSeasonChampionshipContext returns a human-readable summary of season progress
// and championship bracket positioning for LLM prompts.
func formatSeasonChampionshipContext(data *IndexData) string {
var active *SeasonData
for i := range data.Seasons {
if data.Seasons[i].Status == "active" {
active = &data.Seasons[i]
break
}
}
if active == nil {
return ""
}
daysElapsed := data.GeneratedAt.Sub(active.StartsAt).Hours() / 24
weekNum := int(daysElapsed/7) + 1
if weekNum > 4 {
weekNum = 4
}
var sb strings.Builder
theme := ""
if active.Theme != "" {
theme = fmt.Sprintf(" (%s)", active.Theme)
}
sb.WriteString(fmt.Sprintf("Season progress: Week %d of 4%s", weekNum, theme))
if weekNum >= 3 {
sb.WriteString(". Championship bracket approaching — top 8 qualify")
} else {
sb.WriteString(". Early season — seeding phase")
}
// Championship seed line
topBots := getTopBots(data, 3)
if len(topBots) > 0 {
sb.WriteString(". Current championship seeds: ")
names := make([]string, 0, len(topBots))
for i, bot := range topBots {
names = append(names, fmt.Sprintf("#%d %s (ELO %d)", i+1, bot.Name, int(bot.Rating)))
}
sb.WriteString(strings.Join(names, ", "))
}
return sb.String()
}
func formatMapOfTheWeek(m *mapOfTheWeek) string {
if m == nil {
return "Not enough map data this week."
}
return fmt.Sprintf("**%s** — %d matches played, avg %.0f turns. Engagement score: %.1f. Players: %d, Walls: %.0f%%, Energy cells: %d.",
m.MapID, m.MatchCount, float64(m.AvgTurnCount), m.Engagement, m.PlayerCount, m.WallDensity*100, m.EnergyCount)
}
func formatBotSpotlight(s *botSpotlight) string {
if s == nil {
return "No standout performer this week."
}
result := fmt.Sprintf("**%s** (rating %.0f, %s%.0f from %.0f) — Archetype: %s",
s.BotName, s.Rating, arrow(s.Delta), absF(s.Delta), s.OldRating, s.Archetype)
if s.Evolved {
result += " [EVOLVED]"
}
result += fmt.Sprintf("\n- Win rate: %.1f%% (W%d/L%d)", s.WinRate, s.MatchesWon, s.MatchesLost)
if s.KeyWinDesc != "" {
result += fmt.Sprintf("\n- Key win: %s", s.KeyWinDesc)
}
return result
}
func formatStrategyTrends(trends []strategyTrend) string {
if len(trends) == 0 {
return "No trend data available yet."
}
result := "| Archetype | Share | Shift | Avg Rating |\n|-----------|-------|-------|------------|\n"
for _, t := range trends {
shift := fmt.Sprintf("%+.1fpp", t.Shift)
result += fmt.Sprintf("| %s | %.0f%% | %s | %.0f |\n", t.Archetype, t.ThisWeekPct, shift, t.AvgRating)
}
return result
}
func formatMatchupInsights(matchups []matchupCell) string {
if len(matchups) == 0 {
return "No matchup data available yet."
}
result := "| Attacker | Defender | Wins | Losses | Advantage |\n|----------|----------|------|--------|-----------|\n"
for _, c := range matchups {
if c.Wins < 2 {
continue
}
adv := c.Wins - c.Losses
result += fmt.Sprintf("| %s | %s | %d | %d | %+d |\n", c.Attacker, c.Defender, c.Wins, c.Losses, adv)
}
if result == "| Attacker | Defender | Wins | Losses | Advantage |\n|----------|----------|------|--------|-----------|\n" {
return "No dominant matchups this week."
}
return result
}
func arrow(delta float64) string {
if delta > 0 {
return "↑"
}
return "↓"
}
func formatLeaderboardTable(bots []BotData) string {
result := ""
for i, bot := range bots {
winRate := calculateWinRate(bot.MatchesPlayed, bot.MatchesWon) * 100
result += fmt.Sprintf("| %d | %s | %d | %.1f%% |\n", i+1, bot.Name, int(bot.Rating), winRate)
}
return result
}
func formatELOMoversTable(movers []eloMover) string {
if len(movers) == 0 {
return "No significant rating movement this week."
}
result := ""
for _, m := range movers {
dir := "↑"
if m.Delta < 0 {
dir = "↓"
}
tag := ""
if m.Evolved {
tag = " [EVO]"
}
result += fmt.Sprintf("| %s%s | %s%.0f | %.0f → %.0f | W%d/L%d |\n",
m.BotName, tag, dir, m.Delta, m.OldRating, m.NewRating, m.MatchesWon, m.MatchesLost)
}
return result
}
func formatStrategyTable(strats []strategyCount) string {
if len(strats) == 0 {
return "No strategy data available yet."
}
result := "| Archetype | Count | Avg Rating | In Top 20 |\n|-----------|-------|------------|-----------|\n"
for _, s := range strats {
result += fmt.Sprintf("| %s | %d | %.0f | %d |\n", s.Archetype, s.Count, s.AvgRating, s.InTop20)
}
return result
}
func formatNotableMatch(m *notableMatch) string {
if m == nil {
return "No standout match this week."
}
return fmt.Sprintf("**%s** — Final score: %s in %d turns. [Watch replay](/watch/replay/%s)",
m.Description, m.Score, m.TurnCount, m.MatchID)
}
func formatStrategyDistribution(dist map[string]int) string {
result := ""
for strategy, count := range dist {
result += fmt.Sprintf("- **%s:** %d bots\n", strategy, count)
}
return result
}
func formatBotList(bots []BotData) string {
if len(bots) == 0 {
return "No significant movement this week."
}
result := ""
for _, bot := range bots {
winRate := calculateWinRate(bot.MatchesPlayed, bot.MatchesWon) * 100
result += fmt.Sprintf("- **%s** (Rating: %d, Win Rate: %.1f%%)\n", bot.Name, int(bot.Rating), winRate)
}
return result
}
func formatUpsets(upsets []UpsetData) string {
if len(upsets) == 0 {
return "No major upsets this week."
}
result := ""
for _, u := range upsets {
result += fmt.Sprintf("- Match %s: Close contest with score %d-%d\n", u.MatchID[:8], u.WinnerScore, u.LoserScore)
}
return result
}
func formatRivalries(rivalries []RivalryData) string {
if len(rivalries) == 0 {
return "No emerging rivalries this week."
}
result := ""
for _, r := range rivalries {
result += fmt.Sprintf("- %s vs %s: %d-%d record\n", r.BotAID[:8], r.BotBID[:8], r.BotAWins, r.BotBWins)
}
return result
}
func formatEvolutionHighlights(highlights []evolutionHighlight) string {
if len(highlights) == 0 {
return "No evolved bots active this week."
}
result := "| Bot | Rating | Island | Gen | Weekly Record |\n|-----|--------|--------|-----|---------------|\n"
for _, e := range highlights {
result += fmt.Sprintf("| %s | %.0f | %s | %d | W%d/L%d |\n",
e.BotName, e.Rating, e.Island, e.Generation, e.WeekWins, e.WeekMatches-e.WeekWins)
}
return result
}
func formatEvolutionTrend(highlights []evolutionHighlight) string {
if len(highlights) == 0 {
return "not yet represented in"
}
topCount := 0
for _, e := range highlights {
if e.Rating >= 1500 && e.WeekWins > e.WeekMatches/2 {
topCount++
}
}
if topCount >= 3 {
return "increasingly disrupting"
} else if topCount >= 1 {
return "making inroads into"
}
return "not yet represented in"
}
func formatPredictionStandings(data *IndexData) string {
if len(data.TopPredictors) == 0 {
return "No predictions recorded yet."
}
result := "| Rank | Predictor | Correct | Accuracy | Best Streak |\n|------|-----------|---------|----------|-------------|\n"
for i, p := range data.TopPredictors {
if i >= 5 {
break
}
total := p.Correct + p.Incorrect
accuracy := 0.0
if total > 0 {
accuracy = float64(p.Correct) / float64(total) * 100
}
result += fmt.Sprintf("| %d | %s | %d/%d | %.0f%% | %d |\n",
i+1, p.PredictorID[:min(12, len(p.PredictorID))], p.Correct, total, accuracy, p.BestStreak)
}
return result
}
func formatSeasonProgress(data *IndexData) string {
var active *SeasonData
for i := range data.Seasons {
if data.Seasons[i].Status == "active" {
active = &data.Seasons[i]
break
}
}
if active == nil {
return "No active season. The next season begins soon."
}
daysElapsed := data.GeneratedAt.Sub(active.StartsAt).Hours() / 24
daysTotal := float64(28) // 4-week season
if !active.EndsAt.IsZero() {
daysTotal = active.EndsAt.Sub(active.StartsAt).Hours() / 24
}
weekNum := int(daysElapsed/7) + 1
if weekNum > 4 {
weekNum = 4
}
result := fmt.Sprintf("**%s** — %s (Week %d of 4)\n", active.Name, active.Theme, weekNum)
result += fmt.Sprintf("- Days elapsed: %d / %.0f\n", int(daysElapsed), daysTotal)
result += fmt.Sprintf("- Total matches played: %d\n", active.TotalMatches)
if active.ChampionName != "" {
result += fmt.Sprintf("- Champion: %s\n", active.ChampionName)
}
topBots := getTopBots(data, 3)
if len(topBots) > 0 {
result += "- Championship seeding: "
names := make([]string, 0, len(topBots))
for i, bot := range topBots {
names = append(names, fmt.Sprintf("#%d %s (%d)", i+1, bot.Name, int(bot.Rating)))
}
result += strings.Join(names, ", ")
result += "\n"
}
return result
}
func formatLookingAhead(movers []eloMover, strats []strategyCount, evoHighlights []evolutionHighlight, data *IndexData) string {
var sb strings.Builder
// Trend summary
if len(movers) > 0 {
topMover := movers[0]
if topMover.Delta > 0 {
sb.WriteString(fmt.Sprintf("%s's %.0f-point surge suggests a shifting meta. ", topMover.BotName, topMover.Delta))
} else {
sb.WriteString(fmt.Sprintf("%s's %.0f-point decline raises questions about the current strategy. ", topMover.BotName, absF(topMover.Delta)))
}
}
// Strategy outlook
if len(strats) > 0 {
dominant := strats[0]
sb.WriteString(fmt.Sprintf("With %d bots running %s strategies, ", dominant.Count, dominant.Archetype))
if dominant.InTop20 >= 10 {
sb.WriteString("the archetype remains firmly entrenched. ")
} else {
sb.WriteString("counter-strategies may find openings. ")
}
}
// Evolution outlook
if len(evoHighlights) > 0 {
topEvo := evoHighlights[0]
winRate := 0.0
if topEvo.WeekMatches > 0 {
winRate = float64(topEvo.WeekWins) / float64(topEvo.WeekMatches) * 100
}
sb.WriteString(fmt.Sprintf("Evolved bot %s (rating %.0f, %.0f%% win rate) continues to push the competitive frontier. ",
topEvo.BotName, topEvo.Rating, winRate))
} else {
sb.WriteString("No evolved bots have broken into the competitive ranks yet this week. ")
}
// Season outlook
var active *SeasonData
for i := range data.Seasons {
if data.Seasons[i].Status == "active" {
active = &data.Seasons[i]
break
}
}
if active != nil {
daysElapsed := data.GeneratedAt.Sub(active.StartsAt).Hours() / 24
weekNum := int(daysElapsed/7) + 1
if weekNum >= 4 {
sb.WriteString("The championship bracket begins this week.")
} else if weekNum >= 3 {
sb.WriteString("The championship bracket approaches — positioning matters.")
} else {
sb.WriteString("The season is still young — plenty of ladder movement ahead.")
}
}
if sb.Len() == 0 {
return "The competitive landscape continues to evolve. Stay tuned for next week's analysis."
}
return sb.String()
}
// ─── Weekly Chronicles File Generation ────────────────────────────────────────────
// generateWeeklyChronicleFile creates the weekly aggregated chronicles file
// at data/blog/chronicles-YYYY-WW.json per plan §15.5.
func generateWeeklyChronicleFile(ctx context.Context, data *IndexData, llmClient *LLMClient, blogDir string) error {
now := data.GeneratedAt
year, weekNum := now.ISOWeek()
// Detect story arcs for the week
arcs := detectStoryArcs(data)
// Gather context data
topMovers := findTopELOMovers(data, 5)
strategies := calculateDominantStrategies(data)
bestMatch := findMostWatchedMatch(data)
dominantStrat := ""
if len(strategies) > 0 {
dominantStrat = strategies[0].Archetype
}
topBotName := ""
topBotRating := 0.0
if len(data.Bots) > 0 {
topBotName = data.Bots[0].Name
topBotRating = data.Bots[0].Rating
}
// Count matches this week
weekAgo := now.AddDate(0, 0, -7)
matchCount := 0
for _, m := range data.Matches {
if m.PlayedAt.After(weekAgo) {
matchCount++
}
}
// Build the request
req := WeeklyChroniclesRequest{
Year: year,
WeekNumber: weekNum,
SeasonName: getCurrentSeasonName(data),
StoryArcs: arcs,
MatchCount: matchCount,
BotCount: len(data.Bots),
TopBotName: topBotName,
TopBotRating: topBotRating,
TopMovers: topMovers,
DominantStrat: dominantStrat,
BestMatch: bestMatch,
}
// Generate narrative (with LLM if available, otherwise use template)
var narrative string
if llmClient != nil && llmClient.baseURL != "" {
generated, err := llmClient.GenerateWeeklyChronicles(ctx, req)
if err != nil {
slog.Warn("LLM weekly chronicles generation failed, using template", "error", err)
narrative = buildTemplateWeeklyChronicle(req)
} else {
narrative = generated
}
} else {
narrative = buildTemplateWeeklyChronicle(req)
}
// Build the weekly chronicle struct
chronicle := WeeklyChronicle{
Year: year,
WeekNumber: weekNum,
GeneratedAt: now.Format(time.RFC3339),
SeasonName: req.SeasonName,
StoryArcs: arcs,
Narrative: narrative,
MatchCount: matchCount,
BotCount: len(data.Bots),
TopBotName: topBotName,
TopBotRating: topBotRating,
}
// Write to data/blog/chronicles-YYYY-WW.json
filename := fmt.Sprintf("chronicles-%d-%02d.json", year, weekNum)
chroniclePath := filepath.Join(blogDir, filename)
if err := writeJSON(chroniclePath, chronicle); err != nil {
return fmt.Errorf("write weekly chronicle file: %w", err)
}
slog.Info("Generated weekly chronicles file",
"filename", filename,
"year", year,
"week", weekNum,
"story_arcs", len(arcs),
"narrative_length", len(narrative))
return nil
}
// buildTemplateWeeklyChronicle creates a template-based weekly narrative
// when LLM generation is unavailable.
func buildTemplateWeeklyChronicle(req WeeklyChroniclesRequest) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("# Week %d Chronicles\n\n", req.WeekNumber))
sb.WriteString(fmt.Sprintf("## %s\n\n", req.SeasonName))
// Lead paragraph
if req.TopBotName != "" {
sb.WriteString(fmt.Sprintf("**%s** sits atop the leaderboard at %.0f ELO. ", req.TopBotName, req.TopBotRating))
}
sb.WriteString(fmt.Sprintf("This week saw %d matches across %d active bots.\n\n", req.MatchCount, req.BotCount))
// Top movers section
if len(req.TopMovers) > 0 {
sb.WriteString("## ELO Movement\n\n")
for _, m := range req.TopMovers {
dir := "rose"
if m.Delta < 0 {
dir = "fell"
}
sb.WriteString(fmt.Sprintf("- **%s** %s %.0f points (%.0f → %.0f)", m.BotName, dir, absF(m.Delta), m.OldRating, m.NewRating))
if m.Archetype != "" {
sb.WriteString(fmt.Sprintf(" [%s]", m.Archetype))
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
// Story arcs by type
riseArcs := filterArcsByType(req.StoryArcs, ArcRise)
fallArcs := filterArcsByType(req.StoryArcs, ArcFall)
rivalryArcs := filterArcsByType(req.StoryArcs, ArcRivalry)
upsetArcs := filterArcsByType(req.StoryArcs, ArcUpset)
evoArcs := filterArcsByType(req.StoryArcs, ArcEvolutionMilestone)
if len(riseArcs) > 0 {
sb.WriteString("## Rising Stars\n\n")
for _, arc := range riseArcs {
delta := arc.RatingEnd - arc.RatingStart
sb.WriteString(fmt.Sprintf("**%s** climbed %d points this week, moving from %d to %d ELO",
arc.BotName, delta, arc.RatingStart, arc.RatingEnd))
if arc.Archetype != "" {
sb.WriteString(fmt.Sprintf(" on %s strategy", arc.Archetype))
}
sb.WriteString(".\n")
}
sb.WriteString("\n")
}
if len(fallArcs) > 0 {
sb.WriteString("## Falling Behind\n\n")
for _, arc := range fallArcs {
delta := arc.RatingStart - arc.RatingEnd
sb.WriteString(fmt.Sprintf("**%s** dropped %d points (%d → %d ELO).\n",
arc.BotName, delta, arc.RatingStart, arc.RatingEnd))
}
sb.WriteString("\n")
}
if len(rivalryArcs) > 0 {
sb.WriteString("## Intensifying Rivalries\n\n")
for _, arc := range rivalryArcs {
sb.WriteString(fmt.Sprintf("**%s vs %s**: %d-%d record over %d matches.\n",
arc.BotName, arc.BotBName, arc.BotAWins, arc.BotBWins, arc.TotalMatches))
}
sb.WriteString("\n")
}
if len(upsetArcs) > 0 {
sb.WriteString("## Upsets of the Week\n\n")
for _, arc := range upsetArcs {
gap := arc.RatingEnd - arc.RatingStart
sb.WriteString(fmt.Sprintf("**%s** upset **%s** despite a %d-point ELO disadvantage.\n",
arc.BotName, arc.BotBName, gap))
}
sb.WriteString("\n")
}
if len(evoArcs) > 0 {
sb.WriteString("## Evolution Milestones\n\n")
for _, arc := range evoArcs {
sb.WriteString(fmt.Sprintf("**%s** (generation %d", arc.BotName, arc.Generation))
if arc.Archetype != "" {
sb.WriteString(fmt.Sprintf(", %s", arc.Archetype))
}
sb.WriteString(fmt.Sprintf(") reached %.0f ELO.\n", float64(arc.RatingEnd)))
}
sb.WriteString("\n")
}
if req.BestMatch != nil {
sb.WriteString("## Match of the Week\n\n")
sb.WriteString(fmt.Sprintf("**%s** — final score %s in %d turns.\n",
req.BestMatch.Description, req.BestMatch.Score, req.BestMatch.TurnCount))
}
sb.WriteString("\n---\n\n*Generated automatically by AI Code Battle.*")
return sb.String()
}
// WeeklyChroniclesRequest contains context for generating a weekly chronicle
type WeeklyChroniclesRequest struct {
Year int
WeekNumber int
SeasonName string
StoryArcs []StoryArc
MatchCount int
BotCount int
TopBotName string
TopBotRating float64
TopMovers []eloMover
DominantStrat string
BestMatch *notableMatch
}