feat(evolver): persist cross-pollination state to Postgres per §10.2

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 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-22 16:04:15 -04:00
parent 582b4c010d
commit 7a0de02059
26 changed files with 959 additions and 30 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

79
starters/csharp/Grid.cs Normal file
View file

@ -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<Position>? Bfs(Position start, Position goal,
Func<Position, bool> 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<Position> 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<Position>(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;
}
}

View file

@ -70,12 +70,50 @@ string[] Directions = ["N", "E", "S", "W"];
List<Move> ComputeMoves(GameState state)
{
var rows = state.Config.Rows;
var cols = state.Config.Cols;
var moves = new List<Move>();
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
{

View file

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

View file

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

View file

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

81
starters/go/grid.go Normal file
View file

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

View file

@ -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[:]))

View file

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

View file

@ -82,10 +82,37 @@ public class App {
static List<Move> computeMoves(GameState state) {
// Replace this with your strategy!
int rows = state.config.rows;
int cols = state.config.cols;
List<Move> 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));
}

View file

@ -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<int[]> neighbors(int row, int col, int rows, int cols) {
List<int[]> 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<int[]> bfs(int[] start, int[] goal,
java.util.function.Predicate<int[]> passable,
int rows, int cols) {
if (start[0] == goal[0] && start[1] == goal[1]) {
return Collections.emptyList();
}
Set<String> visited = new HashSet<>();
visited.add(start[0] + "," + start[1]);
Queue<int[]> posQueue = new ArrayDeque<>();
Queue<List<int[]>> pathQueue = new ArrayDeque<>();
posQueue.add(start);
pathQueue.add(Collections.emptyList());
while (!posQueue.isEmpty()) {
int[] cur = posQueue.poll();
List<int[]> path = pathQueue.poll();
for (int[] nb : neighbors(cur[0], cur[1], rows, cols)) {
List<int[]> 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

64
starters/python/grid.py Normal file
View file

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

View file

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

View file

@ -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<Vec<Position>>`
## Customization
Edit `compute_moves()` in `src/main.rs` to implement your strategy. The `GameState` struct provides:

74
starters/rust/src/grid.rs Normal file
View file

@ -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<Position> {
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<Vec<Position>> {
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<Position>)> = 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
}

View file

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