feat(starters,web): add 6-language bot starter kits per §6

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 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-21 13:58:27 -04:00
parent 91d807cec2
commit 7bf6566823
31 changed files with 1859 additions and 2 deletions

View file

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

View file

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

179
starters/csharp/Program.cs Normal file
View file

@ -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<GameState>(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<Move> ComputeMoves(GameState state)
{
var moves = new List<Move>();
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<VisibleBot> Bots { get; init; } = [];
public List<Position> Energy { get; init; } = [];
public List<VisibleCore> Cores { get; init; } = [];
public List<Position> Walls { get; init; } = [];
public List<VisibleBot> 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; } = "";
}

65
starters/csharp/README.md Normal file
View file

@ -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<Move>`, 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

View file

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>

29
starters/go/.github/workflows/build.yml vendored Normal file
View file

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

19
starters/go/Dockerfile Normal file
View file

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

63
starters/go/README.md Normal file
View file

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

3
starters/go/go.mod Normal file
View file

@ -0,0 +1,3 @@
module acb-starter-go
go 1.22

186
starters/go/main.go Normal file
View file

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

View file

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

19
starters/java/Dockerfile Normal file
View file

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

67
starters/java/README.md Normal file
View file

@ -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<Map<String, Object>>`, 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

75
starters/java/pom.xml Normal file
View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acb</groupId>
<artifactId>starter-bot</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>ACB Starter Bot</name>
<description>Starter bot for AI Code Battle</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javalin.version>6.3.0</javalin.version>
<jackson.version>2.17.0</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>${javalin.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.acb.starter.App</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -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<Map<String, Object>> 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<Map<String, Object>> computeMoves(GameState state) {
// Replace this with your strategy!
List<Map<String, Object>> moves = new ArrayList<>();
for (Map<String, Object> 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<String, Object> 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<String, Object> map = parseJson(json);
state.matchId = (String) map.get("match_id");
state.turn = ((Number) map.get("turn")).intValue();
state.config = (Map<String, Object>) map.get("config");
Map<String, Object> you = (Map<String, Object>) 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<String, Object>>) map.get("bots");
state.energy = (List<Map<String, Object>>) map.get("energy");
state.cores = (List<Map<String, Object>>) map.get("cores");
state.walls = (List<Map<String, Object>>) map.get("walls");
state.dead = (List<Map<String, Object>>) map.get("dead");
return state;
}
static String toJsonMoves(List<Map<String, Object>> moves) {
StringBuilder sb = new StringBuilder("{\"moves\":[");
for (int i = 0; i < moves.size(); i++) {
if (i > 0) sb.append(",");
Map<String, Object> move = moves.get(i);
Map<String, Object> pos = (Map<String, Object>) 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<String, Object> 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<String, Object> config;
int youId;
int youEnergy;
int youScore;
List<Map<String, Object>> bots;
List<Map<String, Object>> energy;
List<Map<String, Object>> cores;
List<Map<String, Object>> walls;
List<Map<String, Object>> dead;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

63
starters/python/README.md Normal file
View file

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

135
starters/python/main.py Normal file
View file

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

View file

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

View file

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

19
starters/rust/Cargo.toml Normal file
View file

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

19
starters/rust/Dockerfile Normal file
View file

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

65
starters/rust/README.md Normal file
View file

@ -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<Move>`, 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

222
starters/rust/src/main.rs Normal file
View file

@ -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<Sha256>;
// 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<VisibleBot>,
energy: Vec<Position>,
cores: Vec<VisibleCore>,
walls: Vec<Position>,
dead: Vec<VisibleBot>,
}
#[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<Move>,
}
#[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<AppState>,
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<Move> {
// 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())
}

View file

@ -71,8 +71,30 @@ export function renderDocsPage(): void {
</section>
<section>
<h2>Example Bot</h2>
<p>See the <a href="https://github.com/aicodebattle/acb/tree/main/bots" target="_blank">example bots</a> in various languages for reference implementations.</p>
<h2>Starter Kits</h2>
<p>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.</p>
<ul class="starter-links">
<li><a href="https://github.com/jedarden/acb-starter-python" target="_blank">Python 3</a> stdlib HTTP server, zero dependencies</li>
<li><a href="https://github.com/jedarden/acb-starter-go" target="_blank">Go</a> net/http, single-binary deploy</li>
<li><a href="https://github.com/jedarden/acb-starter-javascript" target="_blank">JavaScript (Node.js)</a> zero dependencies, built-in http module</li>
<li><a href="https://github.com/jedarden/acb-starter-rust" target="_blank">Rust</a> axum + serde, minimal binary</li>
<li><a href="https://github.com/jedarden/acb-starter-java" target="_blank">Java</a> Javalin, Maven-based</li>
<li><a href="https://github.com/jedarden/acb-starter-csharp" target="_blank">C# (.NET)</a> ASP.NET Core minimal API</li>
</ul>
</section>
<section>
<h2>Register Your Bot</h2>
<p>Once your bot is deployed and accessible via HTTPS, register it:</p>
<pre><code>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"
}'</code></pre>
<p>The response contains your <code>bot_id</code> and <code>shared_secret</code>. Save the secret it's shown only once.</p>
</section>
<section>
@ -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; }
</style>
`;
}