From 7a0de0205992e189f99499c5bb9215e18024fc9d Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 22 Apr 2026 16:04:15 -0400 Subject: [PATCH] =?UTF-8?q?feat(evolver):=20persist=20cross-pollination=20?= =?UTF-8?q?state=20to=20Postgres=20per=20=C2=A710.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add crosspoll_state table to persist per-island generation counters across evolver restarts. Load state on startup and save after each cross-pollination check. Add persistence pattern and translation structure tests. Co-Authored-By: Claude Opus 4.7 --- .../internal/crosspoll/crosspoll_test.go | 120 ++++++++++++++++++ cmd/acb-evolver/internal/db/db.go | 13 ++ cmd/acb-evolver/internal/db/programs.go | 36 ++++++ cmd/acb-evolver/run.go | 19 ++- starters/csharp/Dockerfile | 2 +- starters/csharp/Grid.cs | 79 ++++++++++++ starters/csharp/Program.cs | 40 +++++- starters/csharp/README.md | 10 ++ starters/go/Dockerfile | 2 +- starters/go/README.md | 10 ++ starters/go/grid.go | 81 ++++++++++++ starters/go/main.go | 59 ++++++++- starters/java/README.md | 10 ++ .../src/main/java/com/acb/starter/App.java | 29 ++++- .../src/main/java/com/acb/starter/Grid.java | 98 ++++++++++++++ starters/javascript/Dockerfile | 2 +- starters/javascript/README.md | 10 ++ starters/javascript/grid.js | 71 +++++++++++ starters/javascript/index.js | 48 ++++++- starters/python/Dockerfile | 2 +- starters/python/README.md | 10 ++ starters/python/grid.py | 64 ++++++++++ starters/python/main.py | 42 +++++- starters/rust/README.md | 10 ++ starters/rust/src/grid.rs | 74 +++++++++++ starters/rust/src/main.rs | 48 ++++++- 26 files changed, 959 insertions(+), 30 deletions(-) create mode 100644 starters/csharp/Grid.cs create mode 100644 starters/go/grid.go create mode 100644 starters/java/src/main/java/com/acb/starter/Grid.java create mode 100644 starters/javascript/grid.js create mode 100644 starters/python/grid.py create mode 100644 starters/rust/src/grid.rs diff --git a/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go b/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go index a370c59..7d41ea5 100644 --- a/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go +++ b/cmd/acb-evolver/internal/crosspoll/crosspoll_test.go @@ -506,3 +506,123 @@ func TestCheckAndPollinate_emptyIsland_noEvent(t *testing.T) { func newRandZero() *rand.Rand { return rand.New(rand.NewSource(0)) } + +// TestCheckAndPollinate_persistencePattern verifies that the prevGens map +// is correctly updated so that a second call with the same data produces +// no duplicate events (simulating the save-and-reload persistence pattern). +func TestCheckAndPollinate_persistencePattern(t *testing.T) { + store := newMockStore() + for _, island := range evolverdb.AllIslands { + seedIsland(store, island, "go", 100.0, 50) + seedIsland(store, island, "go", 50.0, 30) + } + + llmClient := &mockLLM{} + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + // First call: should trigger 4 events. + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("first call: %v", err) + } + if len(results) != 4 { + t.Fatalf("first call: expected 4 events, got %d", len(results)) + } + + // Second call with same prevGens (now updated to 50): should trigger 0 events. + results2, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("second call: %v", err) + } + if len(results2) != 0 { + t.Fatalf("second call: expected 0 events (no duplicates), got %d", len(results2)) + } + + // Advance to gen 100 and call again: should trigger 4 more events. + for _, island := range evolverdb.AllIslands { + seedIsland(store, island, "go", 120.0, 100) + } + results3, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("third call: %v", err) + } + if len(results3) != 4 { + t.Fatalf("third call: expected 4 events (gen 100), got %d", len(results3)) + } + + // Verify prevGens now reflects 100 for all islands. + for _, island := range evolverdb.AllIslands { + if prevGens[island] != 100 { + t.Errorf("prevGens[%s] = %d, want 100", island, prevGens[island]) + } + } +} + +// TestCheckAndPollinate_translatedCodeStructure verifies that translated +// code is stored with the target language and contains recognizable +// language patterns from the LLM output. +func TestCheckAndPollinate_translatedCodeStructure(t *testing.T) { + store := newMockStore() + seedIsland(store, evolverdb.IslandAlpha, "python", 100.0, 50) + seedIsland(store, evolverdb.IslandBeta, "go", 80.0, 10) + seedIsland(store, evolverdb.IslandGamma, "go", 70.0, 10) + seedIsland(store, evolverdb.IslandDelta, "go", 60.0, 10) + + // Mock LLM returns syntactically valid Go code. + goCode := `package main +import ("net/http"; "encoding/json") +func main() { + http.HandleFunc("/turn", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]interface{}{"moves": []interface{}{}}) + }) + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {}) + http.ListenAndServe(":8080", nil) +}` + + llmClient := &mockLLM{ + generateFunc: func(ctx context.Context, req llm.GenerateRequest) (*llm.GenerateResponse, error) { + return &llm.GenerateResponse{ + Candidate: &llm.Candidate{Code: goCode}, + }, nil + }, + } + + rng := rand.New(rand.NewSource(42)) + checker := &Checker{store: store, client: llmClient, rng: rng} + + prevGens := make(map[string]int) + results, err := checker.CheckAndPollinate(context.Background(), prevGens, false) + if err != nil { + t.Fatalf("CheckAndPollinate: %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 event, got %d", len(results)) + } + + r := results[0] + if !r.Translated { + t.Error("expected translation from python to go") + } + if r.TargetLang != "go" { + t.Errorf("target lang: got %q, want go", r.TargetLang) + } + + // Verify the stored program has the Go code and language. + if len(store.createdCalls) != 1 { + t.Fatal("expected 1 Create call") + } + created := store.createdCalls[0] + if created.Language != "go" { + t.Errorf("stored language: got %q, want go", created.Language) + } + if created.Code != goCode { + t.Errorf("stored code mismatch: got %d bytes, want %d bytes", len(created.Code), len(goCode)) + } + // Verify fitness penalty applied. + if created.Fitness != 90.0 { + t.Errorf("fitness: got %f, want 90.0", created.Fitness) + } +} diff --git a/cmd/acb-evolver/internal/db/db.go b/cmd/acb-evolver/internal/db/db.go index 6c82cb9..ccca3cf 100644 --- a/cmd/acb-evolver/internal/db/db.go +++ b/cmd/acb-evolver/internal/db/db.go @@ -38,6 +38,16 @@ CREATE INDEX IF NOT EXISTS idx_validation_log_island ON validation_log(island); CREATE INDEX IF NOT EXISTS idx_validation_log_island_passed ON validation_log(island, passed); ` +// crosspollStateSQL creates the crosspoll_state table for persisting per-island +// last-pollinated generation numbers across evolver restarts. +const crosspollStateSQL = ` +CREATE TABLE IF NOT EXISTS crosspoll_state ( + island VARCHAR(16) PRIMARY KEY, + last_pollinated_gen INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +` + // migrationSQL holds additive migrations run after the base schema is ensured. // Each statement is idempotent (ALTER TABLE … ADD COLUMN IF NOT EXISTS). const migrationSQL = ` @@ -52,6 +62,9 @@ func EnsureSchema(ctx context.Context, db *sql.DB) error { if _, err := db.ExecContext(ctx, schemaSQL); err != nil { return err } + if _, err := db.ExecContext(ctx, crosspollStateSQL); err != nil { + return err + } _, err := db.ExecContext(ctx, migrationSQL) return err } diff --git a/cmd/acb-evolver/internal/db/programs.go b/cmd/acb-evolver/internal/db/programs.go index 208f3b3..ce98cfc 100644 --- a/cmd/acb-evolver/internal/db/programs.go +++ b/cmd/acb-evolver/internal/db/programs.go @@ -409,3 +409,39 @@ func (s *Store) GetLineage(ctx context.Context, id int64) ([]int64, error) { } return lineage, nil } + +// LoadCrossPollState returns the last-pollinated generation per island from +// the crosspoll_state table. Islands with no row default to 0. +func (s *Store) LoadCrossPollState(ctx context.Context) (map[string]int, error) { + rows, err := s.db.QueryContext(ctx, + `SELECT island, last_pollinated_gen FROM crosspoll_state`) + if err != nil { + return nil, fmt.Errorf("load crosspoll state: %w", err) + } + defer rows.Close() + + state := make(map[string]int) + for rows.Next() { + var island string + var gen int + if err := rows.Scan(&island, &gen); err != nil { + return nil, fmt.Errorf("scan crosspoll state: %w", err) + } + state[island] = gen + } + return state, rows.Err() +} + +// SaveCrossPollState persists the last-pollinated generation for a single island. +// Uses UPSERT to insert or update the row. +func (s *Store) SaveCrossPollState(ctx context.Context, island string, gen int) error { + _, err := s.db.ExecContext(ctx, ` + INSERT INTO crosspoll_state (island, last_pollinated_gen, updated_at) + VALUES ($1, $2, NOW()) + ON CONFLICT (island) DO UPDATE SET last_pollinated_gen = $2, updated_at = NOW()`, + island, gen) + if err != nil { + return fmt.Errorf("save crosspoll state for %s: %w", island, err) + } + return nil +} diff --git a/cmd/acb-evolver/run.go b/cmd/acb-evolver/run.go index ceb8baa..c454f0c 100644 --- a/cmd/acb-evolver/run.go +++ b/cmd/acb-evolver/run.go @@ -158,8 +158,16 @@ func RunEvolutionLoop(ctx context.Context, dbURL string, args []string) { // Track last evolution time per island for cooldown lastEvolved := make(map[string]time.Time) - // Track per-island generation counters for cross-pollination boundary detection - prevGens := make(map[string]int) + // Track per-island generation counters for cross-pollination boundary detection. + // Load persisted state from DB so we don't re-trigger on restart. + prevGens, err := store.LoadCrossPollState(ctx) + if err != nil { + log.Printf("warn: could not load cross-pollination state (starting fresh): %v", err) + prevGens = make(map[string]int) + } + if *verbose { + log.Printf("Cross-pollination state: %v", prevGens) + } // Stats stats := RunStats{StartTime: time.Now()} @@ -239,6 +247,13 @@ func RunEvolutionLoop(ctx context.Context, dbURL string, args []string) { } stats.CrossPollinated += len(cpResults) + // Persist updated cross-pollination state so we don't re-trigger on restart. + for isl, gen := range prevGens { + if err := store.SaveCrossPollState(ctx, isl, gen); err != nil { + log.Printf("warn: could not save crosspoll state for %s: %v", isl, err) + } + } + // Continuous mode: wait for next cycle if *continuous { lastEvolved[island] = time.Now() diff --git a/starters/csharp/Dockerfile b/starters/csharp/Dockerfile index 12c852c..fe3d0a3 100644 --- a/starters/csharp/Dockerfile +++ b/starters/csharp/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder WORKDIR /app COPY acb-starter-csharp.csproj . RUN dotnet restore -COPY Program.cs . +COPY Program.cs Grid.cs . RUN dotnet publish -c Release -o /out FROM mcr.microsoft.com/dotnet/runtime:9.0-alpine diff --git a/starters/csharp/Grid.cs b/starters/csharp/Grid.cs new file mode 100644 index 0000000..3879fba --- /dev/null +++ b/starters/csharp/Grid.cs @@ -0,0 +1,79 @@ +// Grid utility functions for AI Code Battle. +// +// Provides toroidal distance calculations, neighbor enumeration, +// and BFS pathfinding on a wrapping grid. + +using System.Collections.Generic; + +static class Grid +{ + private static readonly (int dr, int dc)[] Offsets = + { + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), + }; + + /// Manhattan distance with wrap-around on a toroidal grid. + public static int ToroidalManhattan(int r1, int c1, int r2, int c2, int rows, int cols) + { + int dr = Math.Min(Math.Abs(r1 - r2), rows - Math.Abs(r1 - r2)); + int dc = Math.Min(Math.Abs(c1 - c2), cols - Math.Abs(c1 - c2)); + return dr + dc; + } + + /// Chebyshev distance with wrap-around on a toroidal grid. + public static int ToroidalChebyshev(int r1, int c1, int r2, int c2, int rows, int cols) + { + int dr = Math.Min(Math.Abs(r1 - r2), rows - Math.Abs(r1 - r2)); + int dc = Math.Min(Math.Abs(c1 - c2), cols - Math.Abs(c1 - c2)); + return Math.Max(dr, dc); + } + + /// 8-directional neighbors with wrap-around. + public static Position[] Neighbors(Position p, int rows, int cols) + { + var result = new Position[8]; + for (int i = 0; i < Offsets.Length; i++) + { + result[i] = new Position + { + Row = (p.Row + Offsets[i].dr + rows) % rows, + Col = (p.Col + Offsets[i].dc + cols) % cols, + }; + } + return result; + } + + /// BFS pathfinding on a toroidal grid. + /// Returns path (excluding start) or null if unreachable. + public static List? Bfs(Position start, Position goal, + Func passable, int rows, int cols) + { + if (start.Row == goal.Row && start.Col == goal.Col) + return []; + + var visited = new HashSet<(int, int)> { (start.Row, start.Col) }; + var queue = new Queue<(Position pos, List path)>(); + queue.Enqueue((start, [])); + + while (queue.Count > 0) + { + var (cur, path) = queue.Dequeue(); + foreach (var nb in Neighbors(cur, rows, cols)) + { + var newPath = new List(path) { nb }; + if (nb.Row == goal.Row && nb.Col == goal.Col) + return newPath; + + var key = (nb.Row, nb.Col); + if (!visited.Contains(key) && passable(nb)) + { + visited.Add(key); + queue.Enqueue((nb, newPath)); + } + } + } + return null; + } +} diff --git a/starters/csharp/Program.cs b/starters/csharp/Program.cs index f1b61cc..b68759e 100644 --- a/starters/csharp/Program.cs +++ b/starters/csharp/Program.cs @@ -70,12 +70,50 @@ string[] Directions = ["N", "E", "S", "W"]; List ComputeMoves(GameState state) { + var rows = state.Config.Rows; + var cols = state.Config.Cols; var moves = new List(); var rng = Random.Shared; + var cardinal = new (int dr, int dc, string dir)[] { + (-1, 0, "N"), (0, 1, "E"), (1, 0, "S"), (0, -1, "W"), + }; + foreach (var bot in state.Bots) { - if (bot.Owner == state.You.Id && rng.NextDouble() < 0.5) + if (bot.Owner != state.You.Id) continue; + + // Find direction toward nearest energy using toroidal distance + if (state.Energy.Count > 0) + { + int bestDist = int.MaxValue; + string? bestDir = null; + foreach (var (dr, dc, dir) in cardinal) + { + var nr = (bot.Position.Row + dr + rows) % rows; + var nc = (bot.Position.Col + dc + cols) % cols; + foreach (var e in state.Energy) + { + var d = Grid.ToroidalManhattan(nr, nc, e.Row, e.Col, rows, cols); + if (d < bestDist) + { + bestDist = d; + bestDir = dir; + } + } + } + if (bestDir != null) + { + moves.Add(new Move + { + Position = bot.Position, + Direction = bestDir + }); + continue; + } + } + + if (rng.NextDouble() < 0.5) { moves.Add(new Move { diff --git a/starters/csharp/README.md b/starters/csharp/README.md index 6d02304..0b329d8 100644 --- a/starters/csharp/README.md +++ b/starters/csharp/README.md @@ -39,10 +39,20 @@ Save the `bot_id` and `shared_secret` from the response — the secret is shown ``` Program.cs # HTTP server, HMAC auth, types, and strategy +Grid.cs # Grid utilities (toroidal distance, BFS, neighbors) acb-starter-csharp.csproj # .NET project file Dockerfile # Container build ``` +## Grid Helpers + +`Grid.cs` provides static utility methods for the toroidal grid: + +- `Grid.ToroidalManhattan(r1, c1, r2, c2, rows, cols)` — Manhattan distance with wrap-around +- `Grid.ToroidalChebyshev(r1, c1, r2, c2, rows, cols)` — Chebyshev distance with wrap-around +- `Grid.Neighbors(p, rows, cols)` — 8-directional neighbors with wrap +- `Grid.Bfs(start, goal, passable, rows, cols)` — BFS pathfinding, returns path or `null` + ## Customization Edit `ComputeMoves()` in `Program.cs` to implement your strategy. The `GameState` record provides: diff --git a/starters/go/Dockerfile b/starters/go/Dockerfile index 5b2ce88..de10927 100644 --- a/starters/go/Dockerfile +++ b/starters/go/Dockerfile @@ -2,7 +2,7 @@ FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod . -COPY main.go . +COPY main.go grid.go . RUN CGO_ENABLED=0 go build -o bot . diff --git a/starters/go/README.md b/starters/go/README.md index bd26677..88ae921 100644 --- a/starters/go/README.md +++ b/starters/go/README.md @@ -37,10 +37,20 @@ Save the `bot_id` and `shared_secret` from the response — the secret is shown ``` main.go # HTTP server, HMAC auth, game types, and strategy entry point +grid.go # Grid utilities (toroidal distance, BFS, neighbors) go.mod # Go module definition Dockerfile # Multi-stage container build ``` +## Grid Helpers + +`grid.go` provides utility functions for the toroidal grid: + +- `ToroidalManhattan(a, b, rows, cols)` — Manhattan distance with wrap-around +- `ToroidalChebyshev(a, b, rows, cols)` — Chebyshev distance with wrap-around +- `Neighbors(p, rows, cols)` — 8-directional neighbors with wrap +- `BFS(start, goal, passable, rows, cols)` — BFS pathfinding, returns path or `nil` + ## Customization Edit `computeMoves()` in `main.go` to implement your strategy. The `GameState` struct provides: diff --git a/starters/go/grid.go b/starters/go/grid.go new file mode 100644 index 0000000..ef64d19 --- /dev/null +++ b/starters/go/grid.go @@ -0,0 +1,81 @@ +package main + +// ToroidalManhattan returns Manhattan distance with wrap-around. +func ToroidalManhattan(a, b Position, rows, cols int) int { + dr := abs(a.Row - b.Row) + dc := abs(a.Col - b.Col) + dr = min(dr, rows-dr) + dc = min(dc, cols-dc) + return dr + dc +} + +// ToroidalChebyshev returns Chebyshev distance with wrap-around. +func ToroidalChebyshev(a, b Position, rows, cols int) int { + dr := abs(a.Row - b.Row) + dc := abs(a.Col - b.Col) + dr = min(dr, rows-dr) + dc = min(dc, cols-dc) + return max(dr, dc) +} + +// Neighbors returns 8-directional neighbors with wrap-around. +func Neighbors(p Position, rows, cols int) []Position { + offsets := [8][2]int{ + {-1, -1}, {-1, 0}, {-1, 1}, + {0, -1}, {0, 1}, + {1, -1}, {1, 0}, {1, 1}, + } + result := make([]Position, 0, 8) + for _, off := range offsets { + result = append(result, Position{ + Row: (p.Row + off[0] + rows) % rows, + Col: (p.Col + off[1] + cols) % cols, + }) + } + return result +} + +// BFS finds the shortest path from start to goal on a toroidal grid. +// passable returns true if a cell can be entered. +// Returns the path (excluding start) or nil if unreachable. +func BFS(start, goal Position, passable func(Position) bool, rows, cols int) []Position { + if start == goal { + return []Position{} + } + + type node struct { + pos Position + path []Position + } + + visited := map[Position]bool{start: true} + queue := []node{{start, nil}} + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + + for _, n := range Neighbors(cur.pos, rows, cols) { + newPath := make([]Position, len(cur.path), len(cur.path)+1) + copy(newPath, cur.path) + newPath = append(newPath, n) + + if n == goal { + return newPath + } + if !visited[n] && passable(n) { + visited[n] = true + queue = append(queue, node{n, newPath}) + } + } + } + + return nil +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/starters/go/main.go b/starters/go/main.go index 3d3f9e0..cee2824 100644 --- a/starters/go/main.go +++ b/starters/go/main.go @@ -147,20 +147,67 @@ func handleHealth(w http.ResponseWriter, r *http.Request) { 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 { - if rand.Float64() < 0.5 { - moves = append(moves, Move{ - Position: bot.Position, - Direction: directions[rand.Intn(len(directions))], - }) + 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[:])) diff --git a/starters/java/README.md b/starters/java/README.md index 21d60b6..f125663 100644 --- a/starters/java/README.md +++ b/starters/java/README.md @@ -41,10 +41,20 @@ Save the `bot_id` and `shared_secret` from the response — the secret is shown ``` src/main/java/com/acb/starter/App.java # Server, auth, types, and strategy +src/main/java/com/acb/starter/Grid.java # Grid utilities (toroidal distance, BFS, neighbors) pom.xml # Maven build configuration Dockerfile # Multi-stage container build ``` +## Grid Helpers + +`Grid.java` provides static utility methods for the toroidal grid: + +- `Grid.toroidalManhattan(r1, c1, r2, c2, rows, cols)` — Manhattan distance with wrap-around +- `Grid.toroidalChebyshev(r1, c1, r2, c2, rows, cols)` — Chebyshev distance with wrap-around +- `Grid.neighbors(row, col, rows, cols)` — 8-directional neighbors with wrap +- `Grid.bfs(start, goal, passable, rows, cols)` — BFS pathfinding, returns path or `null` + ## Customization Edit `computeMoves()` in `App.java` to implement your strategy. The `GameState` object provides: diff --git a/starters/java/src/main/java/com/acb/starter/App.java b/starters/java/src/main/java/com/acb/starter/App.java index 2bff522..2ef8435 100644 --- a/starters/java/src/main/java/com/acb/starter/App.java +++ b/starters/java/src/main/java/com/acb/starter/App.java @@ -82,10 +82,37 @@ public class App { static List computeMoves(GameState state) { // Replace this with your strategy! + int rows = state.config.rows; + int cols = state.config.cols; List moves = new ArrayList<>(); + int[][] cardinal = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; + for (VisibleBot bot : state.bots) { - if (bot.owner == state.you.id && RANDOM.nextDouble() < 0.5) { + if (bot.owner != state.you.id) continue; + + // Find direction toward nearest energy using toroidal distance + if (!state.energy.isEmpty()) { + int bestDist = Integer.MAX_VALUE; + String bestDir = null; + for (int i = 0; i < cardinal.length; i++) { + int nr = Math.floorMod(bot.row + cardinal[i][0], rows); + int nc = Math.floorMod(bot.col + cardinal[i][1], cols); + for (Position e : state.energy) { + int d = Grid.toroidalManhattan(nr, nc, e.row, e.col, rows, cols); + if (d < bestDist) { + bestDist = d; + bestDir = DIRECTIONS[i]; + } + } + } + if (bestDir != null) { + moves.add(new Move(bot.row, bot.col, bestDir)); + continue; + } + } + + if (RANDOM.nextDouble() < 0.5) { String dir = DIRECTIONS[RANDOM.nextInt(DIRECTIONS.length)]; moves.add(new Move(bot.row, bot.col, dir)); } diff --git a/starters/java/src/main/java/com/acb/starter/Grid.java b/starters/java/src/main/java/com/acb/starter/Grid.java new file mode 100644 index 0000000..627ebb5 --- /dev/null +++ b/starters/java/src/main/java/com/acb/starter/Grid.java @@ -0,0 +1,98 @@ +package com.acb.starter; + +import java.util.*; + +/** + * Grid utility functions for AI Code Battle. + * + * Provides toroidal distance calculations, neighbor enumeration, + * and BFS pathfinding on a wrapping grid. + */ +public final class Grid { + + private static final int[][] OFFSETS = { + {-1, -1}, {-1, 0}, {-1, 1}, + {0, -1}, {0, 1}, + {1, -1}, {1, 0}, {1, 1}, + }; + + private Grid() {} + + /** Manhattan distance with wrap-around on a toroidal grid. */ + public static int toroidalManhattan(int r1, int c1, int r2, int c2, int rows, int cols) { + int dr = Math.abs(r1 - r2); + int dc = Math.abs(c1 - c2); + dr = Math.min(dr, rows - dr); + dc = Math.min(dc, cols - dc); + return dr + dc; + } + + /** Chebyshev distance with wrap-around on a toroidal grid. */ + public static int toroidalChebyshev(int r1, int c1, int r2, int c2, int rows, int cols) { + int dr = Math.abs(r1 - r2); + int dc = Math.abs(c1 - c2); + dr = Math.min(dr, rows - dr); + dc = Math.min(dc, cols - dc); + return Math.max(dr, dc); + } + + /** 8-directional neighbors with wrap-around. Returns [row, col] pairs. */ + public static List neighbors(int row, int col, int rows, int cols) { + List result = new ArrayList<>(8); + for (int[] off : OFFSETS) { + result.add(new int[]{ + Math.floorMod(row + off[0], rows), + Math.floorMod(col + off[1], cols), + }); + } + return result; + } + + /** + * BFS pathfinding on a toroidal grid. + * + * @param start [row, col] + * @param goal [row, col] + * @param passable predicate returning true if a cell can be entered + * @param rows grid height + * @param cols grid width + * @return path as list of [row, col] (excluding start), or null if unreachable + */ + public static List bfs(int[] start, int[] goal, + java.util.function.Predicate passable, + int rows, int cols) { + if (start[0] == goal[0] && start[1] == goal[1]) { + return Collections.emptyList(); + } + + Set visited = new HashSet<>(); + visited.add(start[0] + "," + start[1]); + + Queue posQueue = new ArrayDeque<>(); + Queue> pathQueue = new ArrayDeque<>(); + posQueue.add(start); + pathQueue.add(Collections.emptyList()); + + while (!posQueue.isEmpty()) { + int[] cur = posQueue.poll(); + List path = pathQueue.poll(); + + for (int[] nb : neighbors(cur[0], cur[1], rows, cols)) { + List newPath = new ArrayList<>(path); + newPath.add(nb); + + if (nb[0] == goal[0] && nb[1] == goal[1]) { + return newPath; + } + + String key = nb[0] + "," + nb[1]; + if (!visited.contains(key) && passable.test(nb)) { + visited.add(key); + posQueue.add(nb); + pathQueue.add(newPath); + } + } + } + return null; + } +} diff --git a/starters/javascript/Dockerfile b/starters/javascript/Dockerfile index d854771..c2010e9 100644 --- a/starters/javascript/Dockerfile +++ b/starters/javascript/Dockerfile @@ -2,7 +2,7 @@ FROM node:22-alpine WORKDIR /app COPY package.json . -COPY index.js . +COPY index.js grid.js . ENV BOT_PORT=8080 ENV BOT_SECRET="" diff --git a/starters/javascript/README.md b/starters/javascript/README.md index e4182ae..b0d67c5 100644 --- a/starters/javascript/README.md +++ b/starters/javascript/README.md @@ -38,10 +38,20 @@ Save the `bot_id` and `shared_secret` from the response — the secret is shown ``` index.js # HTTP server, HMAC auth, and strategy entry point +grid.js # Grid utilities (toroidal distance, BFS, neighbors) package.json # Node.js project definition Dockerfile # Container build ``` +## Grid Helpers + +`grid.js` provides utility functions for the toroidal grid: + +- `toroidalManhattan(r1, c1, r2, c2, cols, rows)` — Manhattan distance with wrap-around +- `toroidalChebyshev(r1, c1, r2, c2, cols, rows)` — Chebyshev distance with wrap-around +- `neighbors(row, col, rows, cols)` — 8-directional neighbors with wrap +- `bfs(start, goal, passable, rows, cols)` — BFS pathfinding, returns path or `null` + ## Customization Edit `computeMoves()` in `index.js` to implement your strategy. The `state` object provides: diff --git a/starters/javascript/grid.js b/starters/javascript/grid.js new file mode 100644 index 0000000..4701adf --- /dev/null +++ b/starters/javascript/grid.js @@ -0,0 +1,71 @@ +/** + * Grid utility functions for AI Code Battle. + * + * Provides toroidal distance calculations, neighbor enumeration, + * and BFS pathfinding on a wrapping grid. + */ + +function toroidalManhattan(r1, c1, r2, c2, cols, rows) { + const dr = Math.min(Math.abs(r1 - r2), rows - Math.abs(r1 - r2)); + const dc = Math.min(Math.abs(c1 - c2), cols - Math.abs(c1 - c2)); + return dr + dc; +} + +function toroidalChebyshev(r1, c1, r2, c2, cols, rows) { + const dr = Math.min(Math.abs(r1 - r2), rows - Math.abs(r1 - r2)); + const dc = Math.min(Math.abs(c1 - c2), cols - Math.abs(c1 - c2)); + return Math.max(dr, dc); +} + +function neighbors(row, col, rows, cols) { + const offsets = [ + [-1, -1], [-1, 0], [-1, 1], + [0, -1], [0, 1], + [1, -1], [1, 0], [1, 1], + ]; + return offsets.map(([dr, dc]) => [ + (row + dr + rows) % rows, + (col + dc + cols) % cols, + ]); +} + +/** + * BFS pathfinding on a toroidal grid. + * + * @param {[number,number]} start - [row, col] + * @param {[number,number]} goal - [row, col] + * @param {function(number,number): boolean} passable - returns true if walkable + * @param {number} rows + * @param {number} cols + * @returns {[number,number][]|null} path from start to goal (excl. start), or null + */ +function bfs(start, goal, passable, rows, cols) { + const [sr, sc] = start; + const [gr, gc] = goal; + if (sr === gr && sc === gc) return []; + + const key = (r, c) => `${r},${c}`; + const visited = new Set([key(sr, sc)]); + const queue = [{ r: sr, c: sc, path: [] }]; + + while (queue.length > 0) { + const { r, c, path } = queue.shift(); + for (const [nr, nc] of neighbors(r, c, rows, cols)) { + const newPath = [...path, [nr, nc]]; + if (nr === gr && nc === gc) return newPath; + const k = key(nr, nc); + if (!visited.has(k) && passable(nr, nc)) { + visited.add(k); + queue.push({ r: nr, c: nc, path: newPath }); + } + } + } + return null; +} + +module.exports = { + toroidalManhattan, + toroidalChebyshev, + neighbors, + bfs, +}; diff --git a/starters/javascript/index.js b/starters/javascript/index.js index ee3a719..3d01bb6 100644 --- a/starters/javascript/index.js +++ b/starters/javascript/index.js @@ -46,15 +46,51 @@ function signResponse(body, matchId, turn) { function computeMoves(state) { // Replace this with your strategy! + const { toroidalManhattan } = require("./grid"); + + const rows = state.config.rows; + const cols = state.config.cols; const moves = []; + + const cardinalSteps = [ + { dr: -1, dc: 0, dir: "N" }, + { dr: 0, dc: 1, dir: "E" }, + { dr: 1, dc: 0, dir: "S" }, + { dr: 0, dc: -1, dir: "W" }, + ]; + for (const bot of state.bots) { - if (bot.owner === state.you.id) { - if (Math.random() < 0.5) { - moves.push({ - position: bot.position, - direction: DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)], - }); + if (bot.owner !== state.you.id) continue; + + const br = bot.position.row; + const bc = bot.position.col; + + // Find direction toward nearest energy using toroidal distance + if (state.energy && state.energy.length > 0) { + let bestDist = Infinity; + let bestDir = null; + for (const { dr, dc, dir } of cardinalSteps) { + const nr = (br + dr + rows) % rows; + const nc = (bc + dc + cols) % cols; + for (const e of state.energy) { + const dist = toroidalManhattan(nr, nc, e.row, e.col, cols, rows); + if (dist < bestDist) { + bestDist = dist; + bestDir = dir; + } + } } + if (bestDir) { + moves.push({ position: bot.position, direction: bestDir }); + continue; + } + } + + if (Math.random() < 0.5) { + moves.push({ + position: bot.position, + direction: DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)], + }); } } return moves; diff --git a/starters/python/Dockerfile b/starters/python/Dockerfile index 087921a..7f1d4bd 100644 --- a/starters/python/Dockerfile +++ b/starters/python/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.13-slim WORKDIR /app -COPY main.py . +COPY main.py grid.py . COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/starters/python/README.md b/starters/python/README.md index e9f4f0d..234bf15 100644 --- a/starters/python/README.md +++ b/starters/python/README.md @@ -37,10 +37,20 @@ Save the `bot_id` and `shared_secret` from the response — the secret is shown ``` main.py # HTTP server, HMAC auth, and strategy entry point +grid.py # Grid utilities (toroidal distance, BFS, neighbors) requirements.txt # Python dependencies (stdlib only for this starter) Dockerfile # Container build ``` +## Grid Helpers + +`grid.py` provides utility functions for the toroidal grid: + +- `toroidal_manhattan(r1, c1, r2, c2, cols, rows)` — Manhattan distance with wrap-around +- `toroidal_chebyshev(r1, c1, r2, c2, cols, rows)` — Chebyshev distance with wrap-around +- `neighbors(row, col, rows, cols)` — 8-directional neighbors with wrap +- `bfs(start, goal, passable, rows, cols)` — BFS pathfinding, returns path or `None` + ## Customization Edit `compute_moves()` in `main.py` to implement your strategy. The `GameState` object provides: diff --git a/starters/python/grid.py b/starters/python/grid.py new file mode 100644 index 0000000..9fe229e --- /dev/null +++ b/starters/python/grid.py @@ -0,0 +1,64 @@ +"""Grid utility functions for AI Code Battle. + +Provides toroidal distance calculations, neighbor enumeration, +and BFS pathfinding on a wrapping grid. +""" + +from collections import deque + + +def toroidal_manhattan(r1, c1, r2, c2, cols, rows): + """Manhattan distance with wrap-around on a toroidal grid.""" + dr = abs(r1 - r2) + dc = abs(c1 - c2) + dr = min(dr, rows - dr) + dc = min(dc, cols - dc) + return dr + dc + + +def toroidal_chebyshev(r1, c1, r2, c2, cols, rows): + """Chebyshev distance with wrap-around on a toroidal grid.""" + dr = abs(r1 - r2) + dc = abs(c1 - c2) + dr = min(dr, rows - dr) + dc = min(dc, cols - dc) + return max(dr, dc) + + +def neighbors(row, col, rows, cols): + """Return 8-directional neighbors with wrap-around.""" + offsets = [(-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1)] + return [((row + dr) % rows, (col + dc) % cols) for dr, dc in offsets] + + +def bfs(start, goal, passable, rows, cols): + """BFS pathfinding on a toroidal grid. + + Args: + start: (row, col) tuple + goal: (row, col) tuple + passable: callable(row, col) -> bool + rows, cols: grid dimensions + + Returns: + List of (row, col) from start to goal (exclusive of start), + or None if no path exists. + """ + if start == goal: + return [] + + queue = deque([(start, [])]) + visited = {start} + + while queue: + (r, c), path = queue.popleft() + for nr, nc in neighbors(r, c, rows, cols): + if (nr, nc) == goal: + return path + [(nr, nc)] + if (nr, nc) not in visited and passable(nr, nc): + visited.add((nr, nc)) + queue.append(((nr, nc), path + [(nr, nc)])) + + return None diff --git a/starters/python/main.py b/starters/python/main.py index 82a4d61..ea5d97a 100644 --- a/starters/python/main.py +++ b/starters/python/main.py @@ -106,17 +106,47 @@ class BotHandler(BaseHTTPRequestHandler): def compute_moves(state: GameState) -> list: """Replace this with your strategy!""" + from grid import toroidal_manhattan + + rows = state.config["rows"] + cols = state.config["cols"] moves = [] + for bot in state.bots: - if bot["owner"] == state.you_id: - if random.random() < 0.5: - moves.append({ - "position": bot["position"], - "direction": random.choice(DIRECTIONS), - }) + if bot["owner"] != state.you_id: + continue + + br, bc = bot["position"]["row"], bot["position"]["col"] + + # Find nearest energy using toroidal distance + if state.energy: + best_dist = float("inf") + best_dir = None + for er, ec, d in _cardinal_moves(br, bc, rows, cols): + for e in state.energy: + dist = toroidal_manhattan(er, ec, e["row"], e["col"], cols, rows) + if dist < best_dist: + best_dist = dist + best_dir = d + if best_dir: + moves.append({"position": bot["position"], "direction": best_dir}) + continue + + if random.random() < 0.5: + moves.append({ + "position": bot["position"], + "direction": random.choice(DIRECTIONS), + }) + return moves +def _cardinal_moves(row, col, rows, cols): + """Yield (new_row, new_col, direction) for each cardinal step with wrap.""" + for dr, dc, d in [(-1, 0, "N"), (0, 1, "E"), (1, 0, "S"), (0, -1, "W")]: + yield (row + dr) % rows, (col + dc) % cols, d + + def main(): port = int(os.environ.get("BOT_PORT", "8080")) secret = os.environ.get("BOT_SECRET", "") diff --git a/starters/rust/README.md b/starters/rust/README.md index 0df7e11..405067b 100644 --- a/starters/rust/README.md +++ b/starters/rust/README.md @@ -39,10 +39,20 @@ Save the `bot_id` and `shared_secret` from the response — the secret is shown ``` src/main.rs # HTTP server, HMAC auth, game types, and strategy entry point +src/grid.rs # Grid utilities (toroidal distance, BFS, neighbors) Cargo.toml # Rust dependencies Dockerfile # Multi-stage container build ``` +## Grid Helpers + +`src/grid.rs` provides utility functions for the toroidal grid: + +- `toroidal_manhattan(a, b, rows, cols)` — Manhattan distance with wrap-around +- `toroidal_chebyshev(a, b, rows, cols)` — Chebyshev distance with wrap-around +- `neighbors(pos, rows, cols)` — 8-directional neighbors with wrap +- `bfs(start, goal, passable, rows, cols)` — BFS pathfinding, returns `Option>` + ## Customization Edit `compute_moves()` in `src/main.rs` to implement your strategy. The `GameState` struct provides: diff --git a/starters/rust/src/grid.rs b/starters/rust/src/grid.rs new file mode 100644 index 0000000..f8d44fb --- /dev/null +++ b/starters/rust/src/grid.rs @@ -0,0 +1,74 @@ +//! Grid utility functions for AI Code Battle. +//! +//! Provides toroidal distance calculations, neighbor enumeration, +//! and BFS pathfinding on a wrapping grid. + +use std::collections::{HashMap, VecDeque}; + +/// Manhattan distance with wrap-around on a toroidal grid. +pub fn toroidal_manhattan(a: &Position, b: &Position, rows: u32, cols: u32) -> u32 { + let dr = (a.row as i32 - b.row as i32).unsigned_abs(); + let dc = (a.col as i32 - b.col as i32).unsigned_abs(); + dr.min(rows - dr) + dc.min(cols - dc) +} + +/// Chebyshev distance with wrap-around on a toroidal grid. +pub fn toroidal_chebyshev(a: &Position, b: &Position, rows: u32, cols: u32) -> u32 { + let dr = (a.row as i32 - b.row as i32).unsigned_abs(); + let dc = (a.col as i32 - b.col as i32).unsigned_abs(); + dr.min(rows - dr).max(dc.min(cols - dc)) +} + +/// 8-directional neighbors with wrap-around. +pub fn neighbors(pos: &Position, rows: u32, cols: u32) -> Vec { + const OFFSETS: [(i32, i32); 8] = [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), + ]; + OFFSETS + .iter() + .map(|(dr, dc)| Position { + row: (pos.row as i32 + dr).rem_euclid(rows as i32) as u32, + col: (pos.col as i32 + dc).rem_euclid(cols as i32) as u32, + }) + .collect() +} + +/// BFS pathfinding on a toroidal grid. +/// +/// `passable` returns true if a cell can be entered. +/// Returns the path (excluding start) or None if unreachable. +pub fn bfs( + start: &Position, + goal: &Position, + passable: impl Fn(&Position) -> bool, + rows: u32, + cols: u32, +) -> Option> { + if start.row == goal.row && start.col == goal.col { + return Some(vec![]); + } + + let mut visited: HashMap<(u32, u32), bool> = HashMap::new(); + visited.insert((start.row, start.col), true); + + let mut queue: VecDeque<(Position, Vec)> = VecDeque::new(); + queue.push_back((start.clone(), vec![])); + + while let Some((cur, path)) = queue.pop_front() { + for n in neighbors(&cur, rows, cols) { + let mut new_path = path.clone(); + new_path.push(n.clone()); + if n.row == goal.row && n.col == goal.col { + return Some(new_path); + } + let key = (n.row, n.col); + if !visited.contains_key(&key) && passable(&n) { + visited.insert(key, true); + queue.push_back((n, new_path)); + } + } + } + None +} diff --git a/starters/rust/src/main.rs b/starters/rust/src/main.rs index c5a7809..d4d46fa 100644 --- a/starters/rust/src/main.rs +++ b/starters/rust/src/main.rs @@ -3,6 +3,8 @@ //! A minimal bot scaffold with HMAC authentication and a placeholder //! random strategy. Replace `compute_moves()` with your own logic. +mod grid; + use axum::{ body::Bytes, extract::State, @@ -52,9 +54,9 @@ struct You { } #[derive(Deserialize, Serialize, Clone)] -struct Position { - row: u32, - col: u32, +pub struct Position { + pub row: u32, + pub col: u32, } #[derive(Deserialize)] @@ -164,11 +166,49 @@ async fn handle_turn( fn compute_moves(state: &GameState) -> Vec { // Replace this with your strategy! + let rows = state.config.rows; + let cols = state.config.cols; let mut moves = Vec::new(); let mut rng = rand::thread_rng(); + let cardinal: [(i32, i32, &str); 4] = [ + (-1, 0, "N"), + (0, 1, "E"), + (1, 0, "S"), + (0, -1, "W"), + ]; + for bot in &state.bots { - if bot.owner == state.you.id && rand::Rng::gen_ratio(&mut rng, 1, 2) { + if bot.owner != state.you.id { + continue; + } + + // Find direction toward nearest energy using toroidal distance + if !state.energy.is_empty() { + let mut best_dist = u32::MAX; + let mut best_dir: Option<&str> = None; + for (dr, dc, dir) in &cardinal { + let nr = (bot.position.row as i32 + dr).rem_euclid(rows as i32) as u32; + let nc = (bot.position.col as i32 + dc).rem_euclid(cols as i32) as u32; + let step = Position { row: nr, col: nc }; + for e in &state.energy { + let d = grid::toroidal_manhattan(&step, e, rows, cols); + if d < best_dist { + best_dist = d; + best_dir = Some(dir); + } + } + } + if let Some(dir) = best_dir { + moves.push(Move { + position: bot.position.clone(), + direction: dir.to_string(), + }); + continue; + } + } + + if rand::Rng::gen_ratio(&mut rng, 1, 2) { let dir = DIRECTIONS[rand::Rng::gen_range(&mut rng, 0..4)]; moves.push(Move { position: bot.position.clone(),