style: apply gofmt alignment fixes across codebase

Tab/space alignment consistency from running gofmt on all packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-24 10:40:33 -04:00
parent 3825cbee22
commit ea04f4debb
78 changed files with 1040 additions and 1037 deletions

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 (01)
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 (01)
Lower float64 // 95% CI lower bound
Upper float64 // 95% CI upper bound
}
// WinRate computes the win rate and Wilson score 95% confidence interval

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ func NewMockS3Client() *MockS3Client {
return &MockS3Client{
Objects: make(map[string]MockObject),
UploadCalls: []UploadCall{},
DeleteCalls: []string{},
DeleteCalls: []string{},
CopyCalls: []CopyCall{},
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -225,7 +225,7 @@ func jsAddPlayer(_ js.Value, args []js.Value) interface{} {
})
return map[string]interface{}{
"ok": true,
"ok": true,
"index": len(jsPlayers) - 1,
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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!
//