From fddcc0ba34f2ce3a57a17122826ef7963f8bf7d3 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 19:01:03 -0400 Subject: [PATCH] fix(index-builder): add missing getCurrentSeasonTheme and buildHeadToHeadFromArc Two functions referenced in generateLLMChronicle were undefined: - getCurrentSeasonTheme: returns the active season's theme string - buildHeadToHeadFromArc: computes W/L head-to-head records for a bot against all opponents from match data, enriching LLM narrative prompts Also improves the sports journalist system prompt with more detailed coverage style guidance for better narrative quality. Co-Authored-By: Claude Opus 4.7 --- .needle-predispatch-sha | 2 +- cmd/acb-index-builder/blog.go | 39 ++++++++++++----- cmd/acb-index-builder/narrative.go | 67 +++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 14 deletions(-) diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index 6062590..05230e5 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -38f14e1997145a3900b10124a18e31a812a65330 +17dbef092717f4f1f81dc870be3f8025466740a0 diff --git a/cmd/acb-index-builder/blog.go b/cmd/acb-index-builder/blog.go index 2bf5c51..0f9099c 100644 --- a/cmd/acb-index-builder/blog.go +++ b/cmd/acb-index-builder/blog.go @@ -1444,19 +1444,25 @@ func generateLLMChronicles(ctx context.Context, data *IndexData, llmClient *LLMC // 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, - SeasonName: seasonName, - RatingStart: arc.RatingStart, - RatingEnd: arc.RatingEnd, - KeyMatches: arc.KeyMatches, - Archetype: arc.Archetype, - Origin: arc.Origin, - ParentIDs: arc.ParentIDs, - Generation: arc.Generation, - BotBName: arc.BotBName, + 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 { @@ -1832,6 +1838,17 @@ func getCurrentSeasonName(data *IndexData) string { 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 diff --git a/cmd/acb-index-builder/narrative.go b/cmd/acb-index-builder/narrative.go index 9ad54fe..2f0ca0b 100644 --- a/cmd/acb-index-builder/narrative.go +++ b/cmd/acb-index-builder/narrative.go @@ -143,7 +143,8 @@ func buildNarrativePrompt(req NarrativeRequest) string { // §15.5 instruction: sports-journalism narrative with structured contextual match data sb.WriteString("Write a 200-word sports-journalism narrative about this event in the AI Code Battle platform. ") - sb.WriteString("Be dramatic but factual. Reference specific matches, ELO before/after deltas, rivalry context, and critical turning points. ") + sb.WriteString("Be dramatic but factual. Reference specific matches by ID, ELO before/after deltas, rivalry context, head-to-head records, critical turning points, and season standings. ") + sb.WriteString("Weave the data into a compelling story — quote scores, cite map names, describe the strategic moments that defined the outcome. ") sb.WriteString("Write in present tense with a punchy, journalistic tone. Do not use emojis.\n\n") // Season and standings context @@ -399,7 +400,15 @@ type llmChatResponse struct { // systemPromptSportsJournalist is the system prompt framing the LLM as a // sports journalist covering AI Code Battle — per plan §15.1 and §15.5. -const systemPromptSportsJournalist = `You are a sports journalist covering an emergent bot league called AI Code Battle, where autonomous programs compete in grid-based strategy matches. Write with the energy and narrative instinct of esports journalism — dramatic but factual, specific but accessible. Reference bots by name, cite ratings and score lines, and describe strategic turning points the way a commentator would. Use present tense. Do not use emojis. Keep paragraphs tight and punchy.` +const systemPromptSportsJournalist = `You are a sports journalist covering an emergent bot league called AI Code Battle, where autonomous programs compete in grid-based strategy matches. Write with the energy and narrative instinct of esports journalism — dramatic but factual, specific but accessible. + +Your coverage style: +- Reference bots by name, cite ELO ratings with before/after deltas, and describe strategic turning points the way a play-by-play commentator would. +- Weave in rivalry context, head-to-head records, season standings, and critical moments from match data. +- Describe ELO shifts the way a power rankings columnist describes team movement — "surged 200 points" not "increased." +- Use present tense. Keep paragraphs tight and punchy. Do not use emojis. +- When lineage or evolution data is provided, frame it like a scouting report — origin story, parent strategies, behavioral archetype. +- Always ground narrative in the specific match data, scores, and ratings provided — never fabricate match details.` func (c *LLMClient) chatCompletion(ctx context.Context, prompt string) (string, error) { body, err := json.Marshal(llmChatRequest{ @@ -921,6 +930,60 @@ func getBotRank(botID string, data *IndexData) int { return 0 } +func buildHeadToHeadFromArc(arc StoryArc, data *IndexData) []HeadToHeadRecord { + if arc.BotID == "" { + return nil + } + + type wl struct{ wins, losses int } + recordMap := make(map[string]*wl) + + for _, m := range data.Matches { + var botIn, opponentIn bool + var opponentID string + for _, p := range m.Participants { + if p.BotID == arc.BotID { + botIn = true + } else { + opponentIn = true + opponentID = p.BotID + } + } + if !botIn || !opponentIn || opponentID == "" { + continue + } + r, ok := recordMap[opponentID] + if !ok { + r = &wl{} + recordMap[opponentID] = r + } + if m.WinnerID == arc.BotID { + r.wins++ + } else if m.WinnerID == opponentID { + r.losses++ + } + } + + var records []HeadToHeadRecord + for oppID, r := range recordMap { + name := oppID + for _, b := range data.Bots { + if b.ID == oppID { + name = b.Name + break + } + } + records = append(records, HeadToHeadRecord{ + OpponentName: name, + OpponentRank: getBotRank(oppID, data), + Wins: r.wins, + Losses: r.losses, + TotalMatches: r.wins + r.losses, + }) + } + return records +} + // getBotRatingHistory returns rating history entries for a specific bot func getBotRatingHistory(botID string, data *IndexData) []RatingHistoryEntry {