From ae8eb0465ef653acc22b5b305d36427db64300c8 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 19:08:50 -0400 Subject: [PATCH] feat(narrative): add critical moment summaries to key match extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generates contextual turning-point descriptions for matches used in blog narratives and rivalry chronicles (§13.2). Summarizes close scores, ELO upsets, non-standard end conditions, and marathon matches. Co-Authored-By: Claude Opus 4.7 --- .needle-predispatch-sha | 2 +- cmd/acb-index-builder/narrative.go | 43 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index 05230e5..3d1880b 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -17dbef092717f4f1f81dc870be3f8025466740a0 +fddcc0ba34f2ce3a57a17122826ef7963f8bf7d3 diff --git a/cmd/acb-index-builder/narrative.go b/cmd/acb-index-builder/narrative.go index 2f0ca0b..4f37127 100644 --- a/cmd/acb-index-builder/narrative.go +++ b/cmd/acb-index-builder/narrative.go @@ -874,6 +874,7 @@ func extractKeyMatches(botID string, data *IndexData) []KeyMatch { TurnCount: m.TurnCount, Won: botPart.Won, EndCondition: m.EndCondition, + CriticalMoment: summarizeCriticalMoment(m, botPart, oppPart), }) if len(matches) >= 3 { @@ -911,6 +912,7 @@ func extractRivalryMatches(botAID, botBID string, data *IndexData) []KeyMatch { Score: fmt.Sprintf("%d-%d", botAPart.Score, botBPart.Score), TurnCount: m.TurnCount, Won: botAPart.Won, + CriticalMoment: summarizeCriticalMoment(m, botAPart, botBPart), }) if len(matches) >= 5 { @@ -921,6 +923,47 @@ func extractRivalryMatches(botAID, botBID string, data *IndexData) []KeyMatch { return matches } +// summarizeCriticalMoment generates a brief turning-point description from +// match data per plan §13.2. The index builder does not have access to the +// full replay JSON (stored on B2/R2), so this uses the score, turn count, +// end condition, and pre-match ELO to synthesize a contextual summary. +func summarizeCriticalMoment(m MatchData, winner, loser *ParticipantData) string { + scoreDelta := winner.Score - loser.Score + if scoreDelta < 0 { + scoreDelta = -scoreDelta + } + + parts := make([]string, 0, 3) + + // Close match indicator + if scoreDelta <= 1 { + parts = append(parts, "decided by a single point") + } + + // ELO upset indicator + if winner.PreMatchRating > 0 && loser.PreMatchRating > 0 { + eloDelta := loser.PreMatchRating - winner.PreMatchRating + if eloDelta >= 150 { + parts = append(parts, fmt.Sprintf("upset by %.0f ELO points", eloDelta)) + } + } + + // End condition context + if m.EndCondition != "" && m.EndCondition != "turn_limit" { + parts = append(parts, m.EndCondition) + } + + // Late-game drama + if m.TurnCount >= 400 { + parts = append(parts, "marathon match") + } + + if len(parts) == 0 { + return "" + } + return strings.Join(parts, ", ") +} + func getBotRank(botID string, data *IndexData) int { for i, bot := range data.Bots { if bot.ID == botID {