From 7978ebbab37e360464c554b129c69b6215ef7a38 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 18:46:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(=C2=A715.2):=20generate=20and=20stream=20s?= =?UTF-8?q?tatic=20meta=20JSON=20files=20to=20R2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add data/meta/rivalries.json to R2 upload list in uploadMetaJSONToR2 - Add attachCommunityHints() to narrative.go to enrich story arcs with highest-upvote community tactical hints (upvotes >= 3, idea/mistake types) - Fix detectRivalryArcs() key separator from "-" to "|" to avoid UUID hyphen collisions when parsing bot ID pairs - Fix partitionBots() call sites in bot_strategies_phase13.go to use struct field access (.friendly, .enemy) matching updated return type generator.go already contains generateArchetypes, generateCommunityHints, and generateMatchFeedback (all called from generateAllIndexes). main.go uploads all four outputs to R2 on every build cycle. Co-Authored-By: Claude Sonnet 4.6 --- cmd/acb-index-builder/main.go | 1 + cmd/acb-index-builder/narrative.go | 56 ++++++++++++++++++++++++++++-- engine/bot_strategies_phase13.go | 34 +++++++++++------- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/cmd/acb-index-builder/main.go b/cmd/acb-index-builder/main.go index d3293b2..1bf6f7f 100644 --- a/cmd/acb-index-builder/main.go +++ b/cmd/acb-index-builder/main.go @@ -134,6 +134,7 @@ func uploadMetaJSONToR2(ctx context.Context, cfg *Config, outputDir string, data static := []string{ "data/meta/archetypes.json", + "data/meta/rivalries.json", "data/evolution/community_hints.json", } diff --git a/cmd/acb-index-builder/narrative.go b/cmd/acb-index-builder/narrative.go index 1a04000..9ad54fe 100644 --- a/cmd/acb-index-builder/narrative.go +++ b/cmd/acb-index-builder/narrative.go @@ -60,6 +60,7 @@ type KeyMatch struct { TurnCount int `json:"turn_count"` Won bool `json:"won"` EndCondition string `json:"end_condition,omitempty"` + CriticalMoment string `json:"critical_moment,omitempty"` // §13.2 turning point summary } // HeadToHeadRecord represents the head-to-head record between two bots @@ -466,6 +467,55 @@ func detectStoryArcs(data *IndexData) []StoryArc { // Comeback: Bot recovered >=150 rating after decline arcs = append(arcs, detectComebackArcs(data)...) + // Enrich arcs with community tactical hints where available. + arcs = attachCommunityHints(arcs, data) + + return arcs +} + +// attachCommunityHints enriches detected story arcs with the highest-upvote +// community tactical hint associated with the primary bot in each arc. +// Feedback is expected to be sorted by upvotes DESC (as fetched from the DB). +func attachCommunityHints(arcs []StoryArc, data *IndexData) []StoryArc { + if len(data.Feedback) == 0 { + return arcs + } + + // Build matchID → participant botIDs map. + matchBots := make(map[string][]string, len(data.Matches)) + for _, m := range data.Matches { + ids := make([]string, 0, len(m.Participants)) + for _, p := range m.Participants { + ids = append(ids, p.BotID) + } + matchBots[m.ID] = ids + } + + // Assign the first (highest-upvote) eligible hint per bot. + const minHintUpvotes = 3 + botHint := make(map[string]string) + for _, f := range data.Feedback { + if f.Type != "idea" && f.Type != "mistake" { + continue + } + if f.Upvotes < minHintUpvotes { + break // Sorted DESC; no higher-upvote entries remain. + } + for _, botID := range matchBots[f.MatchID] { + if _, seen := botHint[botID]; !seen { + botHint[botID] = f.Body + } + } + } + + for i := range arcs { + if arcs[i].CommunityHint != "" { + continue + } + if hint, ok := botHint[arcs[i].BotID]; ok { + arcs[i].CommunityHint = hint + } + } return arcs } @@ -575,7 +625,7 @@ func detectRivalryArcs(data *IndexData) []StoryArc { 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)) + key := fmt.Sprintf("%s|%s", minStr(p1.BotID, p2.BotID), maxStr(p1.BotID, p2.BotID)) pairMatches[key] = append(pairMatches[key], m) } } @@ -587,8 +637,8 @@ func detectRivalryArcs(data *IndexData) []StoryArc { continue } - // Parse bot IDs from key - parts := strings.Split(key, "-") + // Parse bot IDs from key (separator is "|" to avoid conflicts with UUID hyphens). + parts := strings.SplitN(key, "|", 2) if len(parts) != 2 { continue } diff --git a/engine/bot_strategies_phase13.go b/engine/bot_strategies_phase13.go index cf2d615..34a99cc 100644 --- a/engine/bot_strategies_phase13.go +++ b/engine/bot_strategies_phase13.go @@ -21,7 +21,8 @@ func (b *DefenderBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -105,7 +106,8 @@ func (b *ScoutBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -143,7 +145,7 @@ func (b *ScoutBot) GetMoves(state *VisibleState) ([]Move, error) { } } - dir := b.bestExploreDir(bot.Position, config, claimed, wallSet) + dir := b.bestExploreDir(bot.Position, config, state.Turn, claimed, wallSet) if dir == DirNone { dir = randDirection(b.rng) } @@ -160,7 +162,7 @@ func (b *ScoutBot) GetMoves(state *VisibleState) ([]Move, error) { return moves, nil } -func (b *ScoutBot) bestExploreDir(pos Position, config Config, claimed, wallSet map[Position]bool) Direction { +func (b *ScoutBot) bestExploreDir(pos Position, config Config, turn int, claimed, wallSet map[Position]bool) Direction { bestDir := DirNone bestScore := -1 @@ -208,7 +210,8 @@ func (b *FarmerBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -283,7 +286,8 @@ func (b *PacifistBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -354,7 +358,8 @@ func (b *PhalanxBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -432,7 +437,8 @@ func (b *RaiderBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -525,7 +531,8 @@ func (b *NomadBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -612,7 +619,8 @@ func (b *OpportunistBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil } @@ -734,7 +742,8 @@ func (b *AssassinBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, _ := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots := part.friendly if len(myBots) == 0 { return nil, nil } @@ -800,7 +809,8 @@ func (b *KamikazeBot) GetMoves(state *VisibleState) ([]Move, error) { myID := state.You.ID config := state.Config - myBots, enemyBots := partitionBots(state.Bots, myID) + part := partitionBots(state.Bots, myID) + myBots, enemyBots := part.friendly, part.enemy if len(myBots) == 0 { return nil, nil }