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:
parent
582b4c010d
commit
7a0de02059
26 changed files with 959 additions and 30 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
79
starters/csharp/Grid.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 .
|
||||
|
||||
|
|
|
|||
|
|
@ -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
81
starters/go/grid.go
Normal 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
|
||||
}
|
||||
|
|
@ -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[:]))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
98
starters/java/src/main/java/com/acb/starter/Grid.java
Normal file
98
starters/java/src/main/java/com/acb/starter/Grid.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
71
starters/javascript/grid.js
Normal file
71
starters/javascript/grid.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
64
starters/python/grid.py
Normal 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
|
||||
|
|
@ -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", "")
|
||||
|
|
|
|||
|
|
@ -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
74
starters/rust/src/grid.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue