feat(narrative): add critical moment summaries to key match extraction

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 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-22 19:08:50 -04:00
parent fddcc0ba34
commit ae8eb0465e
2 changed files with 44 additions and 1 deletions

View file

@ -1 +1 @@
17dbef092717f4f1f81dc870be3f8025466740a0
fddcc0ba34f2ce3a57a17122826ef7963f8bf7d3

View file

@ -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 {