From 7bf6566823e56609e38c2f92680a983aa55f8476 Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 21 Apr 2026 13:58:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(starters,web):=20add=206-language=20bot=20?= =?UTF-8?q?starter=20kits=20per=20=C2=A76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add forkable starter kit templates for Python, Go, JavaScript, Rust, Java, and C# — each with HTTP server scaffold, HMAC auth, game types, random strategy, Dockerfile, and GHCR workflow. Update /compete/docs page with starter kit links and registration instructions. Co-Authored-By: Claude Opus 4.7 --- starters/csharp/.github/workflows/build.yml | 29 +++ starters/csharp/Dockerfile | 19 ++ starters/csharp/Program.cs | 179 ++++++++++++++ starters/csharp/README.md | 65 +++++ starters/csharp/acb-starter-csharp.csproj | 10 + starters/go/.github/workflows/build.yml | 29 +++ starters/go/Dockerfile | 19 ++ starters/go/README.md | 63 +++++ starters/go/go.mod | 3 + starters/go/main.go | 186 +++++++++++++++ starters/java/.github/workflows/build.yml | 29 +++ starters/java/Dockerfile | 19 ++ starters/java/README.md | 67 ++++++ starters/java/pom.xml | 75 ++++++ .../src/main/java/com/acb/starter/App.java | 201 ++++++++++++++++ .../javascript/.github/workflows/build.yml | 29 +++ starters/javascript/Dockerfile | 12 + starters/javascript/README.md | 64 +++++ starters/javascript/index.js | 124 ++++++++++ starters/javascript/package.json | 12 + starters/python/.github/workflows/build.yml | 29 +++ starters/python/Dockerfile | 14 ++ starters/python/README.md | 63 +++++ starters/python/main.py | 135 +++++++++++ starters/python/requirements.txt | 4 + starters/rust/.github/workflows/build.yml | 29 +++ starters/rust/Cargo.toml | 19 ++ starters/rust/Dockerfile | 19 ++ starters/rust/README.md | 65 +++++ starters/rust/src/main.rs | 222 ++++++++++++++++++ web/src/pages/docs.ts | 28 ++- 31 files changed, 1859 insertions(+), 2 deletions(-) create mode 100644 starters/csharp/.github/workflows/build.yml create mode 100644 starters/csharp/Dockerfile create mode 100644 starters/csharp/Program.cs create mode 100644 starters/csharp/README.md create mode 100644 starters/csharp/acb-starter-csharp.csproj create mode 100644 starters/go/.github/workflows/build.yml create mode 100644 starters/go/Dockerfile create mode 100644 starters/go/README.md create mode 100644 starters/go/go.mod create mode 100644 starters/go/main.go create mode 100644 starters/java/.github/workflows/build.yml create mode 100644 starters/java/Dockerfile create mode 100644 starters/java/README.md create mode 100644 starters/java/pom.xml create mode 100644 starters/java/src/main/java/com/acb/starter/App.java create mode 100644 starters/javascript/.github/workflows/build.yml create mode 100644 starters/javascript/Dockerfile create mode 100644 starters/javascript/README.md create mode 100644 starters/javascript/index.js create mode 100644 starters/javascript/package.json create mode 100644 starters/python/.github/workflows/build.yml create mode 100644 starters/python/Dockerfile create mode 100644 starters/python/README.md create mode 100644 starters/python/main.py create mode 100644 starters/python/requirements.txt create mode 100644 starters/rust/.github/workflows/build.yml create mode 100644 starters/rust/Cargo.toml create mode 100644 starters/rust/Dockerfile create mode 100644 starters/rust/README.md create mode 100644 starters/rust/src/main.rs diff --git a/starters/csharp/.github/workflows/build.yml b/starters/csharp/.github/workflows/build.yml new file mode 100644 index 0000000..217496e --- /dev/null +++ b/starters/csharp/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build and Push + +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/starters/csharp/Dockerfile b/starters/csharp/Dockerfile new file mode 100644 index 0000000..12c852c --- /dev/null +++ b/starters/csharp/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder + +WORKDIR /app +COPY acb-starter-csharp.csproj . +RUN dotnet restore +COPY Program.cs . +RUN dotnet publish -c Release -o /out + +FROM mcr.microsoft.com/dotnet/runtime:9.0-alpine + +WORKDIR /app +COPY --from=builder /out . + +ENV BOT_PORT=8080 +ENV BOT_SECRET="" + +EXPOSE 8080 + +CMD ["dotnet", "acb-starter-csharp.dll"] diff --git a/starters/csharp/Program.cs b/starters/csharp/Program.cs new file mode 100644 index 0000000..f1b61cc --- /dev/null +++ b/starters/csharp/Program.cs @@ -0,0 +1,179 @@ +// AI Code Battle - C# Starter Kit +// +// A minimal bot scaffold with HMAC authentication and a placeholder +// random strategy. Replace ComputeMoves() with your own logic. + +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +var port = Environment.GetEnvironmentVariable("BOT_PORT") ?? "8080"; +var secret = Environment.GetEnvironmentVariable("BOT_SECRET") ?? ""; + +if (string.IsNullOrEmpty(secret)) +{ + Console.Error.WriteLine("ERROR: BOT_SECRET environment variable is required"); + Environment.Exit(1); +} + +var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseUrls($"http://0.0.0.0:{port}"); +var app = builder.Build(); + +app.MapGet("/health", () => Results.Ok("OK")); + +app.MapPost("/turn", (HttpContext ctx) => +{ + var signature = ctx.Request.Headers["X-ACB-Signature"].FirstOrDefault() ?? ""; + var matchId = ctx.Request.Headers["X-ACB-Match-Id"].FirstOrDefault() ?? ""; + var turnStr = ctx.Request.Headers["X-ACB-Turn"].FirstOrDefault() ?? "0"; + var timestamp = ctx.Request.Headers["X-ACB-Timestamp"].FirstOrDefault() ?? ""; + + if (string.IsNullOrEmpty(signature)) + return Results.Unauthorized(); + + using var reader = new StreamReader(ctx.Request.Body); + var body = reader.ReadToEndAsync().GetAwaiter().GetResult(); + + if (!VerifySignature(secret, matchId, turnStr, timestamp, body, signature)) + return Results.Unauthorized(); + + GameState? state; + try + { + state = JsonSerializer.Deserialize(body); + if (state == null) return Results.BadRequest("Invalid game state"); + } + catch + { + return Results.BadRequest("Invalid JSON"); + } + + var moves = ComputeMoves(state); + var responseBody = JsonSerializer.Serialize(new { moves }); + var turn = int.Parse(turnStr); + var responseSig = SignResponse(secret, matchId, turn, responseBody); + + ctx.Response.Headers["X-ACB-Signature"] = responseSig; + return Results.Text(responseBody, "application/json"); +}); + +app.Run(); + +// --- Strategy --- +// Replace this with your own logic! + +string[] Directions = ["N", "E", "S", "W"]; + +List ComputeMoves(GameState state) +{ + var moves = new List(); + var rng = Random.Shared; + + foreach (var bot in state.Bots) + { + if (bot.Owner == state.You.Id && rng.NextDouble() < 0.5) + { + moves.Add(new Move + { + Position = bot.Position, + Direction = Directions[rng.Next(Directions.Length)] + }); + } + } + + return moves; +} + +// --- HMAC helpers --- + +static bool VerifySignature(string secret, string matchId, string turn, + string timestamp, string body, string signature) +{ + var bodyHash = Sha256Hex(Encoding.UTF8.GetBytes(body)); + var signingString = $"{matchId}.{turn}.{timestamp}.{bodyHash}"; + var expected = HmacSha256(secret, signingString); + return CryptographicOperations.FixedTimeEquals( + Convert.FromHexString(signature), + Convert.FromHexString(expected) + ); +} + +static string SignResponse(string secret, string matchId, int turn, string body) +{ + var bodyHash = Sha256Hex(Encoding.UTF8.GetBytes(body)); + var signingString = $"{matchId}.{turn}.{bodyHash}"; + return HmacSha256(secret, signingString); +} + +static string HmacSha256(string key, string data) +{ + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); + return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(data))).ToLower(); +} + +static string Sha256Hex(byte[] data) +{ + return Convert.ToHexString(SHA256.HashData(data)).ToLower(); +} + +// --- Types --- + +record GameState +{ + public string MatchId { get; init; } = ""; + public int Turn { get; init; } + public GameConfig Config { get; init; } = new(); + public You You { get; init; } = new(); + public List Bots { get; init; } = []; + public List Energy { get; init; } = []; + public List Cores { get; init; } = []; + public List Walls { get; init; } = []; + public List Dead { get; init; } = []; +} + +record GameConfig +{ + public int Rows { get; init; } + public int Cols { get; init; } + public int MaxTurns { get; init; } + public int VisionRadius2 { get; init; } + public int AttackRadius2 { get; init; } + public int SpawnCost { get; init; } + public int EnergyInterval { get; init; } +} + +record You +{ + public int Id { get; init; } + public int Energy { get; init; } + public int Score { get; init; } +} + +record VisibleBot +{ + public Position Position { get; init; } = new(); + public int Owner { get; init; } +} + +record VisibleCore +{ + public Position Position { get; init; } = new(); + public int Owner { get; init; } + public bool Active { get; init; } +} + +record Position +{ + public int Row { get; init; } + public int Col { get; init; } +} + +record Move +{ + public Position Position { get; init; } = new(); + public string Direction { get; init; } = ""; +} diff --git a/starters/csharp/README.md b/starters/csharp/README.md new file mode 100644 index 0000000..6d02304 --- /dev/null +++ b/starters/csharp/README.md @@ -0,0 +1,65 @@ +# acb-starter-csharp + +C# (.NET) starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform. + +Uses ASP.NET Core minimal API with zero external dependencies beyond the framework. + +## Quick Start + +```bash +# Run locally +export BOT_SECRET=dev-secret +dotnet run + +# Run with Docker +docker build -t my-bot . +docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot +``` + +Your bot listens on port 8080 and responds to `POST /turn` with move commands. + +## Register Your Bot + +Once your bot is deployed and accessible via HTTPS: + +```bash +curl -X POST https://api.aicodebattle.com/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-csharp-bot", + "endpoint_url": "https://my-bot.example.com", + "owner": "your-name", + "description": "My awesome bot" + }' +``` + +Save the `bot_id` and `shared_secret` from the response — the secret is shown only once. + +## Project Structure + +``` +Program.cs # HTTP server, HMAC auth, types, and strategy +acb-starter-csharp.csproj # .NET project file +Dockerfile # Container build +``` + +## Customization + +Edit `ComputeMoves()` in `Program.cs` to implement your strategy. The `GameState` record provides: + +- `Bots` — all visible bots (yours and enemies) +- `Energy` — visible energy pickup locations +- `Cores` — visible core positions +- `Walls` — visible wall positions +- `You.Energy` — your current energy count +- `You.Score` — your current score +- `Config` — match parameters (grid size, etc.) + +Return a `List`, each with the bot's current `Position` and a `Direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the response stay in place. + +## Protocol + +- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON +- **Health:** `GET /health` — must return 200 +- **Timeout:** 3 seconds per turn +- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header diff --git a/starters/csharp/acb-starter-csharp.csproj b/starters/csharp/acb-starter-csharp.csproj new file mode 100644 index 0000000..a31cf80 --- /dev/null +++ b/starters/csharp/acb-starter-csharp.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + enable + enable + Exe + + + diff --git a/starters/go/.github/workflows/build.yml b/starters/go/.github/workflows/build.yml new file mode 100644 index 0000000..217496e --- /dev/null +++ b/starters/go/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build and Push + +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/starters/go/Dockerfile b/starters/go/Dockerfile new file mode 100644 index 0000000..5b2ce88 --- /dev/null +++ b/starters/go/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.22-alpine AS builder + +WORKDIR /app +COPY go.mod . +COPY main.go . + +RUN CGO_ENABLED=0 go build -o bot . + +FROM alpine:3.21 + +WORKDIR /app +COPY --from=builder /app/bot . + +ENV BOT_PORT=8080 +ENV BOT_SECRET="" + +EXPOSE 8080 + +CMD ["./bot"] diff --git a/starters/go/README.md b/starters/go/README.md new file mode 100644 index 0000000..bd26677 --- /dev/null +++ b/starters/go/README.md @@ -0,0 +1,63 @@ +# acb-starter-go + +Go starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform. + +## Quick Start + +```bash +# Run locally +export BOT_SECRET=dev-secret +go run main.go + +# Run with Docker +docker build -t my-bot . +docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot +``` + +Your bot listens on port 8080 and responds to `POST /turn` with move commands. + +## Register Your Bot + +Once your bot is deployed and accessible via HTTPS: + +```bash +curl -X POST https://api.aicodebattle.com/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-go-bot", + "endpoint_url": "https://my-bot.example.com", + "owner": "your-name", + "description": "My awesome bot" + }' +``` + +Save the `bot_id` and `shared_secret` from the response — the secret is shown only once. + +## Project Structure + +``` +main.go # HTTP server, HMAC auth, game types, and strategy entry point +go.mod # Go module definition +Dockerfile # Multi-stage container build +``` + +## Customization + +Edit `computeMoves()` in `main.go` to implement your strategy. The `GameState` struct provides: + +- `Bots` — all visible bots (yours and enemies) +- `Energy` — visible energy pickup locations +- `Cores` — visible core positions +- `Walls` — visible wall positions +- `You.Energy` — your current energy count +- `You.Score` — your current score +- `Config` — match parameters (grid size, attack range, etc.) + +Return a slice of `Move` structs, each with the bot's current `Position` and a `Direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the response stay in place. + +## Protocol + +- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON +- **Health:** `GET /health` — must return 200 +- **Timeout:** 3 seconds per turn +- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header diff --git a/starters/go/go.mod b/starters/go/go.mod new file mode 100644 index 0000000..a3c8bf5 --- /dev/null +++ b/starters/go/go.mod @@ -0,0 +1,3 @@ +module acb-starter-go + +go 1.22 diff --git a/starters/go/main.go b/starters/go/main.go new file mode 100644 index 0000000..3d3f9e0 --- /dev/null +++ b/starters/go/main.go @@ -0,0 +1,186 @@ +// AI Code Battle - Go Starter Kit +// +// A minimal bot scaffold with HMAC authentication and a placeholder +// random strategy. Replace computeMoves() with your own logic. +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os" + "strconv" +) + +// Engine constants +var directions = []string{"N", "E", "S", "W"} + +// GameConfig holds the match configuration. +type GameConfig struct { + Rows int `json:"rows"` + Cols int `json:"cols"` + MaxTurns int `json:"max_turns"` + VisionRadius2 int `json:"vision_radius2"` + AttackRadius2 int `json:"attack_radius2"` + SpawnCost int `json:"spawn_cost"` + EnergyInterval int `json:"energy_interval"` +} + +// Position is a grid coordinate. +type Position struct { + Row int `json:"row"` + Col int `json:"col"` +} + +// VisibleBot is a bot visible in fog of war. +type VisibleBot struct { + Position Position `json:"position"` + Owner int `json:"owner"` +} + +// VisibleCore is a core visible in fog of war. +type VisibleCore struct { + Position Position `json:"position"` + Owner int `json:"owner"` + Active bool `json:"active"` +} + +// GameState is the fog-filtered state visible to this bot. +type GameState struct { + MatchID string `json:"match_id"` + Turn int `json:"turn"` + Config GameConfig `json:"config"` + You struct { + ID int `json:"id"` + Energy int `json:"energy"` + Score int `json:"score"` + } `json:"you"` + Bots []VisibleBot `json:"bots"` + Energy []Position `json:"energy"` + Cores []VisibleCore `json:"cores"` + Walls []Position `json:"walls"` + Dead []VisibleBot `json:"dead"` +} + +// Move is a movement order for one bot. +type Move struct { + Position Position `json:"position"` + Direction string `json:"direction"` +} + +// MoveResponse is sent back to the engine. +type MoveResponse struct { + Moves []Move `json:"moves"` +} + +func main() { + port := getEnv("BOT_PORT", "8080") + secret := getEnv("BOT_SECRET", "") + + if secret == "" { + log.Fatal("BOT_SECRET environment variable is required") + } + + http.HandleFunc("/turn", func(w http.ResponseWriter, r *http.Request) { + handleTurn(w, r, secret) + }) + http.HandleFunc("/health", handleHealth) + + addr := fmt.Sprintf(":%s", port) + log.Printf("Bot listening on %s", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatalf("Server failed: %v", err) + } +} + +func handleTurn(w http.ResponseWriter, r *http.Request, secret string) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "failed to read body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + sig := r.Header.Get("X-ACB-Signature") + matchID := r.Header.Get("X-ACB-Match-Id") + turnStr := r.Header.Get("X-ACB-Turn") + timestamp := r.Header.Get("X-ACB-Timestamp") + + if sig == "" || !verifySignature(secret, matchID, turnStr, timestamp, body, sig) { + http.Error(w, "invalid signature", http.StatusUnauthorized) + return + } + + var state GameState + if err := json.Unmarshal(body, &state); err != nil { + http.Error(w, "invalid game state", http.StatusBadRequest) + return + } + + moves := computeMoves(&state) + response := MoveResponse{Moves: moves} + responseBody, _ := json.Marshal(response) + + turn, _ := strconv.Atoi(turnStr) + responseSig := signResponse(secret, matchID, turn, responseBody) + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-ACB-Signature", responseSig) + w.Write(responseBody) +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} + +func computeMoves(state *GameState) []Move { + // Replace this with your strategy! + 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))], + }) + } + } + } + return moves +} + +func verifySignature(secret, matchID, turnStr, timestamp string, body []byte, signature string) bool { + bodyHash := sha256.Sum256(body) + signingString := fmt.Sprintf("%s.%s.%s.%s", matchID, turnStr, timestamp, hex.EncodeToString(bodyHash[:])) + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(signingString)) + expected := hex.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(signature), []byte(expected)) +} + +func signResponse(secret, matchID string, turn int, body []byte) string { + bodyHash := sha256.Sum256(body) + signingString := fmt.Sprintf("%s.%d.%s", matchID, turn, hex.EncodeToString(bodyHash[:])) + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(signingString)) + return hex.EncodeToString(mac.Sum(nil)) +} + +func getEnv(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/starters/java/.github/workflows/build.yml b/starters/java/.github/workflows/build.yml new file mode 100644 index 0000000..217496e --- /dev/null +++ b/starters/java/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build and Push + +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/starters/java/Dockerfile b/starters/java/Dockerfile new file mode 100644 index 0000000..998bcc0 --- /dev/null +++ b/starters/java/Dockerfile @@ -0,0 +1,19 @@ +FROM eclipse-temurin:21-alpine AS builder + +WORKDIR /app +COPY pom.xml . +COPY src ./src + +RUN apk add --no-cache maven && mvn package -DskipTests + +FROM eclipse-temurin:21-jre-alpine + +WORKDIR /app +COPY --from=builder /app/target/starter-bot-1.0.0.jar /app/bot.jar + +ENV BOT_PORT=8080 +ENV BOT_SECRET="" + +EXPOSE 8080 + +CMD ["java", "-jar", "/app/bot.jar"] diff --git a/starters/java/README.md b/starters/java/README.md new file mode 100644 index 0000000..21d60b6 --- /dev/null +++ b/starters/java/README.md @@ -0,0 +1,67 @@ +# acb-starter-java + +Java starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform. + +Uses Javalin for the HTTP server, Jackson for JSON, and `javax.crypto` for HMAC authentication. + +## Quick Start + +```bash +# Build +mvn package + +# Run locally +BOT_SECRET=dev-secret java -jar target/starter-bot-1.0.0.jar + +# Run with Docker +docker build -t my-bot . +docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot +``` + +Your bot listens on port 8080 and responds to `POST /turn` with move commands. + +## Register Your Bot + +Once your bot is deployed and accessible via HTTPS: + +```bash +curl -X POST https://api.aicodebattle.com/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-java-bot", + "endpoint_url": "https://my-bot.example.com", + "owner": "your-name", + "description": "My awesome bot" + }' +``` + +Save the `bot_id` and `shared_secret` from the response — the secret is shown only once. + +## Project Structure + +``` +src/main/java/com/acb/starter/App.java # Server, auth, types, and strategy +pom.xml # Maven build configuration +Dockerfile # Multi-stage container build +``` + +## Customization + +Edit `computeMoves()` in `App.java` to implement your strategy. The `GameState` object provides: + +- `bots` — all visible bots (yours and enemies) +- `energy` — visible energy pickup locations +- `cores` — visible core positions +- `walls` — visible wall positions +- `youEnergy` — your current energy count +- `youScore` — your current score +- `config` — match parameters (grid size, etc.) + +Return a `List>`, each entry with `position` (your bot's current position) and `direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the response stay in place. + +## Protocol + +- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON +- **Health:** `GET /health` — must return 200 +- **Timeout:** 3 seconds per turn +- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header diff --git a/starters/java/pom.xml b/starters/java/pom.xml new file mode 100644 index 0000000..b2185a6 --- /dev/null +++ b/starters/java/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + com.acb + starter-bot + 1.0.0 + jar + + ACB Starter Bot + Starter bot for AI Code Battle + + + 21 + 21 + UTF-8 + 6.3.0 + 2.17.0 + + + + + io.javalin + javalin + ${javalin.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.slf4j + slf4j-simple + 2.0.12 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.2 + + + package + + shade + + + + + com.acb.starter.App + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/starters/java/src/main/java/com/acb/starter/App.java b/starters/java/src/main/java/com/acb/starter/App.java new file mode 100644 index 0000000..5f20d7a --- /dev/null +++ b/starters/java/src/main/java/com/acb/starter/App.java @@ -0,0 +1,201 @@ +package com.acb.starter; + +import io.javalin.Javalin; +import io.javalin.http.Context; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.*; + +/** + * AI Code Battle - Java Starter Kit + * + * A minimal bot scaffold with HMAC authentication and a placeholder + * random strategy. Replace computeMoves() with your own logic. + */ +public class App { + + private static final String[] DIRECTIONS = {"N", "E", "S", "W"}; + private static final SecureRandom RANDOM = new SecureRandom(); + + private static String secret; + + public static void main(String[] args) { + String portStr = System.getenv().getOrDefault("BOT_PORT", "8080"); + secret = System.getenv().getOrDefault("BOT_SECRET", ""); + + if (secret.isEmpty()) { + System.err.println("ERROR: BOT_SECRET environment variable is required"); + System.exit(1); + } + + int port = Integer.parseInt(portStr); + + Javalin app = Javalin.create() + .start(port); + + app.get("/health", ctx -> ctx.result("OK")); + app.post("/turn", App::handleTurn); + + System.out.println("Bot listening on port " + port); + } + + private static void handleTurn(Context ctx) { + String signature = ctx.header("X-ACB-Signature"); + String matchId = ctx.header("X-ACB-Match-Id"); + String turnStr = ctx.header("X-ACB-Turn"); + String timestamp = ctx.header("X-ACB-Timestamp"); + + if (signature == null || signature.isEmpty()) { + ctx.status(401).result("Missing signature"); + return; + } + + String body = ctx.body(); + + if (!verifySignature(matchId, turnStr, timestamp, body, signature)) { + ctx.status(401).result("Invalid signature"); + return; + } + + try { + GameState state = parseGameState(body); + List> moves = computeMoves(state); + + String responseBody = toJsonMoves(moves); + int turn = Integer.parseInt(turnStr != null ? turnStr : "0"); + String responseSig = signResponse(matchId, turn, responseBody); + + ctx.status(200); + ctx.header("Content-Type", "application/json"); + ctx.header("X-ACB-Signature", responseSig); + ctx.result(responseBody); + } catch (Exception e) { + ctx.status(400).result("Invalid game state"); + } + } + + static List> computeMoves(GameState state) { + // Replace this with your strategy! + List> moves = new ArrayList<>(); + + for (Map bot : state.bots) { + int owner = ((Number) bot.get("owner")).intValue(); + if (owner == state.youId && RANDOM.nextDouble() < 0.5) { + String dir = DIRECTIONS[RANDOM.nextInt(DIRECTIONS.length)]; + Map move = new LinkedHashMap<>(); + move.put("position", bot.get("position")); + move.put("direction", dir); + moves.add(move); + } + } + + return moves; + } + + // --- JSON helpers --- + + static GameState parseGameState(String json) { + // Minimal JSON parser for the game state + GameState state = new GameState(); + Map map = parseJson(json); + state.matchId = (String) map.get("match_id"); + state.turn = ((Number) map.get("turn")).intValue(); + state.config = (Map) map.get("config"); + + Map you = (Map) map.get("you"); + state.youId = ((Number) you.get("id")).intValue(); + state.youEnergy = ((Number) you.get("energy")).intValue(); + state.youScore = ((Number) you.get("score")).intValue(); + + state.bots = (List>) map.get("bots"); + state.energy = (List>) map.get("energy"); + state.cores = (List>) map.get("cores"); + state.walls = (List>) map.get("walls"); + state.dead = (List>) map.get("dead"); + + return state; + } + + static String toJsonMoves(List> moves) { + StringBuilder sb = new StringBuilder("{\"moves\":["); + for (int i = 0; i < moves.size(); i++) { + if (i > 0) sb.append(","); + Map move = moves.get(i); + Map pos = (Map) move.get("position"); + sb.append("{\"position\":{\"row\":") + .append(pos.get("row")).append(",\"col\":").append(pos.get("col")) + .append("},\"direction\":\"").append(move.get("direction")).append("\"}"); + } + sb.append("]}"); + return sb.toString(); + } + + @SuppressWarnings("unchecked") + static Map parseJson(String json) { + return new io.javalin.json.JavalinJackson().fromJsonString(json, Map.class); + } + + // --- HMAC helpers --- + + static boolean verifySignature(String matchId, String turn, String timestamp, + String body, String signature) { + try { + String bodyHash = sha256Hex(body.getBytes(StandardCharsets.UTF_8)); + String signingString = matchId + "." + turn + "." + timestamp + "." + bodyHash; + String expected = hmacSha256(secret, signingString); + return expected.equals(signature); + } catch (Exception e) { + return false; + } + } + + static String signResponse(String matchId, int turn, String body) { + try { + String bodyHash = sha256Hex(body.getBytes(StandardCharsets.UTF_8)); + String signingString = matchId + "." + turn + "." + bodyHash; + return hmacSha256(secret, signingString); + } catch (Exception e) { + return ""; + } + } + + static String hmacSha256(String key, String data) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hash); + } + + static String sha256Hex(byte[] data) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return bytesToHex(digest.digest(data)); + } + + static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + // --- Data classes --- + + static class GameState { + String matchId; + int turn; + Map config; + int youId; + int youEnergy; + int youScore; + List> bots; + List> energy; + List> cores; + List> walls; + List> dead; + } +} diff --git a/starters/javascript/.github/workflows/build.yml b/starters/javascript/.github/workflows/build.yml new file mode 100644 index 0000000..217496e --- /dev/null +++ b/starters/javascript/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build and Push + +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/starters/javascript/Dockerfile b/starters/javascript/Dockerfile new file mode 100644 index 0000000..d854771 --- /dev/null +++ b/starters/javascript/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine + +WORKDIR /app +COPY package.json . +COPY index.js . + +ENV BOT_PORT=8080 +ENV BOT_SECRET="" + +EXPOSE 8080 + +CMD ["node", "index.js"] diff --git a/starters/javascript/README.md b/starters/javascript/README.md new file mode 100644 index 0000000..e4182ae --- /dev/null +++ b/starters/javascript/README.md @@ -0,0 +1,64 @@ +# acb-starter-javascript + +Node.js starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform. + +Uses Node.js built-in `http` module with zero external dependencies. + +## Quick Start + +```bash +# Run locally +BOT_SECRET=dev-secret node index.js + +# Run with Docker +docker build -t my-bot . +docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot +``` + +Your bot listens on port 8080 and responds to `POST /turn` with move commands. + +## Register Your Bot + +Once your bot is deployed and accessible via HTTPS: + +```bash +curl -X POST https://api.aicodebattle.com/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-js-bot", + "endpoint_url": "https://my-bot.example.com", + "owner": "your-name", + "description": "My awesome bot" + }' +``` + +Save the `bot_id` and `shared_secret` from the response — the secret is shown only once. + +## Project Structure + +``` +index.js # HTTP server, HMAC auth, and strategy entry point +package.json # Node.js project definition +Dockerfile # Container build +``` + +## Customization + +Edit `computeMoves()` in `index.js` to implement your strategy. The `state` object provides: + +- `bots` — all visible bots (yours and enemies) +- `energy` — visible energy pickup locations +- `cores` — visible core positions +- `walls` — visible wall positions +- `you.energy` — your current energy count +- `you.score` — your current score +- `config` — match parameters (grid size, etc.) + +Return an array of moves, each with `position` (your bot's current position) and `direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the response stay in place. + +## Protocol + +- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON +- **Health:** `GET /health` — must return 200 +- **Timeout:** 3 seconds per turn +- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header diff --git a/starters/javascript/index.js b/starters/javascript/index.js new file mode 100644 index 0000000..ee3a719 --- /dev/null +++ b/starters/javascript/index.js @@ -0,0 +1,124 @@ +/** + * AI Code Battle - JavaScript (Node.js) Starter Kit + * + * A minimal bot scaffold with HMAC authentication and a placeholder + * random strategy. Replace computeMoves() with your own logic. + */ + +const http = require("http"); +const crypto = require("crypto"); + +const PORT = parseInt(process.env.BOT_PORT || "8080", 10); +const SECRET = process.env.BOT_SECRET || ""; + +if (!SECRET) { + console.error("ERROR: BOT_SECRET environment variable is required"); + process.exit(1); +} + +const DIRECTIONS = ["N", "E", "S", "W"]; + +// --- HMAC helpers --- + +function verifySignature(body, matchId, turn, timestamp, signature) { + const bodyHash = crypto.createHash("sha256").update(body).digest("hex"); + const signingString = `${matchId}.${turn}.${timestamp}.${bodyHash}`; + const expected = crypto + .createHmac("sha256", SECRET) + .update(signingString) + .digest("hex"); + return crypto.timingSafeEqual( + Buffer.from(signature, "hex"), + Buffer.from(expected, "hex") + ); +} + +function signResponse(body, matchId, turn) { + const bodyHash = crypto.createHash("sha256").update(body).digest("hex"); + const signingString = `${matchId}.${turn}.${bodyHash}`; + return crypto + .createHmac("sha256", SECRET) + .update(signingString) + .digest("hex"); +} + +// --- Strategy --- + +function computeMoves(state) { + // Replace this with your strategy! + const moves = []; + 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)], + }); + } + } + } + return moves; +} + +// --- HTTP server --- + +const server = http.createServer((req, res) => { + if (req.method === "GET" && req.url === "/health") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("OK"); + return; + } + + if (req.method === "POST" && req.url === "/turn") { + const chunks = []; + req.on("data", (chunk) => chunks.push(chunk)); + req.on("end", () => { + const body = Buffer.concat(chunks); + + const matchId = req.headers["x-acb-match-id"] || ""; + const turn = req.headers["x-acb-turn"] || "0"; + const timestamp = req.headers["x-acb-timestamp"] || ""; + const signature = req.headers["x-acb-signature"] || ""; + + if ( + !signature || + !verifySignature(body, matchId, turn, timestamp, signature) + ) { + res.writeHead(401, { "Content-Type": "text/plain" }); + res.end("Invalid signature"); + return; + } + + let state; + try { + state = JSON.parse(body.toString()); + } catch { + res.writeHead(400, { "Content-Type": "text/plain" }); + res.end("Invalid JSON"); + return; + } + + const moves = computeMoves(state); + const responseBody = JSON.stringify({ moves }); + const responseSig = signResponse( + Buffer.from(responseBody), + matchId, + parseInt(turn, 10) + ); + + res.writeHead(200, { + "Content-Type": "application/json", + "X-ACB-Signature": responseSig, + }); + res.end(responseBody); + }); + return; + } + + res.writeHead(404); + res.end("Not Found"); +}); + +server.listen(PORT, () => { + console.log(`Bot listening on port ${PORT}`); +}); diff --git a/starters/javascript/package.json b/starters/javascript/package.json new file mode 100644 index 0000000..36db174 --- /dev/null +++ b/starters/javascript/package.json @@ -0,0 +1,12 @@ +{ + "name": "acb-starter-javascript", + "version": "1.0.0", + "description": "Node.js starter kit for AI Code Battle", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/starters/python/.github/workflows/build.yml b/starters/python/.github/workflows/build.yml new file mode 100644 index 0000000..217496e --- /dev/null +++ b/starters/python/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build and Push + +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/starters/python/Dockerfile b/starters/python/Dockerfile new file mode 100644 index 0000000..087921a --- /dev/null +++ b/starters/python/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13-slim + +WORKDIR /app +COPY main.py . +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +ENV BOT_PORT=8080 +ENV BOT_SECRET="" + +EXPOSE 8080 + +CMD ["python3", "main.py"] diff --git a/starters/python/README.md b/starters/python/README.md new file mode 100644 index 0000000..e9f4f0d --- /dev/null +++ b/starters/python/README.md @@ -0,0 +1,63 @@ +# acb-starter-python + +Python 3 starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform. + +## Quick Start + +```bash +# Run locally +pip install -r requirements.txt +BOT_SECRET=dev-secret python3 main.py + +# Run with Docker +docker build -t my-bot . +docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot +``` + +Your bot listens on port 8080 and responds to `POST /turn` with move commands. + +## Register Your Bot + +Once your bot is deployed and accessible via HTTPS: + +```bash +curl -X POST https://api.aicodebattle.com/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-python-bot", + "endpoint_url": "https://my-bot.example.com", + "owner": "your-name", + "description": "My awesome bot" + }' +``` + +Save the `bot_id` and `shared_secret` from the response — the secret is shown only once. + +## Project Structure + +``` +main.py # HTTP server, HMAC auth, and strategy entry point +requirements.txt # Python dependencies (stdlib only for this starter) +Dockerfile # Container build +``` + +## Customization + +Edit `compute_moves()` in `main.py` to implement your strategy. The `GameState` object provides: + +- `bots` — all visible bots (yours and enemies) +- `energy` — visible energy pickup locations +- `cores` — visible core positions +- `walls` — visible wall positions +- `you_energy` — your current energy count +- `you_score` — your current score +- `config` — match parameters (grid size, etc.) + +Return a list of moves, each with `position` (your bot's current position) and `direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the moves list stay in place. + +## Protocol + +- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON +- **Health:** `GET /health` — must return 200 +- **Timeout:** 3 seconds per turn +- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header diff --git a/starters/python/main.py b/starters/python/main.py new file mode 100644 index 0000000..82a4d61 --- /dev/null +++ b/starters/python/main.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +AI Code Battle - Python Starter Kit + +A minimal bot scaffold. Implements the HTTP protocol with HMAC +authentication and a placeholder random strategy. + +Usage: + BOT_SECRET=your-secret python3 main.py +""" + +import hashlib +import hmac +import json +import os +import random +from http.server import HTTPServer, BaseHTTPRequestHandler + +# Engine constants +DIRECTIONS = ["N", "E", "S", "W"] + + +class GameState: + def __init__(self, data: dict): + self.match_id = data["match_id"] + self.turn = data["turn"] + self.config = data["config"] + self.you_id = data["you"]["id"] + self.you_energy = data["you"]["energy"] + self.you_score = data["you"]["score"] + self.bots = data.get("bots", []) + self.energy = data.get("energy", []) + self.cores = data.get("cores", []) + self.walls = data.get("walls", []) + self.dead = data.get("dead", []) + + +class BotHandler(BaseHTTPRequestHandler): + secret: str = "" + + def log_message(self, format, *args): + pass + + def sign_response(self, body: bytes, match_id: str, turn: int) -> str: + body_hash = hashlib.sha256(body).hexdigest() + signing_string = f"{match_id}.{turn}.{body_hash}" + return hmac.new( + self.secret.encode(), signing_string.encode(), hashlib.sha256 + ).hexdigest() + + def verify_signature(self, body: bytes, match_id: str, turn: str, + timestamp: str, signature: str) -> bool: + body_hash = hashlib.sha256(body).hexdigest() + signing_string = f"{match_id}.{turn}.{timestamp}.{body_hash}" + expected = hmac.new( + self.secret.encode(), signing_string.encode(), hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(signature, expected) + + def do_GET(self): + if self.path == "/health": + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.end_headers() + self.wfile.write(b"OK") + else: + self.send_error(404) + + def do_POST(self): + if self.path != "/turn": + self.send_error(404) + return + + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + + match_id = self.headers.get("X-ACB-Match-Id", "") + turn_str = self.headers.get("X-ACB-Turn", "0") + timestamp = self.headers.get("X-ACB-Timestamp", "") + signature = self.headers.get("X-ACB-Signature", "") + + if not signature or not self.verify_signature( + body, match_id, turn_str, timestamp, signature + ): + self.send_error(401, "Invalid signature") + return + + try: + state = GameState(json.loads(body)) + except (json.JSONDecodeError, KeyError) as e: + self.send_error(400, f"Invalid game state: {e}") + return + + moves = compute_moves(state) + turn = int(turn_str) + + response_body = json.dumps({"moves": moves}).encode() + response_sig = self.sign_response(response_body, match_id, turn) + + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("X-ACB-Signature", response_sig) + self.end_headers() + self.wfile.write(response_body) + + +def compute_moves(state: GameState) -> list: + """Replace this with your strategy!""" + 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), + }) + return moves + + +def main(): + port = int(os.environ.get("BOT_PORT", "8080")) + secret = os.environ.get("BOT_SECRET", "") + + if not secret: + print("ERROR: BOT_SECRET environment variable is required") + exit(1) + + BotHandler.secret = secret + server = HTTPServer(("", port), BotHandler) + print(f"Bot listening on port {port}") + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/starters/python/requirements.txt b/starters/python/requirements.txt new file mode 100644 index 0000000..f7c7998 --- /dev/null +++ b/starters/python/requirements.txt @@ -0,0 +1,4 @@ +# No external dependencies required — uses Python stdlib only. +# Add your own dependencies here, e.g.: +# flask==3.0.0 +# numpy==2.0.0 diff --git a/starters/rust/.github/workflows/build.yml b/starters/rust/.github/workflows/build.yml new file mode 100644 index 0000000..217496e --- /dev/null +++ b/starters/rust/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build and Push + +on: + push: + branches: [main] + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/starters/rust/Cargo.toml b/starters/rust/Cargo.toml new file mode 100644 index 0000000..f2b8252 --- /dev/null +++ b/starters/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "acb-starter-bot" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +hmac = "0.12" +sha2 = "0.10" +hex = "0.4" +rand = "0.8" + +[profile.release] +strip = true +opt-level = "z" +lto = true diff --git a/starters/rust/Dockerfile b/starters/rust/Dockerfile new file mode 100644 index 0000000..7b4b723 --- /dev/null +++ b/starters/rust/Dockerfile @@ -0,0 +1,19 @@ +FROM rust:1.85-alpine AS builder + +WORKDIR /app +COPY Cargo.toml ./ +COPY src ./src + +RUN cargo build --release + +FROM alpine:3.21 + +WORKDIR /app +COPY --from=builder /app/target/release/acb-starter-bot /app/bot + +ENV BOT_PORT=8080 +ENV BOT_SECRET="" + +EXPOSE 8080 + +CMD ["./bot"] diff --git a/starters/rust/README.md b/starters/rust/README.md new file mode 100644 index 0000000..0df7e11 --- /dev/null +++ b/starters/rust/README.md @@ -0,0 +1,65 @@ +# acb-starter-rust + +Rust starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform. + +Uses `axum` for the HTTP server with `serde` for JSON and `hmac`/`sha2` for authentication. + +## Quick Start + +```bash +# Run locally +export BOT_SECRET=dev-secret +cargo run + +# Run with Docker +docker build -t my-bot . +docker run -e BOT_SECRET=your-secret -p 8080:8080 my-bot +``` + +Your bot listens on port 8080 and responds to `POST /turn` with move commands. + +## Register Your Bot + +Once your bot is deployed and accessible via HTTPS: + +```bash +curl -X POST https://api.aicodebattle.com/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-rust-bot", + "endpoint_url": "https://my-bot.example.com", + "owner": "your-name", + "description": "My awesome bot" + }' +``` + +Save the `bot_id` and `shared_secret` from the response — the secret is shown only once. + +## Project Structure + +``` +src/main.rs # HTTP server, HMAC auth, game types, and strategy entry point +Cargo.toml # Rust dependencies +Dockerfile # Multi-stage container build +``` + +## Customization + +Edit `compute_moves()` in `src/main.rs` to implement your strategy. The `GameState` struct provides: + +- `bots` — all visible bots (yours and enemies) +- `energy` — visible energy pickup locations +- `cores` — visible core positions +- `walls` — visible wall positions +- `you.energy` — your current energy count +- `you.score` — your current score +- `config` — match parameters (grid size, etc.) + +Return a `Vec`, each with the bot's current `position` and a `direction` (`"N"`, `"E"`, `"S"`, or `"W"`). Bots not included in the response stay in place. + +## Protocol + +- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON +- **Health:** `GET /health` — must return 200 +- **Timeout:** 3 seconds per turn +- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header diff --git a/starters/rust/src/main.rs b/starters/rust/src/main.rs new file mode 100644 index 0000000..c5a7809 --- /dev/null +++ b/starters/rust/src/main.rs @@ -0,0 +1,222 @@ +//! AI Code Battle - Rust Starter Kit +//! +//! A minimal bot scaffold with HMAC authentication and a placeholder +//! random strategy. Replace `compute_moves()` with your own logic. + +use axum::{ + body::Bytes, + extract::State, + http::{HeaderMap, StatusCode}, + routing::{get, post}, + Json, Router, +}; +use hmac::{Hmac, Mac}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::env; + +type HmacSha256 = Hmac; + +// Engine constants +const DIRECTIONS: [&str; 4] = ["N", "E", "S", "W"]; + +#[derive(Deserialize)] +struct GameState { + match_id: String, + turn: u32, + config: GameConfig, + you: You, + bots: Vec, + energy: Vec, + cores: Vec, + walls: Vec, + dead: Vec, +} + +#[derive(Deserialize)] +struct GameConfig { + rows: u32, + cols: u32, + max_turns: u32, + vision_radius2: u32, + attack_radius2: u32, + spawn_cost: u32, + energy_interval: u32, +} + +#[derive(Deserialize)] +struct You { + id: u32, + energy: u32, + score: u32, +} + +#[derive(Deserialize, Serialize, Clone)] +struct Position { + row: u32, + col: u32, +} + +#[derive(Deserialize)] +struct VisibleBot { + position: Position, + owner: u32, +} + +#[derive(Deserialize)] +struct VisibleCore { + position: Position, + owner: u32, + active: bool, +} + +#[derive(Serialize)] +struct MoveResponse { + moves: Vec, +} + +#[derive(Serialize)] +struct Move { + position: Position, + direction: String, +} + +struct AppState { + secret: String, +} + +#[tokio::main] +async fn main() { + let port = env::var("BOT_PORT").unwrap_or_else(|_| "8080".into()); + let secret = env::var("BOT_SECRET").expect("BOT_SECRET is required"); + + let state = AppState { secret }; + let app = Router::new() + .route("/turn", post(handle_turn)) + .route("/health", get(handle_health)) + .with_state(state); + + let addr = format!("0.0.0.0:{}", port); + println!("Bot listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +async fn handle_health() -> &'static str { + "OK" +} + +async fn handle_turn( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result<(StatusCode, [(&str, String); 2], String), StatusCode> { + let signature = headers + .get("X-ACB-Signature") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + let match_id = headers + .get("X-ACB-Match-Id") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + let turn_str = headers + .get("X-ACB-Turn") + .and_then(|v| v.to_str().ok()) + .unwrap_or("0"); + let timestamp = headers + .get("X-ACB-Timestamp") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + if signature.is_empty() + || !verify_signature( + &state.secret, + match_id, + turn_str, + timestamp, + &body, + signature, + ) + { + return Err(StatusCode::UNAUTHORIZED); + } + + let game_state: GameState = + serde_json::from_slice(&body).map_err(|_| StatusCode::BAD_REQUEST)?; + + let moves = compute_moves(&game_state); + let response = MoveResponse { moves }; + let response_body = serde_json::to_string(&response).unwrap(); + + let turn: u32 = turn_str.parse().unwrap_or(0); + let response_sig = sign_response(&state.secret, match_id, turn, response_body.as_bytes()); + + Ok(( + StatusCode::OK, + [ + ("Content-Type".to_string(), "application/json".to_string()), + ("X-ACB-Signature".to_string(), response_sig), + ], + response_body, + )) +} + +fn compute_moves(state: &GameState) -> Vec { + // Replace this with your strategy! + let mut moves = Vec::new(); + let mut rng = rand::thread_rng(); + + for bot in &state.bots { + if bot.owner == state.you.id && 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(), + direction: dir.to_string(), + }); + } + } + moves +} + +fn verify_signature( + secret: &str, + match_id: &str, + turn: &str, + timestamp: &str, + body: &[u8], + signature: &str, +) -> bool { + use hex::FromHex; + let body_hash = sha2::Sha256::digest(body); + let signing_string = format!( + "{}.{}.{}.{}", + match_id, + turn, + timestamp, + hex::encode(body_hash) + ); + + let mut mac = + HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC key error"); + mac.update(signing_string.as_bytes()); + let expected = mac.finalize().into_bytes(); + + match Vec::from_hex(signature) { + Ok(sig_bytes) => { + let sig_truncated: &[u8] = &sig_bytes; + hmac::digest::constant_time_eq(sig_truncated, &expected) + } + Err(_) => false, + } +} + +fn sign_response(secret: &str, match_id: &str, turn: u32, body: &[u8]) -> String { + let body_hash = sha2::Sha256::digest(body); + let signing_string = format!("{}.{}.{}", match_id, turn, hex::encode(body_hash)); + + let mut mac = + HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC key error"); + mac.update(signing_string.as_bytes()); + hex::encode(mac.finalize().into_bytes()) +} diff --git a/web/src/pages/docs.ts b/web/src/pages/docs.ts index 21a9010..a117eb1 100644 --- a/web/src/pages/docs.ts +++ b/web/src/pages/docs.ts @@ -71,8 +71,30 @@ export function renderDocsPage(): void {
-

Example Bot

-

See the example bots in various languages for reference implementations.

+

Starter Kits

+

Fork a starter kit to get a working bot in minutes. Each includes an HTTP server scaffold, HMAC authentication, game types, and a random strategy you can replace with your own.

+ +
+ +
+

Register Your Bot

+

Once your bot is deployed and accessible via HTTPS, register it:

+
curl -X POST https://api.aicodebattle.com/api/register \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "name": "my-bot",
+    "endpoint_url": "https://my-bot.example.com",
+    "owner": "your-name",
+    "description": "My awesome bot"
+  }'
+

The response contains your bot_id and shared_secret. Save the secret — it's shown only once.

@@ -94,6 +116,8 @@ export function renderDocsPage(): void { .docs-content pre { background-color: var(--bg-primary); border-radius: 6px; padding: 16px; overflow-x: auto; margin: 10px 0; } .docs-content code { font-family: 'Fira Code', 'Monaco', monospace; font-size: 0.875rem; color: var(--text-secondary); } .docs-content a { color: var(--accent); } + .starter-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 8px; } + .starter-links li { margin-bottom: 0; } `; }