ai-code-battle/starters/go/main.go
jedarden 477a54c548 feat(matchmaker): implement §6.1 Pareto skill-proximity + LRU pairing algorithm
Replace random 2-player pairing with the full §6.1 algorithm:
- Seed selection: bot with oldest last-match timestamp (tiebreak: lowest bot ID)
- Format selection: seed's least-played player count among {2, 3, 4, 6}
- Opponent selection: Pareto 80%/16-rank skill proximity + oldest last-pairing
  with seed + fewest 24h games for game-count balance
- Map selection: least-recently-used active map for the chosen player count,
  with map_scores.last_used_at updated after each match
- Random player slot assignment for all participant counts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 17:35:00 -04:00

241 lines
6.1 KiB
Go

// AI Code Battle - Go Starter Kit
//
// A minimal bot scaffold with HMAC authentication and a placeholder
// random strategy. Replace computeMoves() with your own logic.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"strconv"
)
// Engine constants
var directions = []string{"N", "E", "S", "W"}
// GameConfig holds the match configuration.
type GameConfig struct {
Rows int `json:"rows"`
Cols int `json:"cols"`
MaxTurns int `json:"max_turns"`
VisionRadius2 int `json:"vision_radius2"`
AttackRadius2 int `json:"attack_radius2"`
SpawnCost int `json:"spawn_cost"`
EnergyInterval int `json:"energy_interval"`
SeasonID string `json:"season_id,omitempty"`
RulesVersion string `json:"rules_version,omitempty"`
}
// Position is a grid coordinate.
type Position struct {
Row int `json:"row"`
Col int `json:"col"`
}
// VisibleBot is a bot visible in fog of war.
type VisibleBot struct {
Position Position `json:"position"`
Owner int `json:"owner"`
}
// VisibleCore is a core visible in fog of war.
type VisibleCore struct {
Position Position `json:"position"`
Owner int `json:"owner"`
Active bool `json:"active"`
}
// GameState is the fog-filtered state visible to this bot.
type GameState struct {
MatchID string `json:"match_id"`
Turn int `json:"turn"`
Config GameConfig `json:"config"`
You struct {
ID int `json:"id"`
Energy int `json:"energy"`
Score int `json:"score"`
} `json:"you"`
Bots []VisibleBot `json:"bots"`
Energy []Position `json:"energy"`
Cores []VisibleCore `json:"cores"`
Walls []Position `json:"walls"`
Dead []VisibleBot `json:"dead"`
}
// Move is a movement order for one bot.
type Move struct {
Position Position `json:"position"`
Direction string `json:"direction"`
}
// MoveResponse is sent back to the engine.
type MoveResponse struct {
Moves []Move `json:"moves"`
}
func main() {
port := getEnv("BOT_PORT", "8080")
secret := getEnv("BOT_SECRET", "")
if secret == "" {
log.Fatal("BOT_SECRET environment variable is required")
}
http.HandleFunc("/turn", func(w http.ResponseWriter, r *http.Request) {
handleTurn(w, r, secret)
})
http.HandleFunc("/health", handleHealth)
addr := fmt.Sprintf(":%s", port)
log.Printf("Bot listening on %s", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
func handleTurn(w http.ResponseWriter, r *http.Request, secret string) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
sig := r.Header.Get("X-ACB-Signature")
matchID := r.Header.Get("X-ACB-Match-Id")
turnStr := r.Header.Get("X-ACB-Turn")
timestamp := r.Header.Get("X-ACB-Timestamp")
if sig == "" || !verifySignature(secret, matchID, turnStr, timestamp, body, sig) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var state GameState
if err := json.Unmarshal(body, &state); err != nil {
http.Error(w, "invalid game state", http.StatusBadRequest)
return
}
if state.Turn == 0 {
log.Printf("match=%s season_id=%s rules_version=%s rows=%d cols=%d",
state.MatchID, state.Config.SeasonID, state.Config.RulesVersion,
state.Config.Rows, state.Config.Cols)
}
moves := computeMoves(&state)
response := MoveResponse{Moves: moves}
responseBody, _ := json.Marshal(response)
turn, _ := strconv.Atoi(turnStr)
responseSig := signResponse(secret, matchID, turn, responseBody)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-ACB-Signature", responseSig)
w.Write(responseBody)
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func computeMoves(state *GameState) []Move {
// Replace this with your strategy!
rows := state.Config.Rows
cols := state.Config.Cols
var moves []Move
for _, bot := range state.Bots {
if bot.Owner != state.You.ID {
continue
}
// Find direction toward nearest energy using toroidal distance
if len(state.Energy) > 0 {
bestDist := int(^uint(0) >> 1)
bestDir := ""
for _, d := range cardinalSteps(bot.Position, rows, cols) {
for _, e := range state.Energy {
dist := ToroidalManhattan(d.pos, e, rows, cols)
if dist < bestDist {
bestDist = dist
bestDir = d.dir
}
}
}
if bestDir != "" {
moves = append(moves, Move{Position: bot.Position, Direction: bestDir})
continue
}
}
if rand.Float64() < 0.5 {
moves = append(moves, Move{
Position: bot.Position,
Direction: directions[rand.Intn(len(directions))],
})
}
}
return moves
}
type cardinalStep struct {
pos Position
dir string
}
func cardinalSteps(p Position, rows, cols int) []cardinalStep {
steps := []struct {
dr, dc int
dir string
}{{-1, 0, "N"}, {0, 1, "E"}, {1, 0, "S"}, {0, -1, "W"}}
var result []cardinalStep
for _, s := range steps {
result = append(result, cardinalStep{
pos: Position{
Row: (p.Row + s.dr + rows) % rows,
Col: (p.Col + s.dc + cols) % cols,
},
dir: s.dir,
})
}
return result
}
func verifySignature(secret, matchID, turnStr, timestamp string, body []byte, signature string) bool {
bodyHash := sha256.Sum256(body)
signingString := fmt.Sprintf("%s.%s.%s.%s", matchID, turnStr, timestamp, hex.EncodeToString(bodyHash[:]))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signingString))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func signResponse(secret, matchID string, turn int, body []byte) string {
bodyHash := sha256.Sum256(body)
signingString := fmt.Sprintf("%s.%d.%s", matchID, turn, hex.EncodeToString(bodyHash[:]))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signingString))
return hex.EncodeToString(mac.Sum(nil))
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}