diff --git a/acb-map-evolver b/acb-map-evolver index d8b9c7b..b307208 100755 Binary files a/acb-map-evolver and b/acb-map-evolver differ diff --git a/bots/farmer/strategy.go b/bots/farmer/strategy.go index 381b7c5..15274f6 100644 --- a/bots/farmer/strategy.go +++ b/bots/farmer/strategy.go @@ -3,8 +3,8 @@ package main import "math" const ( - fleeRadius2 = 9 // flee if enemy within 3 cells (squared = 9) - dangerBuffer = 20 // extra buffer beyond attack radius for avoidance + fleeRadius2 = 9 // flee if enemy within 3 cells (squared = 9) + dangerBuffer = 20 // extra buffer beyond attack radius for avoidance ) // FarmerStrategy maximizes energy collection and spawn rate while avoiding combat. diff --git a/bots/farmer/strategy_test.go b/bots/farmer/strategy_test.go index d924018..2146672 100644 --- a/bots/farmer/strategy_test.go +++ b/bots/farmer/strategy_test.go @@ -213,9 +213,9 @@ func TestFarmerAvoidsWalls(t *testing.T) { func TestDistance2(t *testing.T) { tests := []struct { - a, b Position + a, b Position rows, cols int - want int + want int }{ {Position{0, 0}, Position{0, 0}, 20, 20, 0}, {Position{0, 0}, Position{0, 3}, 20, 20, 9}, @@ -258,10 +258,10 @@ func TestBFS(t *testing.T) { func TestSimulateMove(t *testing.T) { tests := []struct { - pos Position - dir string + pos Position + dir string rows, cols int - want Position + want Position }{ {Position{5, 5}, "N", 20, 20, Position{4, 5}}, {Position{5, 5}, "S", 20, 20, Position{6, 5}}, diff --git a/bots/gatherer/main.go b/bots/gatherer/main.go index c17c721..1fba9b8 100644 --- a/bots/gatherer/main.go +++ b/bots/gatherer/main.go @@ -61,11 +61,11 @@ type GameState struct { Energy int `json:"energy"` Score int `json:"score"` } `json:"you"` - Bots []VisibleBot `json:"bots"` - Energy []Position `json:"energy"` + Bots []VisibleBot `json:"bots"` + Energy []Position `json:"energy"` Cores []VisibleCore `json:"cores"` - Walls []Position `json:"walls"` - Dead []VisibleBot `json:"dead"` + Walls []Position `json:"walls"` + Dead []VisibleBot `json:"dead"` } // Direction represents a movement direction. @@ -91,9 +91,9 @@ type MoveResponse struct { // Server holds the bot server state. type Server struct { - config Config + config Config strategy *GathererStrategy - mu sync.Mutex + mu sync.Mutex } func main() { diff --git a/cmd/acb-api/alerts.go b/cmd/acb-api/alerts.go index e0a7d2f..df8bf5b 100644 --- a/cmd/acb-api/alerts.go +++ b/cmd/acb-api/alerts.go @@ -135,11 +135,11 @@ type slackPayload struct { } type slackAttachment struct { - Color string `json:"color"` - Title string `json:"title"` - Text string `json:"text"` - Footer string `json:"footer"` - Ts int64 `json:"ts"` + Color string `json:"color"` + Title string `json:"title"` + Text string `json:"text"` + Footer string `json:"footer"` + Ts int64 `json:"ts"` } func (a *Alerter) sendSlack(ctx context.Context, level AlertLevel, title, message string) error { diff --git a/cmd/acb-api/alerts_test.go b/cmd/acb-api/alerts_test.go index 6f7522d..198a12b 100644 --- a/cmd/acb-api/alerts_test.go +++ b/cmd/acb-api/alerts_test.go @@ -105,9 +105,9 @@ func TestAlerterSendSlack(t *testing.T) { func TestAlerterColorCodes(t *testing.T) { tests := []struct { - level AlertLevel - wantDiscord int - wantSlack string + level AlertLevel + wantDiscord int + wantSlack string }{ {AlertInfo, 0x3498db, "#3498db"}, {AlertWarning, 0xf39c12, "#f39c12"}, diff --git a/cmd/acb-api/main.go b/cmd/acb-api/main.go index 020e149..45feaa6 100644 --- a/cmd/acb-api/main.go +++ b/cmd/acb-api/main.go @@ -23,22 +23,22 @@ import ( ) type Config struct { - ListenAddr string - DatabaseURL string - ValkeyAddr string - ValkeyPassword string - WorkerAPIKey string // API key workers use to submit results - EncryptionKey string // AES-256-GCM key for shared secret encryption - DiscordWebhook string // Discord webhook URL for alerts - SlackWebhook string // Slack webhook URL for alerts - MatchmakerSecs int - HealthCheckSecs int - ReaperSecs int - BotTimeoutSecs int - StaleJobMinutes int - MaxConsecFails int - SpamBlockList string // Comma-separated list of blocked terms (env: ACB_SPAM_BLOCK_LIST) - SpamMinLength int // Minimum feedback content length (env: ACB_SPAM_MIN_LENGTH) + ListenAddr string + DatabaseURL string + ValkeyAddr string + ValkeyPassword string + WorkerAPIKey string // API key workers use to submit results + EncryptionKey string // AES-256-GCM key for shared secret encryption + DiscordWebhook string // Discord webhook URL for alerts + SlackWebhook string // Slack webhook URL for alerts + MatchmakerSecs int + HealthCheckSecs int + ReaperSecs int + BotTimeoutSecs int + StaleJobMinutes int + MaxConsecFails int + SpamBlockList string // Comma-separated list of blocked terms (env: ACB_SPAM_BLOCK_LIST) + SpamMinLength int // Minimum feedback content length (env: ACB_SPAM_MIN_LENGTH) } func loadConfig() Config { @@ -85,12 +85,12 @@ func main() { cfg: cfg, db: db, rdb: rdb, - regLimiter: ratelimit.NewLimiter(5, 5.0/3600), // 5/hour per IP - feedbackLtr: ratelimit.NewLimiter(20, 20.0/3600), // 20/hour per IP - predictLtr: ratelimit.NewLimiter(60, 60.0/3600), // 60/hour per IP - submitLtr: ratelimit.NewLimiter(5, 5.0/86400), // 5/day per key - enrichLtr: ratelimit.NewLimiter(5, 5.0/86400), // 5/day per bot - voteLtr: ratelimit.NewLimiter(10, 10.0/3600), // 10/hour per IP + regLimiter: ratelimit.NewLimiter(5, 5.0/3600), // 5/hour per IP + feedbackLtr: ratelimit.NewLimiter(20, 20.0/3600), // 20/hour per IP + predictLtr: ratelimit.NewLimiter(60, 60.0/3600), // 60/hour per IP + submitLtr: ratelimit.NewLimiter(5, 5.0/86400), // 5/day per key + enrichLtr: ratelimit.NewLimiter(5, 5.0/86400), // 5/day per bot + voteLtr: ratelimit.NewLimiter(10, 10.0/3600), // 10/hour per IP } // Initialize spam filter with configurable block-list diff --git a/cmd/acb-api/server.go b/cmd/acb-api/server.go index 71f8b85..e7ddb1c 100644 --- a/cmd/acb-api/server.go +++ b/cmd/acb-api/server.go @@ -25,16 +25,16 @@ import ( // Provides bot registration, job coordination, replay serving, // bot profiles, leaderboards, and UI feedback ingestion. type Server struct { - cfg Config - db *sql.DB - rdb *redis.Client - regLimiter *ratelimit.Limiter // 5/hour per IP - feedbackLtr *ratelimit.Limiter // 20/hour per IP - predictLtr *ratelimit.Limiter // 60/hour per IP - submitLtr *ratelimit.Limiter // 5/day per bot_id - enrichLtr *ratelimit.Limiter // 5/day per bot_id for enrichment requests - voteLtr *ratelimit.Limiter // 10/hour per IP - spamFilter *SpamFilter // word/spam filter for feedback + cfg Config + db *sql.DB + rdb *redis.Client + regLimiter *ratelimit.Limiter // 5/hour per IP + feedbackLtr *ratelimit.Limiter // 20/hour per IP + predictLtr *ratelimit.Limiter // 60/hour per IP + submitLtr *ratelimit.Limiter // 5/day per bot_id + enrichLtr *ratelimit.Limiter // 5/day per bot_id for enrichment requests + voteLtr *ratelimit.Limiter // 10/hour per IP + spamFilter *SpamFilter // word/spam filter for feedback } func (s *Server) RegisterRoutes(mux *http.ServeMux) { @@ -303,10 +303,10 @@ func (s *Server) handleGetJob(w http.ResponseWriter, r *http.Request) { // Parse config_json to get match details var config struct { - MapID string `json:"map_id"` - MapSeed int64 `json:"map_seed"` - BotIDs []string `json:"bot_ids"` - PlayerSlots []int `json:"player_slots"` + MapID string `json:"map_id"` + MapSeed int64 `json:"map_seed"` + BotIDs []string `json:"bot_ids"` + PlayerSlots []int `json:"player_slots"` } if err := json.Unmarshal(job.ConfigJSON, &config); err != nil { log.Printf("failed to parse job config: %v", err) @@ -366,15 +366,15 @@ func (s *Server) handleGetJob(w http.ResponseWriter, r *http.Request) { // Build response response := map[string]interface{}{ - "job_id": job.JobID, - "match_id": job.MatchID, - "map_id": config.MapID, - "map_seed": config.MapSeed, - "map_width": mapData.GridWidth, - "map_height": mapData.GridHeight, - "map_json": mapData.MapJSON, - "bots": bots, - "player_slots": config.PlayerSlots, + "job_id": job.JobID, + "match_id": job.MatchID, + "map_id": config.MapID, + "map_seed": config.MapSeed, + "map_width": mapData.GridWidth, + "map_height": mapData.GridHeight, + "map_json": mapData.MapJSON, + "bots": bots, + "player_slots": config.PlayerSlots, } writeJSON(w, http.StatusOK, response) @@ -786,19 +786,19 @@ func (s *Server) handleGetBot(w http.ResponseWriter, r *http.Request) { // Get bot details var bot struct { - BotID string `json:"bot_id"` - Name string `json:"name"` - Owner string `json:"owner"` - Status string `json:"status"` - RatingMu float64 `json:"rating_mu"` - RatingPhi float64 `json:"rating_phi"` - Evolved bool `json:"evolved"` - Island *string `json:"island,omitempty"` - Generation *int `json:"generation,omitempty"` + BotID string `json:"bot_id"` + Name string `json:"name"` + Owner string `json:"owner"` + Status string `json:"status"` + RatingMu float64 `json:"rating_mu"` + RatingPhi float64 `json:"rating_phi"` + Evolved bool `json:"evolved"` + Island *string `json:"island,omitempty"` + Generation *int `json:"generation,omitempty"` ParentIDs *string `json:"parent_ids,omitempty"` DebugPublic bool `json:"debug_public"` CreatedAt string `json:"created_at"` - LastActive *string `json:"last_active,omitempty"` + LastActive *string `json:"last_active,omitempty"` } err := s.db.QueryRowContext(ctx, ` @@ -884,8 +884,8 @@ func (s *Server) handleBotPatch(w http.ResponseWriter, r *http.Request) { botID := pathParts[0] var req struct { - DebugPublic *bool `json:"debug_public"` - APISecret string `json:"api_secret"` + DebugPublic *bool `json:"debug_public"` + APISecret string `json:"api_secret"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") @@ -1257,10 +1257,10 @@ func (s *Server) handlePredict(w http.ResponseWriter, r *http.Request) { } resp := map[string]interface{}{ - "id": predictionID, - "match_id": req.MatchID, - "predicted": req.BotID, - "predictor": req.Predictor, + "id": predictionID, + "match_id": req.MatchID, + "predicted": req.BotID, + "predictor": req.Predictor, } if req.Confidence != nil { resp["confidence"] = *req.Confidence @@ -1303,10 +1303,10 @@ func (s *Server) handleOpenPredictions(w http.ResponseWriter, r *http.Request) { defer rows.Close() type MatchPrediction struct { - MatchID string `json:"match_id"` - CreatedAt string `json:"created_at"` + MatchID string `json:"match_id"` + CreatedAt string `json:"created_at"` Participants []map[string]interface{} `json:"participants"` - YourPick *string `json:"your_pick,omitempty"` + YourPick *string `json:"your_pick,omitempty"` } var matches []MatchPrediction @@ -2079,15 +2079,15 @@ func (s *Server) enqueueForEnrichment(ctx context.Context, matchID string) error func estimateWaitTime(status string) int { switch status { case "pending": - return 300 // 5 minutes for new requests + return 300 // 5 minutes for new requests case "processing": - return 60 // 1 minute if already being processed + return 60 // 1 minute if already being processed case "completed": - return 0 // Already done + return 0 // Already done case "failed": - return -1 // Failed - will retry + return -1 // Failed - will retry default: - return 300 // Default to 5 minutes + return 300 // Default to 5 minutes } } diff --git a/cmd/acb-api/server_test.go b/cmd/acb-api/server_test.go index 8edc9d4..e06b211 100644 --- a/cmd/acb-api/server_test.go +++ b/cmd/acb-api/server_test.go @@ -18,11 +18,11 @@ func newTestServer() *Server { BotTimeoutSecs: 5, MaxConsecFails: 3, }, - regLimiter: ratelimit.NewLimiter(5, 5.0/3600), - feedbackLtr: ratelimit.NewLimiter(20, 20.0/3600), - predictLtr: ratelimit.NewLimiter(60, 60.0/3600), - submitLtr: ratelimit.NewLimiter(5, 5.0/86400), - voteLtr: ratelimit.NewLimiter(10, 10.0/3600), + regLimiter: ratelimit.NewLimiter(5, 5.0/3600), + feedbackLtr: ratelimit.NewLimiter(20, 20.0/3600), + predictLtr: ratelimit.NewLimiter(60, 60.0/3600), + submitLtr: ratelimit.NewLimiter(5, 5.0/86400), + voteLtr: ratelimit.NewLimiter(10, 10.0/3600), } } diff --git a/cmd/acb-api/spamfilter.go b/cmd/acb-api/spamfilter.go index 86ec641..cfd382f 100644 --- a/cmd/acb-api/spamfilter.go +++ b/cmd/acb-api/spamfilter.go @@ -10,7 +10,7 @@ import ( // It normalizes case and strips common unicode substitutions before matching. type SpamFilter struct { blockedTerms map[string]struct{} // normalized blocked terms - minLength int // minimum content length + minLength int // minimum content length } // Default embedded block-list of common spam/offensive terms. diff --git a/cmd/acb-api/spamfilter_test.go b/cmd/acb-api/spamfilter_test.go index 4acacc4..86773e4 100644 --- a/cmd/acb-api/spamfilter_test.go +++ b/cmd/acb-api/spamfilter_test.go @@ -44,7 +44,7 @@ func TestSpamFilter_BlockedTerms(t *testing.T) { {"blocked in middle", "this is a scam attempt", true}, {"case insensitive", "SPAM everywhere", true}, {"mixed case", "VIAGRA pills", true}, - {"substring not blocked", "spamming is okay", false}, // "spamming" != "spam" + {"substring not blocked", "spamming is okay", false}, // "spamming" != "spam" {"partial word not blocked", "this is spammy", false}, // "spammy" != "spam" } @@ -131,7 +131,7 @@ func TestSpamFilter_WordBoundaries(t *testing.T) { {"with space after", "ass ", true}, {"in middle", "this ass here", true}, {"with punctuation", "ass.", true}, - {"substring should not match", "this is classic", false}, // "ass" in "classic" + {"substring should not match", "this is classic", false}, // "ass" in "classic" {"substring should not match 2", "cassandra is cool", false}, // "ass" in "cassandra" {"casino exact", "casino", true}, {"casino plural", "casinos", false}, // different word @@ -155,11 +155,11 @@ func TestNormalize(t *testing.T) { expected string }{ {"ViAgRA", "viagra"}, - {"V1@GR@", "viagra"}, // 1→i, @→a - {"C451N0", "casino"}, // 4→a, 5→s, 0→o, 1→i - {"Test!", "testi"}, // !→i + {"V1@GR@", "viagra"}, // 1→i, @→a + {"C451N0", "casino"}, // 4→a, 5→s, 0→o, 1→i + {"Test!", "testi"}, // !→i {"Mixed CASE", "mixed case"}, - {"0wned", "owned"}, // 0→o + {"0wned", "owned"}, // 0→o } for _, tt := range tests { diff --git a/cmd/acb-enrichment/config.go b/cmd/acb-enrichment/config.go index 0d55b89..189d9a0 100644 --- a/cmd/acb-enrichment/config.go +++ b/cmd/acb-enrichment/config.go @@ -13,10 +13,10 @@ type Config struct { DatabaseName string // LLM - LLMBaseURL string - LLMAPIKey string - LLMModel string // Model to use for commentary (e.g., "gpt-4o-mini", "claude-3-haiku") - LLMMaxTokens int + LLMBaseURL string + LLMAPIKey string + LLMModel string // Model to use for commentary (e.g., "gpt-4o-mini", "claude-3-haiku") + LLMMaxTokens int LLMTemperature float64 // Rate limiting @@ -24,20 +24,20 @@ type Config struct { MaxConcurrentRequests int // Maximum parallel LLM requests // Storage (B2/R2) - B2BucketName string - B2AccessKeyID string + B2BucketName string + B2AccessKeyID string B2SecretAccessKey string - B2Endpoint string // S3-compatible endpoint URL + B2Endpoint string // S3-compatible endpoint URL - R2BucketName string - R2AccessKeyID string + R2BucketName string + R2AccessKeyID string R2SecretAccessKey string - R2Endpoint string + R2Endpoint string // Enrichment criteria MinTurnCount int // Minimum turn count to consider enrichment MinWinProbCrossings int // Minimum win probability crossings - UpsetThreshold float64 // Minimum rating difference for upset consideration + UpsetThreshold float64 // Minimum rating difference for upset consideration // Timing CycleInterval time.Duration // How often to run enrichment cycles diff --git a/cmd/acb-enrichment/internal/db/store.go b/cmd/acb-enrichment/internal/db/store.go index 85d222a..f02e06f 100644 --- a/cmd/acb-enrichment/internal/db/store.go +++ b/cmd/acb-enrichment/internal/db/store.go @@ -22,15 +22,15 @@ func NewStore(db *sql.DB) *Store { // Match represents a match from the database. type Match struct { - ID string - MapID string - Status string - Winner sql.NullInt32 - Condition sql.NullString - TurnCount sql.NullInt32 - ScoresJSON sql.NullString - CreatedAt time.Time - CompletedAt sql.NullTime + ID string + MapID string + Status string + Winner sql.NullInt32 + Condition sql.NullString + TurnCount sql.NullInt32 + ScoresJSON sql.NullString + CreatedAt time.Time + CompletedAt sql.NullTime CommentaryJSON sql.NullString // NULL if not yet enriched } @@ -54,15 +54,15 @@ type BotInfo struct { // CandidateMatch represents a match that may be enriched. type CandidateMatch struct { - MatchID string - TurnCount int - Winner int - Condition string - FinalScores []int - Players []PlayerData + MatchID string + TurnCount int + Winner int + Condition string + FinalScores []int + Players []PlayerData WinProbCrossings int - IsUpset bool - IsCloseFinish bool + IsUpset bool + IsCloseFinish bool } // PlayerData holds player info for enrichment. @@ -134,9 +134,9 @@ func (s *Store) FindCandidates(ctx context.Context, minTurns, minCrossings int, // Parse participants var participants []struct { - BotID string `json:"bot_id"` - PlayerSlot int `json:"player_slot"` - Name string `json:"name"` + BotID string `json:"bot_id"` + PlayerSlot int `json:"player_slot"` + Name string `json:"name"` RatingMu float64 `json:"rating_mu"` RatingPhi float64 `json:"rating_phi"` } diff --git a/cmd/acb-enrichment/internal/generator/generator.go b/cmd/acb-enrichment/internal/generator/generator.go index d3f2bda..283ddf9 100644 --- a/cmd/acb-enrichment/internal/generator/generator.go +++ b/cmd/acb-enrichment/internal/generator/generator.go @@ -94,15 +94,15 @@ func (g *Generator) enrichOne(ctx context.Context, match db.CandidateMatch) (boo // Build metadata metadata := llm.MatchMetadata{ - Players: make([]llm.PlayerInfo, len(match.Players)), - MapSize: fmt.Sprintf("%dx%d", 60, 60), // Could extract from replay - TurnCount: match.TurnCount, - Winner: match.Winner, - Condition: match.Condition, - FinalScores: match.FinalScores, - IsUpset: match.IsUpset, + Players: make([]llm.PlayerInfo, len(match.Players)), + MapSize: fmt.Sprintf("%dx%d", 60, 60), // Could extract from replay + TurnCount: match.TurnCount, + Winner: match.Winner, + Condition: match.Condition, + FinalScores: match.FinalScores, + IsUpset: match.IsUpset, IsCloseFinish: match.IsCloseFinish, - IsFeatured: true, // All selected matches are featured + IsFeatured: true, // All selected matches are featured } for i, p := range match.Players { @@ -148,7 +148,7 @@ func (g *Generator) enrichOne(ctx context.Context, match db.CandidateMatch) (boo // Store the result commentaryMap := map[string]interface{}{ - "match_id": match.MatchID, + "match_id": match.MatchID, "generated_at": time.Now().UTC().Format(time.RFC3339), "key_moments": commentary.KeyMoments, "summary": commentary.Summary, diff --git a/cmd/acb-enrichment/internal/llm/client.go b/cmd/acb-enrichment/internal/llm/client.go index 4b137a4..f291877 100644 --- a/cmd/acb-enrichment/internal/llm/client.go +++ b/cmd/acb-enrichment/internal/llm/client.go @@ -43,26 +43,26 @@ func NewClient(baseURL, apiKey, model string) *Client { // GenerateCommentaryRequest holds the parameters for generating commentary. type GenerateCommentaryRequest struct { - MatchID string - ReplayJSON string // Full replay JSON as string - Metadata MatchMetadata - KeyMoments []KeyMoment - WinProbData string // Sampled win probability data - MaxTokens int - Temperature float64 + MatchID string + ReplayJSON string // Full replay JSON as string + Metadata MatchMetadata + KeyMoments []KeyMoment + WinProbData string // Sampled win probability data + MaxTokens int + Temperature float64 } // MatchMetadata contains match information for commentary generation. type MatchMetadata struct { - Players []PlayerInfo - MapSize string // e.g. "60x60" - TurnCount int - Winner int - Condition string - FinalScores []int - IsUpset bool + Players []PlayerInfo + MapSize string // e.g. "60x60" + TurnCount int + Winner int + Condition string + FinalScores []int + IsUpset bool IsCloseFinish bool - IsFeatured bool + IsFeatured bool } // PlayerInfo describes a single player/bot in the match. @@ -91,7 +91,7 @@ type KeyMomentCommentary struct { Turn int `json:"turn"` Description string `json:"description"` Significance string `json:"significance"` // "high", "medium", "low" - Tags []string `json:"tags"` // e.g. ["combat", "core_capture", "turning_point"] + Tags []string `json:"tags"` // e.g. ["combat", "core_capture", "turning_point"] } // GenerateCommentary sends the replay data to the LLM and returns structured commentary. diff --git a/cmd/acb-enrichment/internal/selector/selector.go b/cmd/acb-enrichment/internal/selector/selector.go index 4ad8b23..4b81cef 100644 --- a/cmd/acb-enrichment/internal/selector/selector.go +++ b/cmd/acb-enrichment/internal/selector/selector.go @@ -12,19 +12,19 @@ import ( // Selector chooses which matches should be enriched. type Selector struct { - store *db.Store - minTurnCount int - minCrossings int - upsetThreshold float64 - maxPerHour int + store *db.Store + minTurnCount int + minCrossings int + upsetThreshold float64 + maxPerHour int } // Config holds selector configuration. type Config struct { - MinTurnCount int - MinCrossings int - UpsetThreshold float64 - MaxPerHour int + MinTurnCount int + MinCrossings int + UpsetThreshold float64 + MaxPerHour int } // DefaultConfig returns default selector configuration. @@ -53,11 +53,11 @@ func NewSelector(store *db.Store, cfg Config) *Selector { } return &Selector{ - store: store, - minTurnCount: cfg.MinTurnCount, - minCrossings: cfg.MinCrossings, - upsetThreshold: cfg.UpsetThreshold, - maxPerHour: cfg.MaxPerHour, + store: store, + minTurnCount: cfg.MinTurnCount, + minCrossings: cfg.MinCrossings, + upsetThreshold: cfg.UpsetThreshold, + maxPerHour: cfg.MaxPerHour, } } diff --git a/cmd/acb-enrichment/internal/storage/client.go b/cmd/acb-enrichment/internal/storage/client.go index 0fb9b33..4d0bc5d 100644 --- a/cmd/acb-enrichment/internal/storage/client.go +++ b/cmd/acb-enrichment/internal/storage/client.go @@ -15,10 +15,10 @@ import ( // Client is an S3-compatible storage client. type Client struct { - accessKey string - secretKey string - endpoint string - bucket string + accessKey string + secretKey string + endpoint string + bucket string httpClient *http.Client } diff --git a/cmd/acb-enrichment/service.go b/cmd/acb-enrichment/service.go index aaec951..bc8739c 100644 --- a/cmd/acb-enrichment/service.go +++ b/cmd/acb-enrichment/service.go @@ -15,14 +15,14 @@ import ( // EnrichmentService manages the AI replay enrichment process. type EnrichmentService struct { - db *sql.DB - cfg Config - store *dbstore.Store - selector *selector.Selector - generator *generator.Generator - r2Client *storage.Client - b2Client *storage.Client - llmClient *llm.Client + db *sql.DB + cfg Config + store *dbstore.Store + selector *selector.Selector + generator *generator.Generator + r2Client *storage.Client + b2Client *storage.Client + llmClient *llm.Client } // NewEnrichmentService creates a new enrichment service. diff --git a/cmd/acb-evolver/internal/arena/arena.go b/cmd/acb-evolver/internal/arena/arena.go index 0fe0ee4..88a1ebb 100644 --- a/cmd/acb-evolver/internal/arena/arena.go +++ b/cmd/acb-evolver/internal/arena/arena.go @@ -49,7 +49,7 @@ type BotRecord struct { BotID string Name string EndpointURL string - Secret string // plaintext (decrypted when encryption key is provided) + Secret string // plaintext (decrypted when encryption key is provided) RatingMu float64 } @@ -57,8 +57,8 @@ type BotRecord struct { type MatchOutcome struct { OpponentBotID string OpponentName string - CandidateSlot int // player slot (0 or 1) assigned to the candidate - Winner int // 0=player0, 1=player1, -1=draw + CandidateSlot int // player slot (0 or 1) assigned to the candidate + Winner int // 0=player0, 1=player1, -1=draw Scores []int Turns int Err error diff --git a/cmd/acb-evolver/internal/arena/winrate.go b/cmd/acb-evolver/internal/arena/winrate.go index a281dcc..6d0419c 100644 --- a/cmd/acb-evolver/internal/arena/winrate.go +++ b/cmd/acb-evolver/internal/arena/winrate.go @@ -4,11 +4,11 @@ import "math" // WinRateResult holds the observed win rate and its 95% Wilson score confidence interval. type WinRateResult struct { - Wins int - Total int // non-error matches only - Rate float64 // observed win rate (0–1) - Lower float64 // 95% CI lower bound - Upper float64 // 95% CI upper bound + Wins int + Total int // non-error matches only + Rate float64 // observed win rate (0–1) + Lower float64 // 95% CI lower bound + Upper float64 // 95% CI upper bound } // WinRate computes the win rate and Wilson score 95% confidence interval diff --git a/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go b/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go index 7d41ea5..c616515 100644 --- a/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go +++ b/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go @@ -19,7 +19,7 @@ type mockStore struct { mu sync.Mutex programs []*evolverdb.Program nextID int64 - createdCalls [] *evolverdb.Program // captures programs passed to Create + createdCalls []*evolverdb.Program // captures programs passed to Create } func newMockStore(programs ...*evolverdb.Program) *mockStore { diff --git a/cmd/acb-evolver/internal/db/validation_log.go b/cmd/acb-evolver/internal/db/validation_log.go index 6386873..c3356fc 100644 --- a/cmd/acb-evolver/internal/db/validation_log.go +++ b/cmd/acb-evolver/internal/db/validation_log.go @@ -11,12 +11,12 @@ import ( // computed per island and per language. type ValidationLog struct { ID int64 - Island string // one of IslandAlpha … IslandDelta - Language string // e.g. "go", "python" - Stage string // last stage attempted: "syntax", "schema", or "sandbox" - Passed bool // true when all stages up to (and including) Stage passed - ErrorText string // human-readable failure reason (empty on pass) - LLMOutput string // raw LLM response, for retry / learning + Island string // one of IslandAlpha … IslandDelta + Language string // e.g. "go", "python" + Stage string // last stage attempted: "syntax", "schema", or "sandbox" + Passed bool // true when all stages up to (and including) Stage passed + ErrorText string // human-readable failure reason (empty on pass) + LLMOutput string // raw LLM response, for retry / learning CreatedAt time.Time } @@ -62,11 +62,11 @@ func (s *Store) IslandPassRates(ctx context.Context) (map[string]float64, error) // ValidationStats holds aggregate metrics for one island. type ValidationStats struct { - Island string - Total int - Passed int - PassRate float64 - ByStage map[string]int // count of runs that FAILED at each stage + Island string + Total int + Passed int + PassRate float64 + ByStage map[string]int // count of runs that FAILED at each stage } // IslandValidationStats returns per-island validation statistics including diff --git a/cmd/acb-evolver/internal/live/cycle.go b/cmd/acb-evolver/internal/live/cycle.go index 8a5fdc2..a06244a 100644 --- a/cmd/acb-evolver/internal/live/cycle.go +++ b/cmd/acb-evolver/internal/live/cycle.go @@ -10,18 +10,18 @@ import ( // CycleState tracks the current evolution cycle status in real-time. // This is updated throughout the cycle and exported to live.json. type CycleState struct { - mu sync.RWMutex - Generation int - StartedAt time.Time - Phase string // generating, validating, evaluating, promoting, idle - CandidateID string - CandidateIsland string - CandidateLang string - ParentIDs []string - Validation *CycleValidation - Evaluation *CycleEvaluation - PromotionReason string // Set when promoted/rejected - CommunityHint string // Community hint that influenced this candidate + mu sync.RWMutex + Generation int + StartedAt time.Time + Phase string // generating, validating, evaluating, promoting, idle + CandidateID string + CandidateIsland string + CandidateLang string + ParentIDs []string + Validation *CycleValidation + Evaluation *CycleEvaluation + PromotionReason string // Set when promoted/rejected + CommunityHint string // Community hint that influenced this candidate } // CycleValidation tracks validation stage progress. @@ -197,7 +197,7 @@ func (c *CycleState) SetArenaResult(wins, losses, draws, errors int, winRate flo defer c.mu.Unlock() if c.Evaluation == nil { c.Evaluation = &CycleEvaluation{ - MatchesTotal: wins + losses + draws + errors, + MatchesTotal: wins + losses + draws + errors, Results: make([]CycleMatchResult, 0), } } @@ -261,17 +261,17 @@ func (c *CycleState) ToCycleInfo() *CycleInfo { if c.Validation != nil { info.Candidate.Validation = &ValidationStatus{ Syntax: &StageResult{ - Passed: c.Validation.SyntaxPassed, - TimeMs: c.Validation.SyntaxTimeMs, - Error: c.Validation.LastError, + Passed: c.Validation.SyntaxPassed, + TimeMs: c.Validation.SyntaxTimeMs, + Error: c.Validation.LastError, }, Schema: &StageResult{ - Passed: c.Validation.SchemaPassed, - TimeMs: c.Validation.SchemaTimeMs, + Passed: c.Validation.SchemaPassed, + TimeMs: c.Validation.SchemaTimeMs, }, Smoke: &StageResult{ - Passed: c.Validation.SmokePassed, - TimeMs: c.Validation.SmokeTimeMs, + Passed: c.Validation.SmokePassed, + TimeMs: c.Validation.SmokeTimeMs, }, } } diff --git a/cmd/acb-evolver/internal/live/exporter.go b/cmd/acb-evolver/internal/live/exporter.go index ad9544d..2f6d09b 100644 --- a/cmd/acb-evolver/internal/live/exporter.go +++ b/cmd/acb-evolver/internal/live/exporter.go @@ -69,17 +69,17 @@ type CycleInfo struct { // Candidate represents the current candidate being evaluated. type Candidate struct { - ID string `json:"id"` // e.g., "go-847-3" - Island string `json:"island"` - Language string `json:"language"` - Parents []ParentInfo `json:"parents"` - Validation *ValidationStatus `json:"validation,omitempty"` - Evaluation *EvaluationStatus `json:"evaluation,omitempty"` + ID string `json:"id"` // e.g., "go-847-3" + Island string `json:"island"` + Language string `json:"language"` + Parents []ParentInfo `json:"parents"` + Validation *ValidationStatus `json:"validation,omitempty"` + Evaluation *EvaluationStatus `json:"evaluation,omitempty"` } // ParentInfo holds parent bot information. type ParentInfo struct { - ID string `json:"id"` // e.g., "go-831-1" + ID string `json:"id"` // e.g., "go-831-1" Rating int `json:"rating"` } @@ -99,55 +99,55 @@ type StageResult struct { // EvaluationStatus holds arena evaluation results. type EvaluationStatus struct { - MatchesTotal int `json:"matches_total"` - MatchesPlayed int `json:"matches_played"` - Results []MatchResult `json:"results"` + MatchesTotal int `json:"matches_total"` + MatchesPlayed int `json:"matches_played"` + Results []MatchResult `json:"results"` } // MatchResult is a single evaluation match result. type MatchResult struct { Opponent string `json:"opponent"` // opponent bot name Won bool `json:"won"` - Score string `json:"score"` // e.g., "5-1" + Score string `json:"score"` // e.g., "5-1" } // ActivityEntry is a single event in the recent activity feed. type ActivityEntry struct { - Time string `json:"time"` - Generation int `json:"generation"` - Candidate string `json:"candidate"` - Island string `json:"island"` - Result string `json:"result"` // promoted, rejected - Reason string `json:"reason"` - Stage string `json:"stage"` // validation, promotion, deployment - BotID string `json:"bot_id,omitempty"` - InitialRating int `json:"initial_rating,omitempty"` + Time string `json:"time"` + Generation int `json:"generation"` + Candidate string `json:"candidate"` + Island string `json:"island"` + Result string `json:"result"` // promoted, rejected + Reason string `json:"reason"` + Stage string `json:"stage"` // validation, promotion, deployment + BotID string `json:"bot_id,omitempty"` + InitialRating int `json:"initial_rating,omitempty"` } // Totals holds overall evolution statistics. type Totals struct { - GenerationsTotal int `json:"generations_total"` - CandidatesToday int `json:"candidates_today"` - PromotedToday int `json:"promoted_today"` - PromotionRate7d float64 `json:"promotion_rate_7d"` - HighestEvolvedRating int `json:"highest_evolved_rating"` - EvolvedInTop10 int `json:"evolved_in_top_10"` - MutationsPerHour float64 `json:"mutations_per_hour"` + GenerationsTotal int `json:"generations_total"` + CandidatesToday int `json:"candidates_today"` + PromotedToday int `json:"promoted_today"` + PromotionRate7d float64 `json:"promotion_rate_7d"` + HighestEvolvedRating int `json:"highest_evolved_rating"` + EvolvedInTop10 int `json:"evolved_in_top_10"` + MutationsPerHour float64 `json:"mutations_per_hour"` } // LiveData is the full evolution dashboard payload written to live.json (plan §14 format). type LiveData struct { - UpdatedAt string `json:"updated_at"` - Cycle *CycleInfo `json:"cycle,omitempty"` - RecentActivity []ActivityEntry `json:"recent_activity,omitempty"` - Islands map[string]IslandStat `json:"islands"` - Totals Totals `json:"totals"` + UpdatedAt string `json:"updated_at"` + Cycle *CycleInfo `json:"cycle,omitempty"` + RecentActivity []ActivityEntry `json:"recent_activity,omitempty"` + Islands map[string]IslandStat `json:"islands"` + Totals Totals `json:"totals"` // Legacy fields for backward compatibility - TotalPrograms int `json:"total_programs,omitempty"` - PromotedCount int `json:"promoted_count,omitempty"` - GenerationLog []GenerationEntry `json:"generation_log,omitempty"` - Lineage []LineageNode `json:"lineage,omitempty"` - MetaSnapshots []MetaSnapshot `json:"meta_snapshots,omitempty"` + TotalPrograms int `json:"total_programs,omitempty"` + PromotedCount int `json:"promoted_count,omitempty"` + GenerationLog []GenerationEntry `json:"generation_log,omitempty"` + Lineage []LineageNode `json:"lineage,omitempty"` + MetaSnapshots []MetaSnapshot `json:"meta_snapshots,omitempty"` } // Export queries the programs database and builds the current evolution state. @@ -354,14 +354,14 @@ func fillRecentActivity(ctx context.Context, db *sql.DB, data *LiveData) error { continue } activities = append(activities, ActivityEntry{ - Time: createdAt.UTC().Format(time.RFC3339), + Time: createdAt.UTC().Format(time.RFC3339), Generation: generation, - Candidate: botName, - Island: island, - Result: "promoted", - Reason: "Passed promotion gate", - Stage: "deployment", - BotID: botID, + Candidate: botName, + Island: island, + Result: "promoted", + Reason: "Passed promotion gate", + Stage: "deployment", + BotID: botID, }) } data.RecentActivity = activities diff --git a/cmd/acb-evolver/internal/mapelites/grid.go b/cmd/acb-evolver/internal/mapelites/grid.go index a70e0f0..001307e 100644 --- a/cmd/acb-evolver/internal/mapelites/grid.go +++ b/cmd/acb-evolver/internal/mapelites/grid.go @@ -157,9 +157,9 @@ type GridSnapshot struct { // CellSnapshot is one occupied cell in the grid snapshot. type CellSnapshot struct { - Pos [NumDims]int `json:"pos"` - Program int64 `json:"program_id"` - Fitness float64 `json:"fitness"` + Pos [NumDims]int `json:"pos"` + Program int64 `json:"program_id"` + Fitness float64 `json:"fitness"` } // Snapshot returns a JSON-serializable representation of the grid. diff --git a/cmd/acb-evolver/internal/mapelites/grid_test.go b/cmd/acb-evolver/internal/mapelites/grid_test.go index d0450a2..ec7cb07 100644 --- a/cmd/acb-evolver/internal/mapelites/grid_test.go +++ b/cmd/acb-evolver/internal/mapelites/grid_test.go @@ -7,7 +7,7 @@ func TestBehaviorToCell(t *testing.T) { g := New(3) cases := []struct { - agg, eco, expl, form float64 + agg, eco, expl, form float64 wantX, wantY, wantZ, wantW int }{ {0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0}, @@ -152,10 +152,10 @@ func TestSeedBehaviorVectors(t *testing.T) { g := New(3) bots := []struct { - id int64 - name string - aggression, economy float64 - exploration, formation float64 + id int64 + name string + aggression, economy float64 + exploration, formation float64 }{ {1, "gatherer", 0.1, 0.9, 0.3, 0.2}, {2, "guardian", 0.2, 0.6, 0.1, 0.8}, diff --git a/cmd/acb-evolver/internal/promoter/promoter.go b/cmd/acb-evolver/internal/promoter/promoter.go index 663207c..4e1eaf7 100644 --- a/cmd/acb-evolver/internal/promoter/promoter.go +++ b/cmd/acb-evolver/internal/promoter/promoter.go @@ -262,9 +262,10 @@ type RetiredCandidate struct { } // EnforcePolicy auto-retires evolved bots that meet any of these criteria: -// 1. Display rating below cfg.RatingThreshold (bottom 10%) -// 2. 7 consecutive days below rating threshold (per rating_history) -// 3. Population cap exceeded (cfg.PopCap) +// 1. Display rating below cfg.RatingThreshold (bottom 10%) +// 2. 7 consecutive days below rating threshold (per rating_history) +// 3. Population cap exceeded (cfg.PopCap) +// // The slice is ordered lowest-rated first so the weakest bots are retired // first when enforcing the cap. func (p *Promoter) EnforcePolicy(ctx context.Context) ([]RetiredCandidate, error) { @@ -910,8 +911,8 @@ func (p *Promoter) getWorkflowStatus(ctx context.Context, wfName string) (status var wfResp struct { Status struct { - Phase string `json:"phase"` - StartedAt string `json:"startedAt"` + Phase string `json:"phase"` + StartedAt string `json:"startedAt"` FinishedAt string `json:"finishedAt"` } `json:"status"` } diff --git a/cmd/acb-evolver/internal/promoter/promoter_test.go b/cmd/acb-evolver/internal/promoter/promoter_test.go index c95b89f..16a1f75 100644 --- a/cmd/acb-evolver/internal/promoter/promoter_test.go +++ b/cmd/acb-evolver/internal/promoter/promoter_test.go @@ -153,7 +153,9 @@ func TestManifestTemplates_Execute(t *testing.T) { SecretBase64: "dGVzdA==", } - for name, tmpl := range map[string]interface{ Execute(interface{}, interface{}) error }{} { + for name, tmpl := range map[string]interface { + Execute(interface{}, interface{}) error + }{} { _ = name _ = tmpl } diff --git a/cmd/acb-evolver/internal/prompt/convert.go b/cmd/acb-evolver/internal/prompt/convert.go index be773c9..24f9109 100644 --- a/cmd/acb-evolver/internal/prompt/convert.go +++ b/cmd/acb-evolver/internal/prompt/convert.go @@ -106,11 +106,11 @@ func BuildRequest( generation int, ) Request { return Request{ - Parents: parents, - Replays: FromReplayAnalyses(analyses), - Meta: FromMetaDescription(metaDesc), - Island: island, - TargetLang: targetLang, - Generation: generation, + Parents: parents, + Replays: FromReplayAnalyses(analyses), + Meta: FromMetaDescription(metaDesc), + Island: island, + TargetLang: targetLang, + Generation: generation, } } diff --git a/cmd/acb-evolver/internal/replay/analyzer.go b/cmd/acb-evolver/internal/replay/analyzer.go index e3d475c..81d8148 100644 --- a/cmd/acb-evolver/internal/replay/analyzer.go +++ b/cmd/acb-evolver/internal/replay/analyzer.go @@ -48,10 +48,10 @@ func (a *Analyzer) Analyze(replay *engine.Replay) *Analysis { } analysis := &Analysis{ - MatchID: replay.MatchID, - TurnCount: len(replay.Turns), - Scores: make([]int, 0), - Condition: "", + MatchID: replay.MatchID, + TurnCount: len(replay.Turns), + Scores: make([]int, 0), + Condition: "", } // Extract result information diff --git a/cmd/acb-evolver/internal/replay/analyzer_test.go b/cmd/acb-evolver/internal/replay/analyzer_test.go index 53344f5..f502ccc 100644 --- a/cmd/acb-evolver/internal/replay/analyzer_test.go +++ b/cmd/acb-evolver/internal/replay/analyzer_test.go @@ -24,11 +24,11 @@ func TestAnalyzer_Analyze_BasicMatch(t *testing.T) { StartTime: time.Now(), EndTime: time.Now(), Result: &engine.MatchResult{ - Winner: 0, - Reason: "dominance", - Turns: 150, - Scores: []int{120, 45}, - Energy: []int{15, 8}, + Winner: 0, + Reason: "dominance", + Turns: 150, + Scores: []int{120, 45}, + Energy: []int{15, 8}, BotsAlive: []int{8, 2}, }, Players: []engine.ReplayPlayer{ @@ -96,10 +96,10 @@ func TestAnalyzer_Analyze_EliminationMatch(t *testing.T) { replay := &engine.Replay{ MatchID: "elimination-match", Result: &engine.MatchResult{ - Winner: 1, - Reason: "elimination", - Turns: 75, - Scores: []int{10, 85}, + Winner: 1, + Reason: "elimination", + Turns: 75, + Scores: []int{10, 85}, BotsAlive: []int{0, 6}, }, Players: []engine.ReplayPlayer{ @@ -174,10 +174,10 @@ func TestAnalyzer_Analyze_DrawMatch(t *testing.T) { replay := &engine.Replay{ MatchID: "draw-match", Result: &engine.MatchResult{ - Winner: -1, - Reason: "draw", - Turns: 500, - Scores: []int{100, 100}, + Winner: -1, + Reason: "draw", + Turns: 500, + Scores: []int{100, 100}, }, Players: []engine.ReplayPlayer{ {ID: 0, Name: "Bot1"}, @@ -210,10 +210,10 @@ func TestAnalyzer_Analyze_WithEvents(t *testing.T) { replay := &engine.Replay{ MatchID: "eventful-match", Result: &engine.MatchResult{ - Winner: 0, - Reason: "dominance", - Turns: 200, - Scores: []int{200, 50}, + Winner: 0, + Reason: "dominance", + Turns: 200, + Scores: []int{200, 50}, }, Players: []engine.ReplayPlayer{ {ID: 0, Name: "Aggressor"}, diff --git a/cmd/acb-evolver/internal/validator/sandbox.go b/cmd/acb-evolver/internal/validator/sandbox.go index ed09d79..2887904 100644 --- a/cmd/acb-evolver/internal/validator/sandbox.go +++ b/cmd/acb-evolver/internal/validator/sandbox.go @@ -117,7 +117,7 @@ func makeBotCmd(ctx context.Context, execPath string, execArgs []string, dir str // receive requests from the test loop running in the same network namespace. func buildNsjailCmd(ctx context.Context, nsjailBin, execPath string, execArgs []string, dir string, env []string) *exec.Cmd { args := []string{ - "--mode", "o", // single-shot: run one command then exit + "--mode", "o", // single-shot: run one command then exit "--time_limit", "30", // 30-second wall-clock limit "--rlimit_as", "512", // 512 MiB virtual address space "--rlimit_cpu", "15", // 15 CPU seconds @@ -333,15 +333,15 @@ func signSmokeRequest(secret, matchID string, turn int, body []byte) string { // ── Test state types ────────────────────────────────────────────────────── type smokeState struct { - MatchID string `json:"match_id"` - Turn int `json:"turn"` - Config smokeConfig `json:"config"` - You smokePlayer `json:"you"` - Bots []smokeBot `json:"bots"` - Energy []smokePos `json:"energy"` - Cores []smokeCore `json:"cores"` - Walls []smokePos `json:"walls"` - Dead []smokeBot `json:"dead"` + MatchID string `json:"match_id"` + Turn int `json:"turn"` + Config smokeConfig `json:"config"` + You smokePlayer `json:"you"` + Bots []smokeBot `json:"bots"` + Energy []smokePos `json:"energy"` + Cores []smokeCore `json:"cores"` + Walls []smokePos `json:"walls"` + Dead []smokeBot `json:"dead"` } type smokeConfig struct { diff --git a/cmd/acb-evolver/main.go b/cmd/acb-evolver/main.go index a11f261..732329b 100644 --- a/cmd/acb-evolver/main.go +++ b/cmd/acb-evolver/main.go @@ -25,8 +25,8 @@ import ( _ "github.com/lib/pq" - evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/arena" + evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/live" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/llm" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/mapelites" @@ -370,10 +370,10 @@ func runEvaluate(ctx context.Context, db *sql.DB, args []string) { for _, pp := range promoted { if len(pp.BehaviorVector) >= 2 { expl, form := 0.5, 0.5 - if len(pp.BehaviorVector) >= 4 { - expl, form = pp.BehaviorVector[2], pp.BehaviorVector[3] - } - grid.TryPlace(pp.ProgramID, pp.Fitness, pp.BehaviorVector[0], pp.BehaviorVector[1], expl, form) + if len(pp.BehaviorVector) >= 4 { + expl, form = pp.BehaviorVector[2], pp.BehaviorVector[3] + } + grid.TryPlace(pp.ProgramID, pp.Fitness, pp.BehaviorVector[0], pp.BehaviorVector[1], expl, form) } } } @@ -517,9 +517,9 @@ func runRetire(ctx context.Context, db *sql.DB, args []string) { } defer rows.Close() type row struct { - programID int64 + programID int64 botID, botName string - displayRating float64 + displayRating float64 } var bots []row for rows.Next() { diff --git a/cmd/acb-evolver/run.go b/cmd/acb-evolver/run.go index 9f76e9e..d170852 100644 --- a/cmd/acb-evolver/run.go +++ b/cmd/acb-evolver/run.go @@ -32,9 +32,9 @@ import ( _ "github.com/lib/pq" - evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/arena" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/crosspoll" + evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/live" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/llm" "github.com/aicodebattle/acb/cmd/acb-evolver/internal/mapelites" @@ -49,10 +49,10 @@ import ( // RunConfig holds configuration for the autonomous evolution loop. type RunConfig struct { // Evolution parameters - NumParents int // number of parents for tournament selection - TournamentK int // tournament size - MaxRetries int // max LLM retries on validation failure - TopBotLimit int // number of top bots for meta description + NumParents int // number of parents for tournament selection + TournamentK int // tournament size + MaxRetries int // max LLM retries on validation failure + TopBotLimit int // number of top bots for meta description // Gate thresholds NashThreshold float64 // Nash value threshold for promotion @@ -63,8 +63,8 @@ type RunConfig struct { PopCap int // max evolved bots in fleet // Timing - CycleInterval time.Duration // delay between cycles (0 = continuous) - IslandCooldown time.Duration // min time between same-island evolutions + CycleInterval time.Duration // delay between cycles (0 = continuous) + IslandCooldown time.Duration // min time between same-island evolutions RetirementCheckInterval time.Duration // interval between periodic retirement checks // Infrastructure @@ -78,8 +78,8 @@ type RunConfig struct { UploadR2 bool // Declarative config for K8s manifests (§10.8) - DeclarativeConfigRepo string // git repo URL for K8s manifests - DeclarativeConfigBranch string // git branch for K8s manifests + DeclarativeConfigRepo string // git repo URL for K8s manifests + DeclarativeConfigBranch string // git branch for K8s manifests // Languages to evolve (in priority order) Languages []string @@ -107,29 +107,29 @@ type WeeklySchedule struct { // DefaultRunConfig returns production-ready defaults. func DefaultRunConfig() RunConfig { return RunConfig{ - NumParents: 2, - TournamentK: 3, - MaxRetries: 2, - TopBotLimit: 10, - NashThreshold: 0.50, - WinRateLowerBound: 0.40, + NumParents: 2, + TournamentK: 3, + MaxRetries: 2, + TopBotLimit: 10, + NashThreshold: 0.50, + WinRateLowerBound: 0.40, RatingThreshold: 800.0, - PopCap: 50, - CycleInterval: 5 * time.Minute, + PopCap: 50, + CycleInterval: 5 * time.Minute, RetirementCheckInterval: 24 * time.Hour, - IslandCooldown: 2 * time.Minute, - LLMURL: envOrDefault("ACB_LLM_URL", "http://zai-proxy-apexalgo.tail1b1987.ts.net:8080"), - RepoDir: envOrDefault("ACB_REPO_DIR", "."), - Registry: envOrDefault("ACB_REGISTRY", "forgejo.ardenone.com/ai-code-battle"), - KubectlServer: envOrDefault("ACB_KUBECTL_SERVER", "http://kubectl-ardenone-cluster:8001"), - EncryptionKey: os.Getenv("ACB_ENCRYPTION_KEY"), - UseNsjail: true, - LiveExportPath: envOrDefault("ACB_EVOLUTION_OUT", "evolution/live.json"), - UploadR2: envOrDefault("ACB_R2_UPLOAD_ENABLED", "false") == "true", - DeclarativeConfigRepo: envOrDefault("ACB_DECLARATIVE_CONFIG_REPO", "https://forgejo.ardenone.com/infra/ardenone-cluster.git"), - DeclarativeConfigBranch: envOrDefault("ACB_DECLARATIVE_CONFIG_BRANCH", "main"), - Languages: []string{"go", "python", "rust", "typescript", "java", "php"}, - MapEvolutionEnabled: envOrDefault("ACB_MAP_EVOLUTION_ENABLED", "false") == "true", + IslandCooldown: 2 * time.Minute, + LLMURL: envOrDefault("ACB_LLM_URL", "http://zai-proxy-apexalgo.tail1b1987.ts.net:8080"), + RepoDir: envOrDefault("ACB_REPO_DIR", "."), + Registry: envOrDefault("ACB_REGISTRY", "forgejo.ardenone.com/ai-code-battle"), + KubectlServer: envOrDefault("ACB_KUBECTL_SERVER", "http://kubectl-ardenone-cluster:8001"), + EncryptionKey: os.Getenv("ACB_ENCRYPTION_KEY"), + UseNsjail: true, + LiveExportPath: envOrDefault("ACB_EVOLUTION_OUT", "evolution/live.json"), + UploadR2: envOrDefault("ACB_R2_UPLOAD_ENABLED", "false") == "true", + DeclarativeConfigRepo: envOrDefault("ACB_DECLARATIVE_CONFIG_REPO", "https://forgejo.ardenone.com/infra/ardenone-cluster.git"), + DeclarativeConfigBranch: envOrDefault("ACB_DECLARATIVE_CONFIG_BRANCH", "main"), + Languages: []string{"go", "python", "rust", "typescript", "java", "php"}, + MapEvolutionEnabled: envOrDefault("ACB_MAP_EVOLUTION_ENABLED", "false") == "true", MapEvolutionSchedule: WeeklySchedule{ Weekday: time.Sunday, // Default: Sunday 03:00 UTC Hour: 3, @@ -499,32 +499,32 @@ func runCycle(ctx context.Context, db *sql.DB, store *evolverdb.Store, valCfg.UseNsjail = cfg.UseNsjail report, err = validator.Validate(ctx, code, lang, result.Best.Code, valCfg) - cycleState.SetPhase("validating") - exportLiveQuiet(ctx, db, cfg, cycleState) + cycleState.SetPhase("validating") + exportLiveQuiet(ctx, db, cfg, cycleState) if err != nil { - cycleState.SetValidationError("infrastructure", err.Error()) + cycleState.SetValidationError("infrastructure", err.Error()) log.Printf("Validation infrastructure error: %v", err) store.Delete(ctx, programID) programID = 0 continue } - // Track validation results in cycle state - for _, stage := range report.Stages { - timeMs := int(stage.Duration.Milliseconds()) - switch stage.Stage { - case "syntax": - cycleState.SetValidationSyntax(stage.Passed, timeMs) - case "schema": - cycleState.SetValidationSchema(stage.Passed, timeMs) - case "smoke": - cycleState.SetValidationSmoke(stage.Passed, timeMs) - } - if !stage.Passed && stage.Error != "" { - cycleState.SetValidationError(string(stage.Stage), stage.Error) - } + // Track validation results in cycle state + for _, stage := range report.Stages { + timeMs := int(stage.Duration.Milliseconds()) + switch stage.Stage { + case "syntax": + cycleState.SetValidationSyntax(stage.Passed, timeMs) + case "schema": + cycleState.SetValidationSchema(stage.Passed, timeMs) + case "smoke": + cycleState.SetValidationSmoke(stage.Passed, timeMs) } - exportLiveQuiet(ctx, db, cfg, cycleState) + if !stage.Passed && stage.Error != "" { + cycleState.SetValidationError(string(stage.Stage), stage.Error) + } + } + exportLiveQuiet(ctx, db, cfg, cycleState) // Log validation result valLog := &evolverdb.ValidationLog{ Island: island, diff --git a/cmd/acb-index-builder/blog.go b/cmd/acb-index-builder/blog.go index eee64c4..babf280 100644 --- a/cmd/acb-index-builder/blog.go +++ b/cmd/acb-index-builder/blog.go @@ -45,16 +45,16 @@ type BlogEntry struct { // WeeklyChronicle represents the weekly aggregated chronicle file // per plan §15.5 - written to data/blog/chronicles-YYYY-WW.json type WeeklyChronicle struct { - Year int `json:"year"` - WeekNumber int `json:"week_number"` - GeneratedAt string `json:"generated_at"` - SeasonName string `json:"season_name"` - StoryArcs []StoryArc `json:"story_arcs"` - Narrative string `json:"narrative"` - MatchCount int `json:"match_count"` - BotCount int `json:"bot_count"` - TopBotName string `json:"top_bot_name"` - TopBotRating float64 `json:"top_bot_rating"` + Year int `json:"year"` + WeekNumber int `json:"week_number"` + GeneratedAt string `json:"generated_at"` + SeasonName string `json:"season_name"` + StoryArcs []StoryArc `json:"story_arcs"` + Narrative string `json:"narrative"` + MatchCount int `json:"match_count"` + BotCount int `json:"bot_count"` + TopBotName string `json:"top_bot_name"` + TopBotRating float64 `json:"top_bot_rating"` } // generateBlog creates blog posts and the blog index. @@ -182,14 +182,14 @@ func recordMetaReportGenerated(postsDir string) { // ─── ELO mover tracking ────────────────────────────────────────────────────── type eloMover struct { - BotID string - BotName string - OldRating float64 - NewRating float64 - Delta float64 - Evolved bool - Archetype string - MatchesWon int + BotID string + BotName string + OldRating float64 + NewRating float64 + Delta float64 + Evolved bool + Archetype string + MatchesWon int MatchesLost int } @@ -227,14 +227,14 @@ func findTopELOMovers(data *IndexData, count int) []eloMover { wins, losses := countWeeklyResults(bot.ID, data) movers = append(movers, eloMover{ - BotID: bot.ID, - BotName: bot.Name, - OldRating: oldRating, - NewRating: bot.Rating, - Delta: delta, - Evolved: bot.Evolved, - Archetype: bot.Archetype, - MatchesWon: wins, + BotID: bot.ID, + BotName: bot.Name, + OldRating: oldRating, + NewRating: bot.Rating, + Delta: delta, + Evolved: bot.Evolved, + Archetype: bot.Archetype, + MatchesWon: wins, MatchesLost: losses, }) } @@ -1224,12 +1224,12 @@ func getBotArchetype(botID string, data *IndexData) string { // ─── Strategy trend analysis ─────────────────────────────────────────────────── type strategyTrend struct { - Archetype string - ThisWeekPct float64 // % of top-20 this week - LastWeekPct float64 // % of top-20 implied from rating history - Shift float64 // ThisWeekPct - LastWeekPct - AvgRating float64 - Count int + Archetype string + ThisWeekPct float64 // % of top-20 this week + LastWeekPct float64 // % of top-20 implied from rating history + Shift float64 // ThisWeekPct - LastWeekPct + AvgRating float64 + Count int } // calculateStrategyTrends compares archetype representation in the top 20 this @@ -1331,12 +1331,12 @@ type evolutionLiveData struct { BestBot string `json:"best_bot"` } `json:"islands"` RecentActivity []struct { - Time string `json:"time"` - Candidate string `json:"candidate"` - Island string `json:"island"` - Result string `json:"result"` - Reason string `json:"reason"` - Stage string `json:"stage"` + Time string `json:"time"` + Candidate string `json:"candidate"` + Island string `json:"island"` + Result string `json:"result"` + Reason string `json:"reason"` + Stage string `json:"stage"` } `json:"recent_activity"` } @@ -1376,7 +1376,6 @@ func nonEmpty(s, fallback string) string { return fallback } - func findSectionIndex(content, section string) int { // Find "## Looking Ahead" as a section header for i := 0; i < len(content)-len(section); i++ { diff --git a/cmd/acb-index-builder/config.go b/cmd/acb-index-builder/config.go index 1e4ecd7..20181aa 100644 --- a/cmd/acb-index-builder/config.go +++ b/cmd/acb-index-builder/config.go @@ -22,21 +22,21 @@ type Config struct { BuildTimeout time.Duration // Timeout for each build cycle (default: 10m) // Cloudflare configuration - CloudflareAPIToken string + CloudflareAPIToken string CloudflareAccountID string - PagesProjectName string + PagesProjectName string // R2 configuration for warm cache management - R2AccessKey string - R2SecretKey string - R2Endpoint string - R2BucketName string + R2AccessKey string + R2SecretKey string + R2Endpoint string + R2BucketName string // B2 configuration for cold archive - B2AccessKey string - B2SecretKey string - B2Endpoint string - B2BucketName string + B2AccessKey string + B2SecretKey string + B2Endpoint string + B2BucketName string // Output directory for generated files OutputDir string diff --git a/cmd/acb-index-builder/db.go b/cmd/acb-index-builder/db.go index bc73af9..adf6cb0 100644 --- a/cmd/acb-index-builder/db.go +++ b/cmd/acb-index-builder/db.go @@ -32,17 +32,17 @@ type BotData struct { // MatchData represents a match for the index type MatchData struct { - ID string `json:"id"` - MapID string `json:"map_id"` - MapName string `json:"map_name,omitempty"` - WinnerID string `json:"winner_id,omitempty"` - TurnCount int `json:"turn_count"` - EndCondition string `json:"end_condition"` - CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death - Participants []ParticipantData `json:"participants"` - CreatedAt time.Time `json:"created_at"` - CompletedAt time.Time `json:"completed_at"` - PlayedAt time.Time `json:"played_at"` + ID string `json:"id"` + MapID string `json:"map_id"` + MapName string `json:"map_name,omitempty"` + WinnerID string `json:"winner_id,omitempty"` + TurnCount int `json:"turn_count"` + EndCondition string `json:"end_condition"` + CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death + Participants []ParticipantData `json:"participants"` + CreatedAt time.Time `json:"created_at"` + CompletedAt time.Time `json:"completed_at"` + PlayedAt time.Time `json:"played_at"` } // ParticipantData represents a bot in a match with pre-match rating @@ -140,13 +140,13 @@ type SeasonData struct { // PredictionData represents a prediction for the index type PredictionData struct { - ID int64 `json:"id"` - MatchID string `json:"match_id"` - PredictorID string `json:"predictor_id"` - PredictedBot string `json:"predicted_bot"` - Correct *bool `json:"correct,omitempty"` - CreatedAt time.Time `json:"created_at"` - ResolvedAt *time.Time `json:"resolved_at,omitempty"` + ID int64 `json:"id"` + MatchID string `json:"match_id"` + PredictorID string `json:"predictor_id"` + PredictedBot string `json:"predicted_bot"` + Correct *bool `json:"correct,omitempty"` + CreatedAt time.Time `json:"created_at"` + ResolvedAt *time.Time `json:"resolved_at,omitempty"` } // PredictorStats represents predictor statistics @@ -168,25 +168,25 @@ type MapData struct { EnergyCount int `json:"energy_count"` GridWidth int `json:"grid_width"` GridHeight int `json:"grid_height"` - NetVotes int `json:"net_votes"` // Sum of votes from map_votes table + NetVotes int `json:"net_votes"` // Sum of votes from map_votes table CreatedAt time.Time `json:"created_at"` RawJSON json.RawMessage `json:"-"` } // OpenPredictionMatch represents a pending match open for predictions type OpenPredictionMatch struct { - MatchID string `json:"match_id"` - BotAID string `json:"bot_a"` - BotBID string `json:"bot_b"` - BotAName string `json:"bot_a_name"` - BotBName string `json:"bot_b_name"` - ARating float64 `json:"a_rating"` - BRating float64 `json:"b_rating"` - AEvolved bool `json:"a_evolved"` - BEvolved bool `json:"b_evolved"` - CreatedAt time.Time `json:"created_at"` - IsSeriesMatch bool `json:"is_series_match"` - HeadToHeadRecord *string `json:"head_to_head_record,omitempty"` + MatchID string `json:"match_id"` + BotAID string `json:"bot_a"` + BotBID string `json:"bot_b"` + BotAName string `json:"bot_a_name"` + BotBName string `json:"bot_b_name"` + ARating float64 `json:"a_rating"` + BRating float64 `json:"b_rating"` + AEvolved bool `json:"a_evolved"` + BEvolved bool `json:"b_evolved"` + CreatedAt time.Time `json:"created_at"` + IsSeriesMatch bool `json:"is_series_match"` + HeadToHeadRecord *string `json:"head_to_head_record,omitempty"` } // FeedbackEntry represents a community replay annotation from §13.6. @@ -203,18 +203,18 @@ type FeedbackEntry struct { // IndexData contains all data needed for index generation type IndexData struct { - GeneratedAt time.Time - Bots []BotData - Matches []MatchData - RatingHistory []RatingHistoryEntry - Series []SeriesData - Seasons []SeasonData - Predictions []PredictionData - PredictorStats []PredictorStats - Maps []MapData - TopPredictors []PredictorStats + GeneratedAt time.Time + Bots []BotData + Matches []MatchData + RatingHistory []RatingHistoryEntry + Series []SeriesData + Seasons []SeasonData + Predictions []PredictionData + PredictorStats []PredictorStats + Maps []MapData + TopPredictors []PredictorStats OpenPredictionMatches []OpenPredictionMatch - Feedback []FeedbackEntry + Feedback []FeedbackEntry } // fetchAllData retrieves all data from PostgreSQL for index generation diff --git a/cmd/acb-index-builder/enrichment.go b/cmd/acb-index-builder/enrichment.go index 2eb1767..fc449e9 100644 --- a/cmd/acb-index-builder/enrichment.go +++ b/cmd/acb-index-builder/enrichment.go @@ -22,10 +22,10 @@ type CommentaryEntry struct { // EnrichedCommentary wraps all AI commentary for a single match. type EnrichedCommentary struct { - MatchID string `json:"match_id"` + MatchID string `json:"match_id"` Generated string `json:"generated_at"` - Criteria []string `json:"criteria"` // why this match was selected - Entries []CommentaryEntry `json:"entries"` + Criteria []string `json:"criteria"` // why this match was selected + Entries []CommentaryEntry `json:"entries"` } // shouldEnrich returns true and lists criteria if the match qualifies for @@ -146,18 +146,18 @@ func enrichSingleReplay(ctx context.Context, m MatchData, criteria []string, dat Description string `json:"description"` } `json:"critical_moments"` Result struct { - Winner int `json:"winner"` - Reason string `json:"reason"` - Turns int `json:"turns"` - Scores []int `json:"scores"` + Winner int `json:"winner"` + Reason string `json:"reason"` + Turns int `json:"turns"` + Scores []int `json:"scores"` } `json:"result"` Players []struct { ID int `json:"id"` Name string `json:"name"` } `json:"players"` Turns []struct { - Turn int `json:"turn"` - Events []struct { + Turn int `json:"turn"` + Events []struct { Type string `json:"type"` Turn int `json:"turn"` Details any `json:"details"` @@ -220,18 +220,18 @@ func buildCommentaryPrompt(m MatchData, replay struct { Description string `json:"description"` } `json:"critical_moments"` Result struct { - Winner int `json:"winner"` - Reason string `json:"reason"` - Turns int `json:"turns"` - Scores []int `json:"scores"` + Winner int `json:"winner"` + Reason string `json:"reason"` + Turns int `json:"turns"` + Scores []int `json:"scores"` } `json:"result"` Players []struct { ID int `json:"id"` Name string `json:"name"` } `json:"players"` Turns []struct { - Turn int `json:"turn"` - Events []struct { + Turn int `json:"turn"` + Events []struct { Type string `json:"type"` Turn int `json:"turn"` Details any `json:"details"` diff --git a/cmd/acb-index-builder/generator.go b/cmd/acb-index-builder/generator.go index ac0527a..474f18e 100644 --- a/cmd/acb-index-builder/generator.go +++ b/cmd/acb-index-builder/generator.go @@ -16,22 +16,22 @@ import ( // LeaderboardIndex represents the leaderboard.json structure type LeaderboardIndex struct { - UpdatedAt string `json:"updated_at"` + UpdatedAt string `json:"updated_at"` Entries []LeaderboardEntry `json:"entries"` } // LeaderboardEntry represents a single bot on the leaderboard type LeaderboardEntry struct { - Rank int `json:"rank"` - BotID string `json:"bot_id"` - Name string `json:"name"` - OwnerID string `json:"owner_id"` - Rating int `json:"rating"` + Rank int `json:"rank"` + BotID string `json:"bot_id"` + Name string `json:"name"` + OwnerID string `json:"owner_id"` + Rating int `json:"rating"` RatingDeviation float64 `json:"rating_deviation"` - MatchesPlayed int `json:"matches_played"` - MatchesWon int `json:"matches_won"` - WinRate float64 `json:"win_rate"` - HealthStatus string `json:"health_status"` + MatchesPlayed int `json:"matches_played"` + MatchesWon int `json:"matches_won"` + WinRate float64 `json:"win_rate"` + HealthStatus string `json:"health_status"` } // BotDirectory represents bots/index.json @@ -51,38 +51,38 @@ type BotDirectoryEntry struct { // BotProfile represents data/bots/{bot_id}.json type BotProfile struct { - ID string `json:"id"` - Name string `json:"name"` - OwnerID string `json:"owner_id"` - Description string `json:"description,omitempty"` - Rating int `json:"rating"` - RatingDeviation float64 `json:"rating_deviation"` - RatingVolatility float64 `json:"rating_volatility"` - MatchesPlayed int `json:"matches_played"` - MatchesWon int `json:"matches_won"` - WinRate float64 `json:"win_rate"` - HealthStatus string `json:"health_status"` - Evolved bool `json:"evolved"` - Island string `json:"island,omitempty"` - Generation int `json:"generation,omitempty"` - DebugPublic bool `json:"debug_public"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - RatingHistory []RatingHistoryEntry `json:"rating_history"` - RecentMatches []MatchSummary `json:"recent_matches"` + ID string `json:"id"` + Name string `json:"name"` + OwnerID string `json:"owner_id"` + Description string `json:"description,omitempty"` + Rating int `json:"rating"` + RatingDeviation float64 `json:"rating_deviation"` + RatingVolatility float64 `json:"rating_volatility"` + MatchesPlayed int `json:"matches_played"` + MatchesWon int `json:"matches_won"` + WinRate float64 `json:"win_rate"` + HealthStatus string `json:"health_status"` + Evolved bool `json:"evolved"` + Island string `json:"island,omitempty"` + Generation int `json:"generation,omitempty"` + DebugPublic bool `json:"debug_public"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + RatingHistory []RatingHistoryEntry `json:"rating_history"` + RecentMatches []MatchSummary `json:"recent_matches"` } // MatchSummary represents a match in listings type MatchSummary struct { - ID string `json:"id"` - CompletedAt string `json:"completed_at"` + ID string `json:"id"` + CompletedAt string `json:"completed_at"` Participants []MatchParticipantSummary `json:"participants"` - WinnerID string `json:"winner_id,omitempty"` - MapID string `json:"map_id,omitempty"` - Turns int `json:"turns"` - EndReason string `json:"end_reason"` - Enriched bool `json:"enriched"` - CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death + WinnerID string `json:"winner_id,omitempty"` + MapID string `json:"map_id,omitempty"` + Turns int `json:"turns"` + EndReason string `json:"end_reason"` + Enriched bool `json:"enriched"` + CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death } // MatchParticipantSummary represents a bot in a match summary @@ -464,13 +464,13 @@ func generatePredictionsIndex(data *IndexData, outputDir string) error { // evolved bot vs top-10). func generatePredictionsOpen(data *IndexData, outputDir string) error { type OpenMatchEntry struct { - MatchID string `json:"match_id"` - BotA string `json:"bot_a"` - BotB string `json:"bot_b"` - ARating int `json:"a_rating"` - BRating int `json:"b_rating"` - OpenUntil string `json:"open_until"` - HeadToHeadRecord *string `json:"head_to_head_record,omitempty"` + MatchID string `json:"match_id"` + BotA string `json:"bot_a"` + BotB string `json:"bot_b"` + ARating int `json:"a_rating"` + BRating int `json:"b_rating"` + OpenUntil string `json:"open_until"` + HeadToHeadRecord *string `json:"head_to_head_record,omitempty"` } type OpenPredictionsIndex struct { @@ -722,12 +722,12 @@ func generatePlaylists(data *IndexData, outputDir string, botNameMap map[string] thumbMatchID = curatedMatches[0].ID } summaries = append(summaries, PlaylistSummary{ - Slug: def.slug, - Title: def.title, - Description: def.description, - Category: def.category, - MatchCount: len(curatedMatches), - UpdatedAt: data.GeneratedAt.Format(time.RFC3339), + Slug: def.slug, + Title: def.title, + Description: def.description, + Category: def.category, + MatchCount: len(curatedMatches), + UpdatedAt: data.GeneratedAt.Format(time.RFC3339), ThumbnailMatchID: thumbMatchID, }) continue @@ -748,12 +748,12 @@ func generatePlaylists(data *IndexData, outputDir string, botNameMap map[string] thumbMatchID = filtered[0].ID } summaries = append(summaries, PlaylistSummary{ - Slug: def.slug, - Title: def.title, - Description: def.description, - Category: def.category, - MatchCount: len(filtered), - UpdatedAt: data.GeneratedAt.Format(time.RFC3339), + Slug: def.slug, + Title: def.title, + Description: def.description, + Category: def.category, + MatchCount: len(filtered), + UpdatedAt: data.GeneratedAt.Format(time.RFC3339), ThumbnailMatchID: thumbMatchID, }) } @@ -766,8 +766,8 @@ func generatePlaylists(data *IndexData, outputDir string, botNameMap map[string] } type PlaylistIndex struct { - UpdatedAt string `json:"updated_at"` - Playlists []PlaylistSummary `json:"playlists"` + UpdatedAt string `json:"updated_at"` + Playlists []PlaylistSummary `json:"playlists"` } type PlaylistSummary struct { @@ -1373,22 +1373,22 @@ func isRivalryMatch(m MatchData, data *IndexData) bool { // ─── Rivalry Detection (§13.5) ───────────────────────────────────────────────── const ( - rivalryMinMatches = 10 // minimum h2h matches to qualify - rivalryTopK = 20 // max rivalries to emit + rivalryMinMatches = 10 // minimum h2h matches to qualify + rivalryTopK = 20 // max rivalries to emit rivalryRecencyDecay = 0.95 // per-day decay for recency weighting ) // RivalryEntry represents a detected rivalry pair for data/meta/rivalries.json. type RivalryEntry struct { - BotA RivalryBot `json:"bot_a"` - BotB RivalryBot `json:"bot_b"` - TotalMatches int `json:"matches"` - Record RivalryRecord `json:"record"` - ClosestMatch string `json:"closest_match,omitempty"` + BotA RivalryBot `json:"bot_a"` + BotB RivalryBot `json:"bot_b"` + TotalMatches int `json:"matches"` + Record RivalryRecord `json:"record"` + ClosestMatch string `json:"closest_match,omitempty"` LongestStreak *RivalryStreak `json:"longest_streak,omitempty"` - RecentMatches []string `json:"recent_matches"` - Narrative string `json:"narrative"` - Score float64 `json:"score"` + RecentMatches []string `json:"recent_matches"` + Narrative string `json:"narrative"` + Score float64 `json:"score"` } type RivalryBot struct { @@ -1534,15 +1534,15 @@ func computeRivalries(data *IndexData, botNameMap map[string]string) []RivalryEn bName := botNameMap[rec.botBID] candidates = append(candidates, RivalryEntry{ - BotA: RivalryBot{ID: rec.botAID, Name: aName}, - BotB: RivalryBot{ID: rec.botBID, Name: bName}, - TotalMatches: total, - Record: RivalryRecord{AWins: rec.aWins, BWins: rec.bWins, Draws: rec.draws}, - ClosestMatch: closestMatch, + BotA: RivalryBot{ID: rec.botAID, Name: aName}, + BotB: RivalryBot{ID: rec.botBID, Name: bName}, + TotalMatches: total, + Record: RivalryRecord{AWins: rec.aWins, BWins: rec.bWins, Draws: rec.draws}, + ClosestMatch: closestMatch, LongestStreak: streak, RecentMatches: recentMatches, - Narrative: buildRivalryNarrative(aName, bName, rec.botAID, rec.botBID, total, rec.aWins, rec.bWins, rec.draws, streak), - Score: score, + Narrative: buildRivalryNarrative(aName, bName, rec.botAID, rec.botBID, total, rec.aWins, rec.bWins, rec.draws, streak), + Score: score, }) } @@ -1687,7 +1687,7 @@ type MapIndexEntry struct { EnergyCount int `json:"energy_count"` GridWidth int `json:"grid_width"` GridHeight int `json:"grid_height"` - NetVotes int `json:"net_votes"` // Sum of +1/-1 votes from map_votes table + NetVotes int `json:"net_votes"` // Sum of +1/-1 votes from map_votes table CreatedAt string `json:"created_at"` } @@ -1708,7 +1708,7 @@ type MapDetail struct { EnergyCount int `json:"energy_count"` GridWidth int `json:"grid_width"` GridHeight int `json:"grid_height"` - NetVotes int `json:"net_votes"` // Sum of +1/-1 votes from map_votes table + NetVotes int `json:"net_votes"` // Sum of +1/-1 votes from map_votes table CreatedAt string `json:"created_at"` Walls []mapPosition `json:"walls"` Cores []mapCore `json:"cores"` @@ -2086,8 +2086,8 @@ type SitemapURL struct { // Sitemap represents the root sitemap XML structure type Sitemap struct { - XMLName xml.Name `xml:"urlset"` - Xmlns string `xml:"xmlns,attr"` + XMLName xml.Name `xml:"urlset"` + Xmlns string `xml:"xmlns,attr"` URLs []SitemapURL `xml:"url"` } @@ -2112,10 +2112,10 @@ func generateSitemap(data *IndexData, outputDir string, siteURL string) error { // Bot list page urls = append(urls, SitemapURL{ - Loc: siteURL + "/bots", - LastMod: now, + Loc: siteURL + "/bots", + LastMod: now, ChangeFreq: "daily", - Priority: "0.8", + Priority: "0.8", }) // Individual bot profiles (limit to 1000 for sitemap size) @@ -2128,10 +2128,10 @@ func generateSitemap(data *IndexData, outputDir string, siteURL string) error { priority = "0.8" // Top bots get higher priority } urls = append(urls, SitemapURL{ - Loc: siteURL + "/bot/" + bot.ID, - LastMod: bot.UpdatedAt.Format("2006-01-02"), + Loc: siteURL + "/bot/" + bot.ID, + LastMod: bot.UpdatedAt.Format("2006-01-02"), ChangeFreq: "daily", - Priority: priority, + Priority: priority, }) } @@ -2151,57 +2151,57 @@ func generateSitemap(data *IndexData, outputDir string, siteURL string) error { lastMod = m.CreatedAt.Format("2006-01-02") } urls = append(urls, SitemapURL{ - Loc: siteURL + "/watch/replay/" + m.ID, - LastMod: lastMod, + Loc: siteURL + "/watch/replay/" + m.ID, + LastMod: lastMod, ChangeFreq: "monthly", - Priority: priority, + Priority: priority, }) } // Series pages for _, s := range data.Series { urls = append(urls, SitemapURL{ - Loc: siteURL + "/watch/series/" + fmt.Sprintf("%d", s.ID), - LastMod: s.UpdatedAt.Format("2006-01-02"), + Loc: siteURL + "/watch/series/" + fmt.Sprintf("%d", s.ID), + LastMod: s.UpdatedAt.Format("2006-01-02"), ChangeFreq: "weekly", - Priority: "0.6", + Priority: "0.6", }) } // Seasons list page urls = append(urls, SitemapURL{ - Loc: siteURL + "/season", - LastMod: now, + Loc: siteURL + "/season", + LastMod: now, ChangeFreq: "weekly", - Priority: "0.7", + Priority: "0.7", }) // Individual season pages for _, s := range data.Seasons { urls = append(urls, SitemapURL{ - Loc: siteURL + "/season/" + fmt.Sprintf("%d", s.ID), - LastMod: s.StartsAt.Format("2006-01-02"), + Loc: siteURL + "/season/" + fmt.Sprintf("%d", s.ID), + LastMod: s.StartsAt.Format("2006-01-02"), ChangeFreq: "weekly", - Priority: "0.7", + Priority: "0.7", }) } // Rivalries page urls = append(urls, SitemapURL{ - Loc: siteURL + "/rivalries", - LastMod: now, + Loc: siteURL + "/rivalries", + LastMod: now, ChangeFreq: "weekly", - Priority: "0.6", + Priority: "0.6", }) // Docs pages docsPages := []string{"protocol", "replay-format", "getting-started", "starter-kits"} for _, doc := range docsPages { urls = append(urls, SitemapURL{ - Loc: siteURL + "/compete/docs/" + doc, - LastMod: now, + Loc: siteURL + "/compete/docs/" + doc, + LastMod: now, ChangeFreq: "monthly", - Priority: "0.5", + Priority: "0.5", }) } diff --git a/cmd/acb-index-builder/main_test.go b/cmd/acb-index-builder/main_test.go index 3c7b3c4..3817dd9 100644 --- a/cmd/acb-index-builder/main_test.go +++ b/cmd/acb-index-builder/main_test.go @@ -88,30 +88,30 @@ func TestGenerateLeaderboard(t *testing.T) { GeneratedAt: time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC), Bots: []BotData{ { - ID: "bot1", - Name: "TestBot1", - OwnerID: "owner1", - Rating: 1650.0, - RatingDeviation: 50.0, - MatchesPlayed: 100, - MatchesWon: 75, - HealthStatus: "ACTIVE", - Evolved: false, - CreatedAt: time.Now(), + ID: "bot1", + Name: "TestBot1", + OwnerID: "owner1", + Rating: 1650.0, + RatingDeviation: 50.0, + MatchesPlayed: 100, + MatchesWon: 75, + HealthStatus: "ACTIVE", + Evolved: false, + CreatedAt: time.Now(), }, { - ID: "bot2", - Name: "TestBot2", - OwnerID: "owner2", - Rating: 1550.0, - RatingDeviation: 75.0, - MatchesPlayed: 50, - MatchesWon: 25, - HealthStatus: "ACTIVE", - Evolved: true, - Island: "python", - Generation: 5, - CreatedAt: time.Now(), + ID: "bot2", + Name: "TestBot2", + OwnerID: "owner2", + Rating: 1550.0, + RatingDeviation: 75.0, + MatchesPlayed: 50, + MatchesWon: 25, + HealthStatus: "ACTIVE", + Evolved: true, + Island: "python", + Generation: 5, + CreatedAt: time.Now(), }, }, Matches: []MatchData{}, @@ -382,8 +382,8 @@ func TestInterestScore(t *testing.T) { now := time.Now() // Close finish + upset + long game → high score m := MatchData{ - WinnerID: "bot2", - TurnCount: 450, + WinnerID: "bot2", + TurnCount: 450, CompletedAt: now, Participants: []ParticipantData{ {BotID: "bot1", Score: 3, Won: false, PreMatchRating: 1800}, @@ -397,8 +397,8 @@ func TestInterestScore(t *testing.T) { // Boring match → low score m2 := MatchData{ - WinnerID: "bot1", - TurnCount: 100, + WinnerID: "bot1", + TurnCount: 100, CompletedAt: now, Participants: []ParticipantData{ {BotID: "bot1", Score: 10, Won: true, PreMatchRating: 1500}, @@ -1095,9 +1095,9 @@ func TestGenerateAllBotCards(t *testing.T) { func TestGetColorForRating(t *testing.T) { tests := []struct { - rating int - name string - checkR uint8 + rating int + name string + checkR uint8 }{ {2100, "gold", 255}, {1850, "silver", 192}, @@ -1136,9 +1136,9 @@ func TestGetWinRateColor(t *testing.T) { func TestGetRankBadgeColor(t *testing.T) { tests := []struct { - rank int - name string - checkR uint8 + rank int + name string + checkR uint8 }{ {1, "gold", 255}, {2, "silver", 192}, diff --git a/cmd/acb-index-builder/narrative.go b/cmd/acb-index-builder/narrative.go index daa7c57..e9a8fe0 100644 --- a/cmd/acb-index-builder/narrative.go +++ b/cmd/acb-index-builder/narrative.go @@ -24,15 +24,15 @@ const ( // StoryArc represents a detected narrative arc type StoryArc struct { - Type StoryArcType `json:"type"` - BotID string `json:"bot_id,omitempty"` - BotName string `json:"bot_name,omitempty"` - BotBID string `json:"bot_b_id,omitempty"` - BotBName string `json:"bot_b_name,omitempty"` - RatingStart int `json:"rating_start,omitempty"` - RatingEnd int `json:"rating_end,omitempty"` - MatchID string `json:"match_id,omitempty"` - SeasonName string `json:"season_name,omitempty"` + Type StoryArcType `json:"type"` + BotID string `json:"bot_id,omitempty"` + BotName string `json:"bot_name,omitempty"` + BotBID string `json:"bot_b_id,omitempty"` + BotBName string `json:"bot_b_name,omitempty"` + RatingStart int `json:"rating_start,omitempty"` + RatingEnd int `json:"rating_end,omitempty"` + MatchID string `json:"match_id,omitempty"` + SeasonName string `json:"season_name,omitempty"` // Context for LLM prompt KeyMatches []KeyMatch `json:"key_matches,omitempty"` @@ -573,9 +573,9 @@ func detectRivalryArcs(data *IndexData) []StoryArc { arcs := make([]StoryArc, 0) pairData := make(map[string]*struct { - botAID, botBID string - aWins, bWins int - total int + botAID, botBID string + aWins, bWins int + total int }) for _, m := range data.Matches { @@ -590,9 +590,9 @@ func detectRivalryArcs(data *IndexData) []StoryArc { if pairData[key] == nil { pairData[key] = &struct { - botAID, botBID string - aWins, bWins int - total int + botAID, botBID string + aWins, bWins int + total int }{botAID: aID, botBID: bID} } pairData[key].total++ @@ -1021,7 +1021,6 @@ func getBotRatingHistory(botID string, data *IndexData) []RatingHistoryEntry { // ─── Weekly Chronicles Generation ──────────────────────────────────────────────── - // GenerateWeeklyChronicles creates a ~500-word aggregated narrative for the week func (c *LLMClient) GenerateWeeklyChronicles(ctx context.Context, req WeeklyChroniclesRequest) (string, error) { prompt := buildWeeklyChroniclesPrompt(req) diff --git a/cmd/acb-index-builder/narrative_test.go b/cmd/acb-index-builder/narrative_test.go index eaad4aa..11b6af0 100644 --- a/cmd/acb-index-builder/narrative_test.go +++ b/cmd/acb-index-builder/narrative_test.go @@ -10,16 +10,16 @@ import ( func TestBuildNarrativePrompt_Rise(t *testing.T) { req := NarrativeRequest{ - ArcType: ArcRise, - BotName: "TestBot", - SeasonName: "Season 4", + ArcType: ArcRise, + BotName: "TestBot", + SeasonName: "Season 4", RatingStart: 1200, - RatingEnd: 1450, + RatingEnd: 1450, KeyMatches: []KeyMatch{ {MatchID: "m1", OpponentName: "TopBot", OpponentRating: 1800, MapName: "The Labyrinth", Score: "3-2", TurnCount: 200, Won: true}, }, Archetype: "aggressive", - Origin: "evolved, go island, generation 5", + Origin: "evolved, go island, generation 5", } prompt := buildNarrativePrompt(req) @@ -40,11 +40,11 @@ func TestBuildNarrativePrompt_Rise(t *testing.T) { func TestBuildNarrativePrompt_Upset(t *testing.T) { req := NarrativeRequest{ - ArcType: ArcUpset, - BotName: "UnderdogBot", - BotBName: "FavoriteBot", + ArcType: ArcUpset, + BotName: "UnderdogBot", + BotBName: "FavoriteBot", RatingStart: 1100, - RatingEnd: 1800, + RatingEnd: 1800, KeyMatches: []KeyMatch{ {MatchID: "m2", OpponentName: "FavoriteBot", OpponentRating: 1800, MapName: "Open Field", Score: "4-3", TurnCount: 150, Won: true}, }, @@ -65,13 +65,13 @@ func TestBuildNarrativePrompt_Upset(t *testing.T) { func TestBuildNarrativePrompt_Rivalry(t *testing.T) { req := NarrativeRequest{ - ArcType: ArcRivalry, - BotName: "SwarmBot", - BotBName: "HunterBot", - BotAWins: 5, - BotBWins: 4, + ArcType: ArcRivalry, + BotName: "SwarmBot", + BotBName: "HunterBot", + BotAWins: 5, + BotBWins: 4, TotalMatches: 9, - SeasonName: "Season 4", + SeasonName: "Season 4", } prompt := buildNarrativePrompt(req) @@ -93,10 +93,10 @@ func TestBuildNarrativePrompt_Evolution(t *testing.T) { BotName: "evo-go-g31", SeasonName: "Season 4", RatingEnd: 1580, - Origin: "evolved, go island", + Origin: "evolved, go island", Generation: 31, - ParentIDs: []string{"evo-go-g28", "evo-go-g25"}, - Archetype: "hybrid swarm-gatherer", + ParentIDs: []string{"evo-go-g28", "evo-go-g25"}, + Archetype: "hybrid swarm-gatherer", } prompt := buildNarrativePrompt(req) @@ -145,8 +145,8 @@ func TestTruncateSummary(t *testing.T) { for _, tc := range tests { result := truncateSummary(tc.input, tc.maxLen) if result != tc.expected { - t.Errorf("truncateSummary(%q, %d) = %q, want %q", tc.input, tc.maxLen, result, tc.expected) - } + t.Errorf("truncateSummary(%q, %d) = %q, want %q", tc.input, tc.maxLen, result, tc.expected) + } } } diff --git a/cmd/acb-index-builder/rivalry_test.go b/cmd/acb-index-builder/rivalry_test.go index 45eb490..9043f9e 100644 --- a/cmd/acb-index-builder/rivalry_test.go +++ b/cmd/acb-index-builder/rivalry_test.go @@ -16,9 +16,9 @@ func TestComputeRivalries_BasicPair(t *testing.T) { matches := make([]MatchData, 12) for i := 0; i < 6; i++ { matches[i] = MatchData{ - ID: fmt.Sprintf("m_a_%d", i), - WinnerID: "bot1", - PlayedAt: now.Add(-time.Duration(12-i) * 24 * time.Hour), + ID: fmt.Sprintf("m_a_%d", i), + WinnerID: "bot1", + PlayedAt: now.Add(-time.Duration(12-i) * 24 * time.Hour), Participants: []ParticipantData{ {BotID: "bot1", Score: 5 - i%3, Won: true}, {BotID: "bot2", Score: 2 + i%2, Won: false}, @@ -27,9 +27,9 @@ func TestComputeRivalries_BasicPair(t *testing.T) { } for i := 0; i < 6; i++ { matches[6+i] = MatchData{ - ID: fmt.Sprintf("m_b_%d", i), - WinnerID: "bot2", - PlayedAt: now.Add(-time.Duration(6-i) * 24 * time.Hour), + ID: fmt.Sprintf("m_b_%d", i), + WinnerID: "bot2", + PlayedAt: now.Add(-time.Duration(6-i) * 24 * time.Hour), Participants: []ParticipantData{ {BotID: "bot1", Score: 2 + i%2, Won: false}, {BotID: "bot2", Score: 5 - i%3, Won: true}, @@ -84,9 +84,9 @@ func TestComputeRivalries_BelowThreshold(t *testing.T) { matches := make([]MatchData, 5) for i := 0; i < 5; i++ { matches[i] = MatchData{ - ID: fmt.Sprintf("m_%d", i), - WinnerID: "bot1", - PlayedAt: now.Add(-time.Duration(i) * time.Hour), + ID: fmt.Sprintf("m_%d", i), + WinnerID: "bot1", + PlayedAt: now.Add(-time.Duration(i) * time.Hour), Participants: []ParticipantData{ {BotID: "bot1", Score: 3, Won: true}, {BotID: "bot2", Score: 1, Won: false}, @@ -197,9 +197,9 @@ func TestComputeRivalries_TopKLimit(t *testing.T) { winner = botB } matches = append(matches, MatchData{ - ID: fmt.Sprintf("pair%d_m%d", pair, i), - WinnerID: winner, - PlayedAt: now.Add(-time.Duration(i) * time.Hour), + ID: fmt.Sprintf("pair%d_m%d", pair, i), + WinnerID: winner, + PlayedAt: now.Add(-time.Duration(i) * time.Hour), Participants: []ParticipantData{ {BotID: botA, Score: 3, Won: winner == botA}, {BotID: botB, Score: 2, Won: winner == botB}, @@ -267,9 +267,9 @@ func TestComputeRivalries_MultiPlayerSkipped(t *testing.T) { var matches []MatchData for i := 0; i < 10; i++ { matches = append(matches, MatchData{ - ID: fmt.Sprintf("2p_%d", i), - WinnerID: "bot1", - PlayedAt: now.Add(-time.Duration(i) * time.Hour), + ID: fmt.Sprintf("2p_%d", i), + WinnerID: "bot1", + PlayedAt: now.Add(-time.Duration(i) * time.Hour), Participants: []ParticipantData{ {BotID: "bot1", Score: 3, Won: true}, {BotID: "bot2", Score: 1, Won: false}, @@ -278,9 +278,9 @@ func TestComputeRivalries_MultiPlayerSkipped(t *testing.T) { } for i := 0; i < 5; i++ { matches = append(matches, MatchData{ - ID: fmt.Sprintf("3p_%d", i), - WinnerID: "bot1", - PlayedAt: now.Add(-time.Duration(i) * time.Hour), + ID: fmt.Sprintf("3p_%d", i), + WinnerID: "bot1", + PlayedAt: now.Add(-time.Duration(i) * time.Hour), Participants: []ParticipantData{ {BotID: "bot1", Score: 3, Won: true}, {BotID: "bot2", Score: 1, Won: false}, @@ -375,12 +375,12 @@ func TestComputeRivalries_RecencyBoost(t *testing.T) { func TestLongestStreak(t *testing.T) { tests := []struct { - name string - winners []string - botA string - botB string - wantLen int - wantNil bool + name string + winners []string + botA string + botB string + wantLen int + wantNil bool }{ {"empty", []string{}, "a", "b", 0, true}, {"single", []string{"a"}, "a", "b", 0, true}, // < 2 diff --git a/cmd/acb-index-builder/s3.go b/cmd/acb-index-builder/s3.go index a3b5311..201e526 100644 --- a/cmd/acb-index-builder/s3.go +++ b/cmd/acb-index-builder/s3.go @@ -50,14 +50,14 @@ func (c *S3Client) listObjects(ctx context.Context, prefix string) ([]R2Object, for { input := &s3.ListObjectsV2Input{ - Bucket: aws.String(c.bucket), - Prefix: aws.String(prefix), + Bucket: aws.String(c.bucket), + Prefix: aws.String(prefix), ContinuationToken: continuationToken, } output, err := c.client.ListObjectsV2(ctx, input) if err != nil { - return nil, fmt.Errorf("list objects: %w", err) + return nil, fmt.Errorf("list objects: %w", err) } // Add objects to result diff --git a/cmd/acb-index-builder/s3_test.go b/cmd/acb-index-builder/s3_test.go index 02c1d88..e82d80a 100644 --- a/cmd/acb-index-builder/s3_test.go +++ b/cmd/acb-index-builder/s3_test.go @@ -37,7 +37,7 @@ func NewMockS3Client() *MockS3Client { return &MockS3Client{ Objects: make(map[string]MockObject), UploadCalls: []UploadCall{}, - DeleteCalls: []string{}, + DeleteCalls: []string{}, CopyCalls: []CopyCall{}, } } diff --git a/cmd/acb-map-evolver/main.go b/cmd/acb-map-evolver/main.go index 05d9fa1..9a0122d 100644 --- a/cmd/acb-map-evolver/main.go +++ b/cmd/acb-map-evolver/main.go @@ -160,7 +160,7 @@ func parseConfig() *Config { EvolutionPeriod: 30 * time.Minute, WeeklySchedule: WeeklySchedule{ Weekday: time.Sunday, // Default: Sunday - Hour: 3, // Default: 03:00 UTC + Hour: 3, // Default: 03:00 UTC Minute: 0, }, } @@ -552,8 +552,8 @@ func (e *MapEvolver) mutate(m *Map) { for _, core := range m.Cores { for dr := -3; dr <= 3; dr++ { for dc := -3; dc <= 3; dc++ { - nr := ((core.Position.Row + dr) % m.Rows + m.Rows) % m.Rows - nc := ((core.Position.Col + dc) % m.Cols + m.Cols) % m.Cols + nr := ((core.Position.Row+dr)%m.Rows + m.Rows) % m.Rows + nc := ((core.Position.Col+dc)%m.Cols + m.Cols) % m.Cols protected[Position{Row: nr, Col: nc}] = true } } @@ -774,7 +774,7 @@ func (e *MapEvolver) smoothWalls(m *Map, protected PositionSet) { m.Walls = append(m.Walls, pos) } - m.WallDensity = float64(len(m.Walls)) / float64(m.Rows * m.Cols) + m.WallDensity = float64(len(m.Walls)) / float64(m.Rows*m.Cols) } // validate checks if a map meets all validation criteria. @@ -850,8 +850,8 @@ func (e *MapEvolver) checkConnectivity(m *Map) bool { queue = queue[1:] for _, d := range dirs { - nr := ((curr.Row + d.Row) % m.Rows + m.Rows) % m.Rows - nc := ((curr.Col + d.Col) % m.Cols + m.Cols) % m.Cols + nr := ((curr.Row+d.Row)%m.Rows + m.Rows) % m.Rows + nc := ((curr.Col+d.Col)%m.Cols + m.Cols) % m.Cols np := Position{Row: nr, Col: nc} if passable[np] && !visited[np] { @@ -893,8 +893,8 @@ func (e *MapEvolver) countReachableEnergyNodes(m *Map, start Position) int { } for _, d := range dirs { - nr := ((curr.Row + d.Row) % m.Rows + m.Rows) % m.Rows - nc := ((curr.Col + d.Col) % m.Cols + m.Cols) % m.Cols + nr := ((curr.Row+d.Row)%m.Rows + m.Rows) % m.Rows + nc := ((curr.Col+d.Col)%m.Cols + m.Cols) % m.Cols np := Position{Row: nr, Col: nc} if !wallSet[np] && !visited[np] { @@ -955,8 +955,8 @@ func (e *MapEvolver) canReach(m *Map, start, end Position) bool { } for _, d := range dirs { - nr := ((curr.Row + d.Row) % m.Rows + m.Rows) % m.Rows - nc := ((curr.Col + d.Col) % m.Cols + m.Cols) % m.Cols + nr := ((curr.Row+d.Row)%m.Rows + m.Rows) % m.Rows + nc := ((curr.Col+d.Col)%m.Cols + m.Cols) % m.Cols np := Position{Row: nr, Col: nc} if !wallSet[np] && !visited[np] { diff --git a/cmd/acb-map-evolver/main_test.go b/cmd/acb-map-evolver/main_test.go index 6e2ff83..eb2389e 100644 --- a/cmd/acb-map-evolver/main_test.go +++ b/cmd/acb-map-evolver/main_test.go @@ -160,7 +160,7 @@ func TestValidate(t *testing.T) { Players: 2, Rows: 60, Cols: 60, - WallDensity: 0.50, // Too high + WallDensity: 0.50, // Too high Walls: make([]Position, 1800), // 50% density Cores: []Core{ {Position: Position{Row: 15, Col: 30}, Owner: 0}, @@ -483,76 +483,76 @@ func TestNextScheduledTime(t *testing.T) { baseTime := time.Date(2025, 5, 12, 10, 0, 0, 0, time.UTC) // Monday tests := []struct { - name string - now time.Time - schedule WeeklySchedule - wantScheduled string // Expected scheduled time in RFC3339 - wantHour int - wantMinute int - wantWeekday time.Weekday + name string + now time.Time + schedule WeeklySchedule + wantScheduled string // Expected scheduled time in RFC3339 + wantHour int + wantMinute int + wantWeekday time.Weekday }{ { - name: "Sunday 03:00 when current time is Monday 10:00", - now: baseTime, // Monday 10:00 - schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, + name: "Sunday 03:00 when current time is Monday 10:00", + now: baseTime, // Monday 10:00 + schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-18T03:00:00Z", // Next Sunday - wantHour: 3, - wantMinute: 0, - wantWeekday: time.Sunday, + wantHour: 3, + wantMinute: 0, + wantWeekday: time.Sunday, }, { - name: "Monday 03:00 when current time is Monday 10:00 (should schedule next Monday)", - now: baseTime, // Monday 10:00 - schedule: WeeklySchedule{Weekday: time.Monday, Hour: 3, Minute: 0}, + name: "Monday 03:00 when current time is Monday 10:00 (should schedule next Monday)", + now: baseTime, // Monday 10:00 + schedule: WeeklySchedule{Weekday: time.Monday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-19T03:00:00Z", // Next Monday (passed today) - wantHour: 3, - wantMinute: 0, - wantWeekday: time.Monday, + wantHour: 3, + wantMinute: 0, + wantWeekday: time.Monday, }, { - name: "Monday 15:00 when current time is Monday 10:00 (same day, future)", - now: baseTime, // Monday 10:00 - schedule: WeeklySchedule{Weekday: time.Monday, Hour: 15, Minute: 0}, + name: "Monday 15:00 when current time is Monday 10:00 (same day, future)", + now: baseTime, // Monday 10:00 + schedule: WeeklySchedule{Weekday: time.Monday, Hour: 15, Minute: 0}, wantScheduled: "2025-05-12T15:00:00Z", // Same day - wantHour: 15, - wantMinute: 0, - wantWeekday: time.Monday, + wantHour: 15, + wantMinute: 0, + wantWeekday: time.Monday, }, { - name: "Wednesday 12:30 when current time is Monday 10:00", - now: baseTime, // Monday 10:00 - schedule: WeeklySchedule{Weekday: time.Wednesday, Hour: 12, Minute: 30}, + name: "Wednesday 12:30 when current time is Monday 10:00", + now: baseTime, // Monday 10:00 + schedule: WeeklySchedule{Weekday: time.Wednesday, Hour: 12, Minute: 30}, wantScheduled: "2025-05-14T12:30:00Z", // 2 days from now - wantHour: 12, - wantMinute: 30, - wantWeekday: time.Wednesday, + wantHour: 12, + wantMinute: 30, + wantWeekday: time.Wednesday, }, { - name: "Saturday 23:59 when current time is Monday 10:00", - now: baseTime, // Monday 10:00 - schedule: WeeklySchedule{Weekday: time.Saturday, Hour: 23, Minute: 59}, + name: "Saturday 23:59 when current time is Monday 10:00", + now: baseTime, // Monday 10:00 + schedule: WeeklySchedule{Weekday: time.Saturday, Hour: 23, Minute: 59}, wantScheduled: "2025-05-17T23:59:00Z", // 5 days from now - wantHour: 23, - wantMinute: 59, - wantWeekday: time.Saturday, + wantHour: 23, + wantMinute: 59, + wantWeekday: time.Saturday, }, { - name: "Default schedule (Sunday 03:00) on Saturday before midnight", - now: time.Date(2025, 5, 10, 23, 0, 0, 0, time.UTC), // Saturday 23:00 - schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, + name: "Default schedule (Sunday 03:00) on Saturday before midnight", + now: time.Date(2025, 5, 10, 23, 0, 0, 0, time.UTC), // Saturday 23:00 + schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-11T03:00:00Z", // Next day (Sunday) - wantHour: 3, - wantMinute: 0, - wantWeekday: time.Sunday, + wantHour: 3, + wantMinute: 0, + wantWeekday: time.Sunday, }, { - name: "Default schedule (Sunday 03:00) on Sunday after 03:00", - now: time.Date(2025, 5, 11, 5, 0, 0, 0, time.UTC), // Sunday 05:00 - schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, + name: "Default schedule (Sunday 03:00) on Sunday after 03:00", + now: time.Date(2025, 5, 11, 5, 0, 0, 0, time.UTC), // Sunday 05:00 + schedule: WeeklySchedule{Weekday: time.Sunday, Hour: 3, Minute: 0}, wantScheduled: "2025-05-18T03:00:00Z", // Next week (passed today) - wantHour: 3, - wantMinute: 0, - wantWeekday: time.Sunday, + wantHour: 3, + wantMinute: 0, + wantWeekday: time.Sunday, }, } @@ -641,19 +641,19 @@ func TestWeeklyScheduleEnvParsing(t *testing.T) { name: "Invalid weekday 7 (out of range 0-6)", envValue: "7:03:00", wantParseError: false, // Sscanf parses it fine - wantValid: false, // But validation rejects it + wantValid: false, // But validation rejects it }, { name: "Invalid hour 24 (out of range 0-23)", envValue: "0:24:00", wantParseError: false, // Sscanf parses it fine - wantValid: false, // But validation rejects it + wantValid: false, // But validation rejects it }, { name: "Invalid minute 60 (out of range 0-59)", envValue: "0:03:60", wantParseError: false, // Sscanf parses it fine - wantValid: false, // But validation rejects it + wantValid: false, // But validation rejects it }, { name: "Invalid format", diff --git a/cmd/acb-mapgen/connectivity.go b/cmd/acb-mapgen/connectivity.go index 219c658..991fe50 100644 --- a/cmd/acb-mapgen/connectivity.go +++ b/cmd/acb-mapgen/connectivity.go @@ -47,8 +47,8 @@ func CheckConnectivity(m *Map) bool { for _, d := range dirs { // Toroidal wrapping - nr := ((curr.Row + d.Row) % m.Rows + m.Rows) % m.Rows - nc := ((curr.Col + d.Col) % m.Cols + m.Cols) % m.Cols + nr := ((curr.Row+d.Row)%m.Rows + m.Rows) % m.Rows + nc := ((curr.Col+d.Col)%m.Cols + m.Cols) % m.Cols np := Position{Row: nr, Col: nc} if passable[np] && !visited[np] { diff --git a/cmd/acb-mapgen/main.go b/cmd/acb-mapgen/main.go index 12756ca..53183da 100644 --- a/cmd/acb-mapgen/main.go +++ b/cmd/acb-mapgen/main.go @@ -269,8 +269,8 @@ func generateMap(numPlayers, rows, cols int, wallDensity float64, numEnergyNodes if ndr == 0 && ndc == 0 { continue } - nr := ((r + ndr) % rows + rows) % rows - nc := ((c + ndc) % cols + cols) % cols + nr := ((r+ndr)%rows + rows) % rows + nc := ((c+ndc)%cols + cols) % cols if grid[nr][nc] { neighbors++ } @@ -345,4 +345,3 @@ func generateMap(numPlayers, rows, cols int, wallDensity float64, numEnergyNodes return m } - diff --git a/cmd/acb-matchmaker/main.go b/cmd/acb-matchmaker/main.go index 5a0e7ca..4f2dfb6 100644 --- a/cmd/acb-matchmaker/main.go +++ b/cmd/acb-matchmaker/main.go @@ -19,23 +19,23 @@ import ( ) type Config struct { - DatabaseURL string - ValkeyAddr string - ValkeyPassword string - EncryptionKey string // AES-256-GCM key for shared secret decryption - DiscordWebhook string - SlackWebhook string - MatchmakerSecs int - HealthCheckSecs int - ReaperSecs int - SeriesSchedSecs int - SeasonResetSecs int - FairnessAuditSecs int - FeaturedSchedSecs int // featured series check interval (Friday 20:00 UTC) - BotTimeoutSecs int - StaleJobMinutes int - MaxConsecFails int - SeasonDecayFactor float64 + DatabaseURL string + ValkeyAddr string + ValkeyPassword string + EncryptionKey string // AES-256-GCM key for shared secret decryption + DiscordWebhook string + SlackWebhook string + MatchmakerSecs int + HealthCheckSecs int + ReaperSecs int + SeriesSchedSecs int + SeasonResetSecs int + FairnessAuditSecs int + FeaturedSchedSecs int // featured series check interval (Friday 20:00 UTC) + BotTimeoutSecs int + StaleJobMinutes int + MaxConsecFails int + SeasonDecayFactor float64 } type Matchmaker struct { @@ -51,19 +51,19 @@ func loadConfig() Config { ValkeyAddr: envOr("ACB_VALKEY_ADDR", "localhost:6379"), ValkeyPassword: os.Getenv("ACB_VALKEY_PASSWORD"), EncryptionKey: os.Getenv("ACB_ENCRYPTION_KEY"), - DiscordWebhook: os.Getenv("ACB_DISCORD_WEBHOOK"), - SlackWebhook: os.Getenv("ACB_SLACK_WEBHOOK"), - MatchmakerSecs: envInt("ACB_MATCHMAKER_INTERVAL", 60), - HealthCheckSecs: envInt("ACB_HEALTHCHECK_INTERVAL", 900), - ReaperSecs: envInt("ACB_REAPER_INTERVAL", 300), - SeriesSchedSecs: envInt("ACB_SERIES_SCHED_INTERVAL", 120), - SeasonResetSecs: envInt("ACB_SEASON_RESET_INTERVAL", 300), - FairnessAuditSecs: envInt("ACB_FAIRNESS_AUDIT_INTERVAL", 3600), - FeaturedSchedSecs: envInt("ACB_FEATURED_SCHED_INTERVAL", 3600), // check hourly - BotTimeoutSecs: envInt("ACB_BOT_TIMEOUT", 5), - StaleJobMinutes: envInt("ACB_STALE_JOB_MINUTES", 15), - MaxConsecFails: envInt("ACB_MAX_CONSEC_FAILS", 3), - SeasonDecayFactor: envFloat("ACB_SEASON_DECAY_FACTOR", 0.7), + DiscordWebhook: os.Getenv("ACB_DISCORD_WEBHOOK"), + SlackWebhook: os.Getenv("ACB_SLACK_WEBHOOK"), + MatchmakerSecs: envInt("ACB_MATCHMAKER_INTERVAL", 60), + HealthCheckSecs: envInt("ACB_HEALTHCHECK_INTERVAL", 900), + ReaperSecs: envInt("ACB_REAPER_INTERVAL", 300), + SeriesSchedSecs: envInt("ACB_SERIES_SCHED_INTERVAL", 120), + SeasonResetSecs: envInt("ACB_SEASON_RESET_INTERVAL", 300), + FairnessAuditSecs: envInt("ACB_FAIRNESS_AUDIT_INTERVAL", 3600), + FeaturedSchedSecs: envInt("ACB_FEATURED_SCHED_INTERVAL", 3600), // check hourly + BotTimeoutSecs: envInt("ACB_BOT_TIMEOUT", 5), + StaleJobMinutes: envInt("ACB_STALE_JOB_MINUTES", 15), + MaxConsecFails: envInt("ACB_MAX_CONSEC_FAILS", 3), + SeasonDecayFactor: envFloat("ACB_SEASON_DECAY_FACTOR", 0.7), } } diff --git a/cmd/acb-matchmaker/map_fairness.go b/cmd/acb-matchmaker/map_fairness.go index a9c9f2d..0333411 100644 --- a/cmd/acb-matchmaker/map_fairness.go +++ b/cmd/acb-matchmaker/map_fairness.go @@ -9,12 +9,12 @@ import ( ) const ( - fairnessMinGames = 80 - fairnessThresholdPP = 0.10 + fairnessMinGames = 80 + fairnessThresholdPP = 0.10 voteForceRetireThreshold = -20 - engagementPrunePct = 0.10 - classicMinMonths = 3 - classicTopN = 5 + engagementPrunePct = 0.10 + classicMinMonths = 3 + classicTopN = 5 ) // tickFairnessAudit runs the full map lifecycle audit: diff --git a/cmd/acb-matchmaker/map_fairness_test.go b/cmd/acb-matchmaker/map_fairness_test.go index 45ed6be..a8d02ec 100644 --- a/cmd/acb-matchmaker/map_fairness_test.go +++ b/cmd/acb-matchmaker/map_fairness_test.go @@ -10,10 +10,10 @@ func TestFairnessThresholdCalculation(t *testing.T) { // For N-player maps, expected win rate is 1/N. // A slot is flagged unfair if its win rate deviates by > 10pp. tests := []struct { - name string - playerCount int - winRate float64 - shouldFlag bool + name string + playerCount int + winRate float64 + shouldFlag bool }{ {"2-player exact 50%", 2, 0.50, false}, {"2-player 59%", 2, 0.59, false}, @@ -71,7 +71,7 @@ func TestFairnessMinGamesThreshold(t *testing.T) { func TestVoteForceRetireThreshold(t *testing.T) { // Maps with >20 net negative votes are force-retired. tests := []struct { - netVotes int + netVotes int shouldRetire bool }{ {-25, true}, @@ -98,7 +98,7 @@ func TestEngagementPrunePercentage(t *testing.T) { totalActive int wantPruned int }{ - {5, 0}, // too few to prune + {5, 0}, // too few to prune {10, 1}, {20, 2}, {50, 5}, @@ -120,10 +120,10 @@ func TestClassicPromotionCriteria(t *testing.T) { // Maps must be active, have engagement > 0, be 3+ months old, // and be in the top 5 by engagement for their player count. tests := []struct { - name string - engagement float64 - ageMonths int - status string + name string + engagement float64 + ageMonths int + status string shouldPromote bool }{ {"meets all criteria", 8.5, 4, "active", true}, @@ -163,8 +163,8 @@ func TestFairnessAuditConfigOverride(t *testing.T) { func TestMonthlyPruneOnlyOnFirst(t *testing.T) { // pruneLowEngagementMaps only runs on the 1st of each month. tests := []struct { - day int - run bool + day int + run bool }{ {1, true}, {2, false}, diff --git a/cmd/acb-matchmaker/series_season.go b/cmd/acb-matchmaker/series_season.go index 8cef425..f49f713 100644 --- a/cmd/acb-matchmaker/series_season.go +++ b/cmd/acb-matchmaker/series_season.go @@ -257,12 +257,12 @@ func (m *Matchmaker) scheduleNextSeriesGames(ctx context.Context) error { defer rows.Close() type pendingSeries struct { - ID int64 - BotAID string - BotBID string - Format int - AWins int - BWins int + ID int64 + BotAID string + BotBID string + Format int + AWins int + BWins int LastGameNum int } var pending []pendingSeries @@ -828,9 +828,9 @@ func (m *Matchmaker) advanceChampionshipBracket(ctx context.Context) error { // Group by season and create semifinal matchups type semifinalPair struct { - seasonID int64 - position int - winners []string + seasonID int64 + position int + winners []string } pairs := make(map[string]*semifinalPair) for _, qf := range completed { @@ -1020,7 +1020,7 @@ func (m *Matchmaker) tickFeaturedSeries(ctx context.Context) { defer rows.Close() type botRating struct { - ID string + ID string Rating float64 } var topBots []botRating diff --git a/cmd/acb-matchmaker/series_season_test.go b/cmd/acb-matchmaker/series_season_test.go index 88c2b55..6b488f1 100644 --- a/cmd/acb-matchmaker/series_season_test.go +++ b/cmd/acb-matchmaker/series_season_test.go @@ -118,11 +118,11 @@ func TestDecayDifferentFactors(t *testing.T) { current float64 want float64 }{ - {0.0, 2000, 1500}, // full reset - {0.5, 2000, 1750}, // half decay - {1.0, 2000, 2000}, // no decay - {0.3, 1000, 1350}, // heavy decay toward center - {0.9, 1000, 1050}, // light decay + {0.0, 2000, 1500}, // full reset + {0.5, 2000, 1750}, // half decay + {1.0, 2000, 2000}, // no decay + {0.3, 1000, 1350}, // heavy decay toward center + {0.9, 1000, 1050}, // light decay } for _, tc := range tests { @@ -160,14 +160,14 @@ func TestSeriesFormatSelection(t *testing.T) { gap float64 format int }{ - {0, 7}, // identical ratings → bo7 - {25, 7}, // small gap → bo7 - {49, 7}, // just under threshold → bo7 - {50, 5}, // at threshold → bo5 - {100, 5}, // moderate gap → bo5 - {199, 5}, // just under threshold → bo5 - {200, 3}, // at threshold → bo3 - {500, 3}, // large gap → bo3 + {0, 7}, // identical ratings → bo7 + {25, 7}, // small gap → bo7 + {49, 7}, // just under threshold → bo7 + {50, 5}, // at threshold → bo5 + {100, 5}, // moderate gap → bo5 + {199, 5}, // just under threshold → bo5 + {200, 3}, // at threshold → bo3 + {500, 3}, // large gap → bo3 } for _, tc := range tests { @@ -569,11 +569,11 @@ func TestAllPlayedFinalization(t *testing.T) { // winning threshold (possible with draws), the series should be finalized // with the bot that has more wins, or NULL if equal. tests := []struct { - name string - format int - aWins int - bWins int - winner string // "a", "b", or "" for draw + name string + format int + aWins int + bWins int + winner string // "a", "b", or "" for draw }{ {"bo3 with 1-1 and 1 draw", 3, 1, 1, ""}, {"bo5 with 2-1 and 2 draws", 5, 2, 1, "a"}, diff --git a/cmd/acb-matchmaker/tickers_test.go b/cmd/acb-matchmaker/tickers_test.go index 2eaaffb..b953dcc 100644 --- a/cmd/acb-matchmaker/tickers_test.go +++ b/cmd/acb-matchmaker/tickers_test.go @@ -99,8 +99,8 @@ func TestSelectOpponents_ParetoDistribution(t *testing.T) { pool := make([]candidateBot, 20) for i := range pool { pool[i] = candidateBot{ - ID: fmt.Sprintf("bot_%02d", i), - Mu: 1400 + float64(i)*10, + ID: fmt.Sprintf("bot_%02d", i), + Mu: 1400 + float64(i)*10, LastPairedAt: time.Time{}, Games24h: 0, } @@ -187,7 +187,7 @@ func TestGridForPlayers(t *testing.T) { minArea int maxArea int }{ - {2, 1200, 2000}, // 40x40 = 1600 (reduced from 60x60 for combat density) + {2, 1200, 2000}, // 40x40 = 1600 (reduced from 60x60 for combat density) {3, 4000, 6000}, {4, 5000, 8500}, {6, 7000, 12000}, diff --git a/cmd/acb-wasm/bot-template/main.go b/cmd/acb-wasm/bot-template/main.go index 19a654e..50cabcd 100644 --- a/cmd/acb-wasm/bot-template/main.go +++ b/cmd/acb-wasm/bot-template/main.go @@ -4,8 +4,9 @@ // Compile with: GOOS=js GOARCH=wasm go build -o mybot.wasm . // // The bot exports an 'acbBot' global object with: -// init(configJSON: string) - called once at match start -// compute_moves(stateJSON: string) - called each turn, returns moves JSON +// +// init(configJSON: string) - called once at match start +// compute_moves(stateJSON: string) - called each turn, returns moves JSON package main import ( @@ -17,9 +18,9 @@ import ( // botState holds persistent state across turns (e.g., pathfinding cache). type botState struct { - config engine.Config - myID int - knownPos map[string]bool // positions we've seen + config engine.Config + myID int + knownPos map[string]bool // positions we've seen } var state = &botState{ @@ -90,7 +91,7 @@ func computeMoves(visible *engine.VisibleState) []engine.Move { } moves = append(moves, engine.Move{ - Position: bot.Position, + Position: bot.Position, Direction: dir, }) } @@ -115,8 +116,8 @@ func bestFleeDir(from engine.Position, enemies map[engine.Position]bool) engine. for _, d := range []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW} { dr, dc := d.Delta() np := engine.Position{ - Row: ((from.Row + dr) % state.config.Rows + state.config.Rows) % state.config.Rows, - Col: ((from.Col + dc) % state.config.Cols + state.config.Cols) % state.config.Cols, + Row: ((from.Row+dr)%state.config.Rows + state.config.Rows) % state.config.Rows, + Col: ((from.Col+dc)%state.config.Cols + state.config.Cols) % state.config.Cols, } minDist := 1 << 30 @@ -146,8 +147,8 @@ func towardNearest(from engine.Position, targets map[engine.Position]bool) engin for _, d := range []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW} { dr, dc := d.Delta() np := engine.Position{ - Row: ((from.Row + dr) % state.config.Rows + state.config.Rows) % state.config.Rows, - Col: ((from.Col + dc) % state.config.Cols + state.config.Cols) % state.config.Cols, + Row: ((from.Row+dr)%state.config.Rows + state.config.Rows) % state.config.Rows, + Col: ((from.Col+dc)%state.config.Cols + state.config.Cols) % state.config.Cols, } for t := range targets { @@ -188,7 +189,7 @@ func main() { done := make(chan struct{}) js.Global().Set("acbBot", js.ValueOf(map[string]interface{}{ - "init": js.FuncOf(jsInit), + "init": js.FuncOf(jsInit), "compute_moves": js.FuncOf(jsComputeMoves), })) diff --git a/cmd/acb-wasm/main.go b/cmd/acb-wasm/main.go index a56ea12..32d2ae2 100644 --- a/cmd/acb-wasm/main.go +++ b/cmd/acb-wasm/main.go @@ -225,7 +225,7 @@ func jsAddPlayer(_ js.Value, args []js.Value) interface{} { }) return map[string]interface{}{ - "ok": true, + "ok": true, "index": len(jsPlayers) - 1, } } diff --git a/cmd/acb-worker/api.go b/cmd/acb-worker/api.go index cc631bc..04898f5 100644 --- a/cmd/acb-worker/api.go +++ b/cmd/acb-worker/api.go @@ -43,15 +43,15 @@ type Match struct { // Participant represents a match participant. type Participant struct { - ID string `json:"id"` - MatchID string `json:"match_id"` - BotID string `json:"bot_id"` - PlayerIndex int `json:"player_index"` - Score int `json:"score"` - RatingBefore int `json:"rating_before"` - RatingAfter *int `json:"rating_after"` - RatingDeviationBefore int `json:"rating_deviation_before"` - RatingDeviationAfter *int `json:"rating_deviation_after"` + ID string `json:"id"` + MatchID string `json:"match_id"` + BotID string `json:"bot_id"` + PlayerIndex int `json:"player_index"` + Score int `json:"score"` + RatingBefore int `json:"rating_before"` + RatingAfter *int `json:"rating_after"` + RatingDeviationBefore int `json:"rating_deviation_before"` + RatingDeviationAfter *int `json:"rating_deviation_after"` } // MapData represents map configuration. @@ -83,7 +83,7 @@ type MatchResult struct { EndReason string `json:"end_reason"` Scores map[string]int `json:"scores"` CrashedBots map[string]bool `json:"crashed_bots"` // bot_id -> crashed - CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death + CombatTurns int `json:"combat_turns"` // turns with ≥1 enemy-kill combat death } // ConvertDBJobToJob converts a DBJob to Job type. @@ -115,12 +115,12 @@ func ConvertDBClaimToResponse(data *JobClaimData) *JobClaimResponse { for i, p := range data.Participants { participants[i] = Participant{ - ID: p.MatchID + "-" + p.BotID, - MatchID: p.MatchID, - BotID: p.BotID, - PlayerIndex: p.PlayerSlot, - Score: p.Score, - RatingBefore: int(p.RatingMuBefore), + ID: p.MatchID + "-" + p.BotID, + MatchID: p.MatchID, + BotID: p.BotID, + PlayerIndex: p.PlayerSlot, + Score: p.Score, + RatingBefore: int(p.RatingMuBefore), RatingDeviationBefore: int(p.RatingPhiBefore), } botSecrets[i] = BotSecret{ diff --git a/cmd/acb-worker/db.go b/cmd/acb-worker/db.go index 43b04ec..a96e7ea 100644 --- a/cmd/acb-worker/db.go +++ b/cmd/acb-worker/db.go @@ -69,16 +69,16 @@ type DBMatch struct { // DBParticipant represents a match participant. type DBParticipant struct { - MatchID string `json:"match_id"` - BotID string `json:"bot_id"` - PlayerSlot int `json:"player_slot"` - Score int `json:"score"` - RatingMuBefore float64 - RatingPhiBefore float64 - RatingSigmaBefore float64 - RatingMuAfter *float64 - RatingPhiAfter *float64 - RatingSigmaAfter *float64 + MatchID string `json:"match_id"` + BotID string `json:"bot_id"` + PlayerSlot int `json:"player_slot"` + Score int `json:"score"` + RatingMuBefore float64 + RatingPhiBefore float64 + RatingSigmaBefore float64 + RatingMuAfter *float64 + RatingPhiAfter *float64 + RatingSigmaAfter *float64 } // DBBotInfo contains bot endpoint and secret information. @@ -692,16 +692,16 @@ func (c *DBClient) UpdateMapEngagement(ctx context.Context, mapID string, engage // CompletedMatchForRecalc represents a completed match with participants for rating recalculation. type CompletedMatchForRecalc struct { - ID string - CompletedAt time.Time - Winner *int // player_slot of winner, nil for draw - WinnerBotID *string // bot_id of winner (derived from winner player_slot) + ID string + CompletedAt time.Time + Winner *int // player_slot of winner, nil for draw + WinnerBotID *string // bot_id of winner (derived from winner player_slot) Participants []MatchParticipantForRecalc } // MatchParticipantForRecalc represents a match participant for rating recalculation. type MatchParticipantForRecalc struct { - BotID string + BotID string PlayerSlot int } diff --git a/cmd/acb-worker/glicko2.go b/cmd/acb-worker/glicko2.go index 8c353c9..90a4c3a 100644 --- a/cmd/acb-worker/glicko2.go +++ b/cmd/acb-worker/glicko2.go @@ -6,10 +6,10 @@ import "math" const ( glicko2Scale = 173.7178 - glicko2Tau = 0.5 // Volatility parameter (tau) + glicko2Tau = 0.5 // Volatility parameter (tau) glicko2DefaultMu = 1500.0 glicko2DefaultRD = 350.0 - glicko2DefaultSigma = 0.06 // Default volatility/sigma for new bots + glicko2DefaultSigma = 0.06 // Default volatility/sigma for new bots glicko2Epsilon = 1e-6 ) diff --git a/cmd/acb-worker/main.go b/cmd/acb-worker/main.go index 173df0f..85bd692 100644 --- a/cmd/acb-worker/main.go +++ b/cmd/acb-worker/main.go @@ -76,20 +76,20 @@ func main() { DatabaseURL: *databaseURL, EncryptionKey: *encryptionKey, B2Endpoint: *b2Endpoint, - B2Bucket: *b2Bucket, - B2AccessKey: *b2AccessKey, - B2SecretKey: *b2SecretKey, - B2Region: *b2Region, - R2Endpoint: *r2Endpoint, - R2Bucket: *r2Bucket, - R2AccessKey: *r2AccessKey, - R2SecretKey: *r2SecretKey, - WorkerID: *workerID, - PollPeriod: *pollPeriod, - Heartbeat: *heartbeat, - TurnTimeout: *turnTimeout, - MaxRetries: *maxRetries, - Verbose: *verbose, + B2Bucket: *b2Bucket, + B2AccessKey: *b2AccessKey, + B2SecretKey: *b2SecretKey, + B2Region: *b2Region, + R2Endpoint: *r2Endpoint, + R2Bucket: *r2Bucket, + R2AccessKey: *r2AccessKey, + R2SecretKey: *r2SecretKey, + WorkerID: *workerID, + PollPeriod: *pollPeriod, + Heartbeat: *heartbeat, + TurnTimeout: *turnTimeout, + MaxRetries: *maxRetries, + Verbose: *verbose, } // Create database client diff --git a/cmd/acb-worker/metrics.go b/cmd/acb-worker/metrics.go index 69ed8d8..79aa829 100644 --- a/cmd/acb-worker/metrics.go +++ b/cmd/acb-worker/metrics.go @@ -28,10 +28,10 @@ type Metrics struct { heartbeatErrors atomic.Int64 // Histograms (stored as individual observations) - mu sync.Mutex - matchDurations []float64 // seconds + mu sync.Mutex + matchDurations []float64 // seconds replayUploadDurations []float64 // seconds - replaySizes []float64 // bytes + replaySizes []float64 // bytes // State startTime time.Time diff --git a/engine/bot_http.go b/engine/bot_http.go index 8a3ef53..7df93f8 100644 --- a/engine/bot_http.go +++ b/engine/bot_http.go @@ -19,7 +19,7 @@ type HTTPBot struct { matchID string turn int crashed bool - failCount int // consecutive failures + failCount int // consecutive failures lastDebug *DebugInfo // debug info from last response } @@ -74,16 +74,16 @@ func (b *HTTPBot) IsCrashed() bool { // MoveResponse represents the JSON response from a bot. type MoveResponse struct { - Moves []Move `json:"moves"` + Moves []Move `json:"moves"` Debug *DebugInfo `json:"debug,omitempty"` } // DebugInfo contains optional debug telemetry from the bot. type DebugInfo struct { Reasoning string `json:"reasoning,omitempty"` - Targets []DebugTarget `json:"targets,omitempty"` + Targets []DebugTarget `json:"targets,omitempty"` Values map[string]interface{} `json:"values,omitempty"` - Heatmap *DebugHeatmap `json:"heatmap,omitempty"` + Heatmap *DebugHeatmap `json:"heatmap,omitempty"` } // DebugTarget represents a debug target marker. @@ -95,8 +95,8 @@ type DebugTarget struct { // DebugHeatmap represents a 2D grid overlay for visualization. type DebugHeatmap struct { - Name string `json:"name"` // e.g., "threat", "influence" - Data [][]float64 `json:"data"` // 2D array of values (row-major) + Name string `json:"name"` // e.g., "threat", "influence" + Data [][]float64 `json:"data"` // 2D array of values (row-major) } // GetMoves sends the game state to the bot and returns its moves. diff --git a/engine/bot_http_test.go b/engine/bot_http_test.go index f13922d..47eb199 100644 --- a/engine/bot_http_test.go +++ b/engine/bot_http_test.go @@ -220,7 +220,7 @@ func TestHTTPBot_ValidateMoves(t *testing.T) { Score int `json:"score"` }{ID: 0}, Bots: []VisibleBot{ - {Position: Position{Row: 5, Col: 5}, Owner: 0}, // Our bot + {Position: Position{Row: 5, Col: 5}, Owner: 0}, // Our bot {Position: Position{Row: 10, Col: 10}, Owner: 1}, // Enemy bot }, } diff --git a/engine/bot_strategies.go b/engine/bot_strategies.go index af3d88c..90c0ff6 100644 --- a/engine/bot_strategies.go +++ b/engine/bot_strategies.go @@ -262,8 +262,8 @@ func (b *GathererBot) getExploreMove( // RusherBot aggressively rushes toward enemy cores. type RusherBot struct { - rng *rand.Rand - knownEnemyCores map[Position]bool + rng *rand.Rand + knownEnemyCores map[Position]bool } // NewRusherBot creates a new rusher bot. @@ -875,8 +875,8 @@ func (b *SwarmBot) maintainsCohesion(newPos, oldPos Position, myBotPositions map // HunterBot targets isolated enemy units. type HunterBot struct { - rng *rand.Rand - enemyTrackers map[Position]*enemyTracker + rng *rand.Rand + enemyTrackers map[Position]*enemyTracker } type enemyTracker struct { diff --git a/engine/game.go b/engine/game.go index 5380adc..35651a4 100644 --- a/engine/game.go +++ b/engine/game.go @@ -8,23 +8,23 @@ import ( // GameState represents the complete state of a match. type GameState struct { - Config Config - Grid *Grid - Bots []*Bot - Cores []*Core - Energy []*EnergyNode - Players []*Player - Turn int + Config Config + Grid *Grid + Bots []*Bot + Cores []*Core + Energy []*EnergyNode + Players []*Player + Turn int MatchID string NextBotID int NextCoreID int rng *rand.Rand // Turn state - Moves map[int]Move // bot ID -> move - DeadBots []*Bot // bots that died this turn (for fog display) - Events []Event // events that occurred this turn - Dominance map[int]int // player -> consecutive turns with 80%+ bots + Moves map[int]Move // bot ID -> move + DeadBots []*Bot // bots that died this turn (for fog display) + Events []Event // events that occurred this turn + Dominance map[int]int // player -> consecutive turns with 80%+ bots // Stalemate detection StalemateTurns int // consecutive turns with no progress @@ -32,9 +32,9 @@ type GameState struct { LastTotalBots int // total living bots at last progress // Zone (storm) state - ZoneCenter Position // center of the zone (map center) - ZoneRadius int // current radius of the safe zone - ZoneActive bool // whether the zone is currently shrinking + ZoneCenter Position // center of the zone (map center) + ZoneRadius int // current radius of the safe zone + ZoneActive bool // whether the zone is currently shrinking } // Event represents something that happened during a turn. @@ -46,13 +46,13 @@ type Event struct { // Event types const ( - EventBotSpawned = "bot_spawned" - EventBotDied = "bot_died" + EventBotSpawned = "bot_spawned" + EventBotDied = "bot_died" EventEnergyCollected = "energy_collected" - EventCoreCaptured = "core_captured" - EventCombatDeath = "combat_death" - EventCollisionDeath = "collision_death" - EventZoneDeath = "zone_death" + EventCoreCaptured = "core_captured" + EventCombatDeath = "combat_death" + EventCollisionDeath = "collision_death" + EventZoneDeath = "zone_death" ) // NewGameState creates a new game state with the given configuration. @@ -61,20 +61,20 @@ func NewGameState(config Config, rng *rand.Rand) *GameState { initialRadius := min(config.Rows, config.Cols) / 2 return &GameState{ - Config: config, - Grid: NewGrid(config.Rows, config.Cols), - Bots: make([]*Bot, 0), - Cores: make([]*Core, 0), - Energy: make([]*EnergyNode, 0), - Players: make([]*Player, 0), - Turn: 0, - MatchID: generateMatchID(rng), - NextBotID: 0, - rng: rng, - Moves: make(map[int]Move), - DeadBots: make([]*Bot, 0), - Events: make([]Event, 0), - Dominance: make(map[int]int), + Config: config, + Grid: NewGrid(config.Rows, config.Cols), + Bots: make([]*Bot, 0), + Cores: make([]*Core, 0), + Energy: make([]*EnergyNode, 0), + Players: make([]*Player, 0), + Turn: 0, + MatchID: generateMatchID(rng), + NextBotID: 0, + rng: rng, + Moves: make(map[int]Move), + DeadBots: make([]*Bot, 0), + Events: make([]Event, 0), + Dominance: make(map[int]int), ZoneCenter: center, ZoneRadius: initialRadius, ZoneActive: false, diff --git a/engine/grid.go b/engine/grid.go index 7bce5d3..9343ac0 100644 --- a/engine/grid.go +++ b/engine/grid.go @@ -6,10 +6,10 @@ import ( // Grid represents the toroidal game board. type Grid struct { - Rows int - Cols int - Tiles [][]Tile - Walls map[Position]bool // cached wall positions for fast lookup + Rows int + Cols int + Tiles [][]Tile + Walls map[Position]bool // cached wall positions for fast lookup } // NewGrid creates a new empty grid with the given dimensions. diff --git a/engine/grid_test.go b/engine/grid_test.go index 348c272..345eeda 100644 --- a/engine/grid_test.go +++ b/engine/grid_test.go @@ -15,11 +15,11 @@ func TestGridWrap(t *testing.T) { }{ {0, 0, Position{0, 0}}, {59, 59, Position{59, 59}}, - {60, 0, Position{0, 0}}, // wrap row - {0, 60, Position{0, 0}}, // wrap col - {-1, 0, Position{59, 0}}, // negative wrap row - {0, -1, Position{0, 59}}, // negative wrap col - {65, 65, Position{5, 5}}, // both wrap + {60, 0, Position{0, 0}}, // wrap row + {0, 60, Position{0, 0}}, // wrap col + {-1, 0, Position{59, 0}}, // negative wrap row + {0, -1, Position{0, 59}}, // negative wrap col + {65, 65, Position{5, 5}}, // both wrap {-5, -5, Position{55, 55}}, // both negative wrap } @@ -45,10 +45,10 @@ func TestGridDistance2(t *testing.T) { {Position{10, 10}, Position{13, 14}, 25}, // Toroidal wrapping - shorter path across boundary - {Position{0, 0}, Position{59, 0}, 1}, // distance 1 via wrap - {Position{0, 0}, Position{58, 0}, 4}, // distance 2 via wrap - {Position{0, 0}, Position{0, 59}, 1}, // distance 1 via wrap col - {Position{0, 0}, Position{59, 59}, 2}, // distance sqrt(2) via corner wrap + {Position{0, 0}, Position{59, 0}, 1}, // distance 1 via wrap + {Position{0, 0}, Position{58, 0}, 4}, // distance 2 via wrap + {Position{0, 0}, Position{0, 59}, 1}, // distance 1 via wrap col + {Position{0, 0}, Position{59, 59}, 2}, // distance sqrt(2) via corner wrap } for _, tt := range tests { @@ -269,11 +269,11 @@ func TestINV6_ToroidalBounds(t *testing.T) { rows int cols int }{ - {30, 30}, // Minimum - {40, 60}, // Rectangular - {60, 60}, // Standard square + {30, 30}, // Minimum + {40, 60}, // Rectangular + {60, 60}, // Standard square {100, 100}, // Large - {120, 80}, // Large rectangular + {120, 80}, // Large rectangular {200, 200}, // Maximum } @@ -297,7 +297,7 @@ func testToroidalBoundsScenario(t *testing.T, rng *rand.Rand, rows, cols int) { g := NewGrid(rows, cols) // Add random walls - numWalls := rng.Intn(rows*cols/20) // Up to 5% wall density + numWalls := rng.Intn(rows * cols / 20) // Up to 5% wall density for i := 0; i < numWalls; i++ { row := rng.Intn(rows*3) - rows // Can be negative or >= rows col := rng.Intn(cols*3) - cols @@ -325,7 +325,7 @@ func testToroidalBoundsScenario(t *testing.T, rng *rand.Rand, rows, cols int) { // Test Move from random positions in all directions testPositions := []Position{ {0, 0}, {0, cols - 1}, {rows - 1, 0}, {rows - 1, cols - 1}, // Corners - {rows / 2, cols / 2}, // Center + {rows / 2, cols / 2}, // Center {0, cols / 2}, {rows - 1, cols / 2}, // Middle of edges } diff --git a/engine/integration_test.go b/engine/integration_test.go index b8666c6..4b6c003 100644 --- a/engine/integration_test.go +++ b/engine/integration_test.go @@ -195,6 +195,7 @@ func createMockBotServer(t *testing.T, secret string, playerID int) *httptest.Se w.Write(body) })) } + // TestIntegration_CenterWeightedEnergy verifies that energy nodes are biased // toward the map center to force contested energy collection. func TestIntegration_CenterWeightedEnergy(t *testing.T) { diff --git a/engine/map_engagement.go b/engine/map_engagement.go index fd455f6..9050601 100644 --- a/engine/map_engagement.go +++ b/engine/map_engagement.go @@ -15,7 +15,8 @@ type MapEngagementScore struct { // CalculateMapEngagement computes the engagement score for a map based on replay data. // The engagement formula (from plan §14.6, extended for combat density) is: // score = win_prob_crossings * 3.0 + combat_deaths * 3.0 + critical_moments * 2.0 + -// resource_contest_turns * 1.5 + survival_turns * 0.5 +// +// resource_contest_turns * 1.5 + survival_turns * 0.5 func CalculateMapEngagement(replay *Replay) MapEngagementScore { if replay == nil || len(replay.Turns) == 0 { return MapEngagementScore{} diff --git a/engine/map_engagement_test.go b/engine/map_engagement_test.go index 19a87eb..e5a9388 100644 --- a/engine/map_engagement_test.go +++ b/engine/map_engagement_test.go @@ -81,7 +81,7 @@ func TestMapEngagement_ResourceContestTurns(t *testing.T) { Result: &MatchResult{Turns: 50, Scores: []int{5, 4}}, Turns: []ReplayTurn{ { - Turn: 0, + Turn: 0, Energy: []Position{{Row: 5, Col: 5}}, Bots: []ReplayBot{ {Position: Position{Row: 5, Col: 4}, Alive: true, Owner: 0}, // Adjacent to energy @@ -89,7 +89,7 @@ func TestMapEngagement_ResourceContestTurns(t *testing.T) { }, }, { - Turn: 1, + Turn: 1, Energy: []Position{{Row: 10, Col: 10}}, Bots: []ReplayBot{ {Position: Position{Row: 10, Col: 9}, Alive: true, Owner: 0}, // Only player 0 adjacent @@ -178,7 +178,7 @@ func TestMapEngagement_Formula(t *testing.T) { }, Turns: []ReplayTurn{ { - Turn: 0, + Turn: 0, Energy: []Position{{Row: 5, Col: 5}}, Bots: []ReplayBot{ {Position: Position{Row: 5, Col: 4}, Alive: true, Owner: 0}, @@ -186,7 +186,7 @@ func TestMapEngagement_Formula(t *testing.T) { }, }, { - Turn: 1, + Turn: 1, Energy: []Position{{Row: 10, Col: 10}}, Bots: []ReplayBot{ {Position: Position{Row: 0, Col: 0}, Alive: true, Owner: 0}, @@ -203,8 +203,8 @@ func TestMapEngagement_Formula(t *testing.T) { score := CalculateMapEngagement(replay) // Count each metric - winProbCrossings := 1.0 // One lead change - combatDeaths := 0 // No combat deaths in this replay + winProbCrossings := 1.0 // One lead change + combatDeaths := 0 // No combat deaths in this replay criticalMoments := 1 // One critical moment resourceContestTurns := 1 // Turn 0 has contested energy survivalTurns := 2 // Both turns have all players alive @@ -244,14 +244,14 @@ func TestMapEngagement_NoContestedEnergy(t *testing.T) { Result: &MatchResult{Turns: 50, Scores: []int{5, 4}}, Turns: []ReplayTurn{ { - Turn: 0, + Turn: 0, Energy: []Position{{Row: 5, Col: 5}}, Bots: []ReplayBot{ {Position: Position{Row: 5, Col: 4}, Alive: true, Owner: 0}, // Only player 0 adjacent }, }, { - Turn: 1, + Turn: 1, Energy: []Position{}, // No energy Bots: []ReplayBot{ {Position: Position{Row: 0, Col: 0}, Alive: true, Owner: 0}, diff --git a/engine/match.go b/engine/match.go index 75964b7..a69255c 100644 --- a/engine/match.go +++ b/engine/match.go @@ -11,13 +11,13 @@ import ( // MatchRunner orchestrates a match between multiple bots. type MatchRunner struct { - config Config - bots []BotInterface - names []string - rng *rand.Rand - verbose bool - logger *log.Logger - timeout time.Duration // per-turn timeout + config Config + bots []BotInterface + names []string + rng *rand.Rand + verbose bool + logger *log.Logger + timeout time.Duration // per-turn timeout } // MatchOption is a functional option for MatchRunner. @@ -390,4 +390,3 @@ func (mr *MatchRunner) isValidWallPosition(gs *GameState, pos Position) bool { } return true } - diff --git a/engine/replay.go b/engine/replay.go index 40ddd73..5ad9c66 100644 --- a/engine/replay.go +++ b/engine/replay.go @@ -9,32 +9,32 @@ import ( // Replay records the complete history of a match for playback. type Replay struct { - FormatVersion string `json:"format_version"` // semver, e.g. "1.0" - MatchID string `json:"match_id"` - Config Config `json:"config"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - Result *MatchResult `json:"result"` - Players []ReplayPlayer `json:"players"` - Map ReplayMap `json:"map"` - Turns []ReplayTurn `json:"turns"` - WinProb []WinProbEntry `json:"win_prob,omitempty"` - CriticalMoments []CriticalMoment `json:"critical_moments,omitempty"` + FormatVersion string `json:"format_version"` // semver, e.g. "1.0" + MatchID string `json:"match_id"` + Config Config `json:"config"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Result *MatchResult `json:"result"` + Players []ReplayPlayer `json:"players"` + Map ReplayMap `json:"map"` + Turns []ReplayTurn `json:"turns"` + WinProb []WinProbEntry `json:"win_prob,omitempty"` + CriticalMoments []CriticalMoment `json:"critical_moments,omitempty"` } // ReplayPlayer represents player info in a replay. type ReplayPlayer struct { - ID int `json:"id"` - Name string `json:"name"` + ID int `json:"id"` + Name string `json:"name"` } // ReplayMap represents the static map data. type ReplayMap struct { - Rows int `json:"rows"` - Cols int `json:"cols"` - Walls []Position `json:"walls"` - Cores []ReplayCore `json:"cores"` - EnergyNodes []Position `json:"energy_nodes"` + Rows int `json:"rows"` + Cols int `json:"cols"` + Walls []Position `json:"walls"` + Cores []ReplayCore `json:"cores"` + EnergyNodes []Position `json:"energy_nodes"` } // ReplayCore represents a core in the replay. @@ -45,15 +45,15 @@ type ReplayCore struct { // ReplayTurn represents the state at a single turn. type ReplayTurn struct { - Turn int `json:"turn"` - Bots []ReplayBot `json:"bots"` - Cores []ReplayCoreState `json:"cores"` - Energy []Position `json:"energy"` - Scores []int `json:"scores"` - EnergyHeld []int `json:"energy_held"` - Events []Event `json:"events,omitempty"` - Debug map[int]*DebugInfo `json:"debug,omitempty"` // optional bot debug telemetry - ZoneBounds *ZoneBounds `json:"zone_bounds,omitempty"` // active zone bounds if enabled + Turn int `json:"turn"` + Bots []ReplayBot `json:"bots"` + Cores []ReplayCoreState `json:"cores"` + Energy []Position `json:"energy"` + Scores []int `json:"scores"` + EnergyHeld []int `json:"energy_held"` + Events []Event `json:"events,omitempty"` + Debug map[int]*DebugInfo `json:"debug,omitempty"` // optional bot debug telemetry + ZoneBounds *ZoneBounds `json:"zone_bounds,omitempty"` // active zone bounds if enabled } // ReplayBot represents a bot in a replay turn. diff --git a/engine/thumbnail.go b/engine/thumbnail.go index 04468c7..78e654d 100644 --- a/engine/thumbnail.go +++ b/engine/thumbnail.go @@ -9,15 +9,15 @@ import ( // ThumbnailConfig configures thumbnail rendering. type ThumbnailConfig struct { - Width int - Height int - CellSize int - Background color.Color - WallColor color.Color - GridColor color.Color + Width int + Height int + CellSize int + Background color.Color + WallColor color.Color + GridColor color.Color PlayerColors []color.Color - EnergyColor color.Color - CoreColor color.Color + EnergyColor color.Color + CoreColor color.Color } // DefaultThumbnailConfig returns the default thumbnail configuration. @@ -30,8 +30,8 @@ func DefaultThumbnailConfig() ThumbnailConfig { WallColor: color.RGBA{60, 60, 70, 255}, GridColor: color.RGBA{30, 30, 40, 255}, PlayerColors: []color.Color{ - color.RGBA{66, 165, 245, 255}, // Blue - color.RGBA{239, 83, 80, 255}, // Red + color.RGBA{66, 165, 245, 255}, // Blue + color.RGBA{239, 83, 80, 255}, // Red color.RGBA{102, 187, 106, 255}, // Green color.RGBA{255, 202, 40, 255}, // Yellow color.RGBA{171, 71, 188, 255}, // Purple diff --git a/engine/turn.go b/engine/turn.go index 5a60b5e..9f1fe3d 100644 --- a/engine/turn.go +++ b/engine/turn.go @@ -51,7 +51,7 @@ func (gs *GameState) ExecuteTurn() *MatchResult { // executeMoves processes all submitted moves. func (gs *GameState) executeMoves() { // First, compute intended destinations - intended := make(map[int]Position) // bot ID -> intended position + intended := make(map[int]Position) // bot ID -> intended position botsAtPos := make(map[Position][]*Bot) // position -> bots trying to move there for _, b := range gs.Bots { @@ -150,7 +150,7 @@ func (gs *GameState) executeZone() { // executeCombat resolves the focus-fire combat algorithm. func (gs *GameState) executeCombat() { // For each bot, count enemies within attack radius - enemyCounts := make(map[int]int) // bot ID -> enemy count + enemyCounts := make(map[int]int) // bot ID -> enemy count botsInRadius := make(map[int][]*Bot) // bot ID -> enemies within radius for _, b := range gs.Bots { @@ -210,8 +210,8 @@ func (gs *GameState) executeCombat() { var killers []map[string]interface{} for _, e := range botsInRadius[b.ID] { killers = append(killers, map[string]interface{}{ - "bot_id": e.ID, - "owner": e.Owner, + "bot_id": e.ID, + "owner": e.Owner, "position": e.Position, }) } diff --git a/starters/go/game/grid_test.go b/starters/go/game/grid_test.go index fa177a6..525213b 100644 --- a/starters/go/game/grid_test.go +++ b/starters/go/game/grid_test.go @@ -6,10 +6,10 @@ import ( func TestToroidalManhattan(t *testing.T) { tests := []struct { - name string - a, b Position + name string + a, b Position rows, cols int - want int + want int }{ { name: "adjacent", @@ -52,10 +52,10 @@ func TestToroidalManhattan(t *testing.T) { func TestToroidalDistance2(t *testing.T) { tests := []struct { - name string - a, b Position + name string + a, b Position rows, cols int - want int + want int }{ { name: "adjacent", @@ -116,11 +116,11 @@ func TestNeighbors(t *testing.T) { func TestNeighborInDirection(t *testing.T) { tests := []struct { - name string - pos Position - dir Direction + name string + pos Position + dir Direction rows, cols int - want Position + want Position }{ { name: "north", diff --git a/starters/go/game/types.go b/starters/go/game/types.go index 5e136ed..e070a50 100644 --- a/starters/go/game/types.go +++ b/starters/go/game/types.go @@ -55,15 +55,15 @@ type VisibleCore struct { // GameState represents the fog-filtered game state for a single turn. type GameState struct { - MatchID string `json:"match_id"` - Turn int `json:"turn"` - Config GameConfig `json:"config"` - You PlayerInfo `json:"you"` - Bots []VisibleBot `json:"bots"` - Energy []Position `json:"energy"` + MatchID string `json:"match_id"` + Turn int `json:"turn"` + Config GameConfig `json:"config"` + You PlayerInfo `json:"you"` + Bots []VisibleBot `json:"bots"` + Energy []Position `json:"energy"` Cores []VisibleCore `json:"cores"` - Walls []Position `json:"walls"` - Dead []VisibleBot `json:"dead"` + Walls []Position `json:"walls"` + Dead []VisibleBot `json:"dead"` } // PlayerInfo contains information about the current player. diff --git a/starters/go/main.go b/starters/go/main.go index 8d1aa73..b50364a 100644 --- a/starters/go/main.go +++ b/starters/go/main.go @@ -108,16 +108,17 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { // - game.Neighbors() for getting adjacent positions // // Example: -// moves := []game.Move{} -// for _, bot := range state.Bots { -// if bot.Owner == state.You.ID { -// moves = append(moves, game.Move{ -// Position: bot.Position, -// Direction: game.DirN, // Move north -// }) -// } -// } -// return moves +// +// moves := []game.Move{} +// for _, bot := range state.Bots { +// if bot.Owner == state.You.ID { +// moves = append(moves, game.Move{ +// Position: bot.Position, +// Direction: game.DirN, // Move north +// }) +// } +// } +// return moves func computeMoves(state *game.GameState) []game.Move { // TODO: Implement your strategy here! //