feat(§15.2): generate and stream static meta JSON files to R2

- 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 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-22 18:46:27 -04:00
parent 60b83a02d9
commit 7978ebbab3
3 changed files with 76 additions and 15 deletions

View file

@ -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",
}

View file

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

View file

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