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 }