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:
parent
91d807cec2
commit
7bf6566823
31 changed files with 1859 additions and 2 deletions
29
starters/csharp/.github/workflows/build.yml
vendored
Normal file
29
starters/csharp/.github/workflows/build.yml
vendored
Normal 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/csharp/Dockerfile
Normal file
19
starters/csharp/Dockerfile
Normal 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
179
starters/csharp/Program.cs
Normal 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
65
starters/csharp/README.md
Normal 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
|
||||
10
starters/csharp/acb-starter-csharp.csproj
Normal file
10
starters/csharp/acb-starter-csharp.csproj
Normal 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
29
starters/go/.github/workflows/build.yml
vendored
Normal 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
19
starters/go/Dockerfile
Normal 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
63
starters/go/README.md
Normal 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
3
starters/go/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module acb-starter-go
|
||||
|
||||
go 1.22
|
||||
186
starters/go/main.go
Normal file
186
starters/go/main.go
Normal 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
|
||||
}
|
||||
29
starters/java/.github/workflows/build.yml
vendored
Normal file
29
starters/java/.github/workflows/build.yml
vendored
Normal 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
19
starters/java/Dockerfile
Normal 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
67
starters/java/README.md
Normal 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
75
starters/java/pom.xml
Normal 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>
|
||||
201
starters/java/src/main/java/com/acb/starter/App.java
Normal file
201
starters/java/src/main/java/com/acb/starter/App.java
Normal 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;
|
||||
}
|
||||
}
|
||||
29
starters/javascript/.github/workflows/build.yml
vendored
Normal file
29
starters/javascript/.github/workflows/build.yml
vendored
Normal 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 }}
|
||||
12
starters/javascript/Dockerfile
Normal file
12
starters/javascript/Dockerfile
Normal 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"]
|
||||
64
starters/javascript/README.md
Normal file
64
starters/javascript/README.md
Normal 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
|
||||
124
starters/javascript/index.js
Normal file
124
starters/javascript/index.js
Normal 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}`);
|
||||
});
|
||||
12
starters/javascript/package.json
Normal file
12
starters/javascript/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
29
starters/python/.github/workflows/build.yml
vendored
Normal file
29
starters/python/.github/workflows/build.yml
vendored
Normal 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 }}
|
||||
14
starters/python/Dockerfile
Normal file
14
starters/python/Dockerfile
Normal 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
63
starters/python/README.md
Normal 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
135
starters/python/main.py
Normal 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()
|
||||
4
starters/python/requirements.txt
Normal file
4
starters/python/requirements.txt
Normal 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
|
||||
29
starters/rust/.github/workflows/build.yml
vendored
Normal file
29
starters/rust/.github/workflows/build.yml
vendored
Normal 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
19
starters/rust/Cargo.toml
Normal 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
19
starters/rust/Dockerfile
Normal 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
65
starters/rust/README.md
Normal 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
222
starters/rust/src/main.rs
Normal 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())
|
||||
}
|
||||
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue