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:
parent
3825cbee22
commit
ea04f4debb
78 changed files with 1040 additions and 1037 deletions
BIN
acb-map-evolver
BIN
acb-map-evolver
Binary file not shown.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import "math"
|
|||
|
||||
// WinRateResult holds the observed win rate and its 95% Wilson score confidence interval.
|
||||
type WinRateResult struct {
|
||||
Wins int
|
||||
Total int // non-error matches only
|
||||
Rate float64 // observed win rate (0–1)
|
||||
Lower float64 // 95% CI lower bound
|
||||
Upper float64 // 95% CI upper bound
|
||||
Wins int
|
||||
Total int // non-error matches only
|
||||
Rate float64 // observed win rate (0–1)
|
||||
Lower float64 // 95% CI lower bound
|
||||
Upper float64 // 95% CI upper bound
|
||||
}
|
||||
|
||||
// WinRate computes the win rate and Wilson score 95% confidence interval
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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++ {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func NewMockS3Client() *MockS3Client {
|
|||
return &MockS3Client{
|
||||
Objects: make(map[string]MockObject),
|
||||
UploadCalls: []UploadCall{},
|
||||
DeleteCalls: []string{},
|
||||
DeleteCalls: []string{},
|
||||
CopyCalls: []CopyCall{},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ func jsAddPlayer(_ js.Value, args []js.Value) interface{} {
|
|||
})
|
||||
|
||||
return map[string]interface{}{
|
||||
"ok": true,
|
||||
"ok": true,
|
||||
"index": len(jsPlayers) - 1,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue