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>
241 lines
6.1 KiB
Go
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
|
|
}
|