feat(starter-typescript): add TypeScript/Node.js starter kit with Fastify

- Fastify HTTP server with HMAC-SHA256 authentication
- Full TypeScript type definitions for game protocol
- Grid utilities: toroidal distance, BFS, neighbors
- HMAC signing/verification via Node.js crypto
- Multi-stage Dockerfile for production builds
- GitHub Actions workflow for CI/CD
- Placeholder strategy that moves toward energy
- ES modules with Node.js 20+ support
This commit is contained in:
jedarden 2026-05-08 10:29:49 -04:00
parent fb6eeaed6a
commit 164fcd225b
13 changed files with 1634 additions and 0 deletions

View file

@ -0,0 +1,8 @@
node_modules/
dist/
*.log
.DS_Store
.git/
.github/
.gitignore
README.md

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

4
starters/typescript/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
dist/
*.log
.DS_Store

View file

@ -0,0 +1,45 @@
# AI Code Battle - TypeScript Starter Bot
FROM node:22-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
# Install all dependencies (including devDependencies for TypeScript build)
RUN npm ci
# Copy source code
COPY src/ ./src/
COPY tsconfig.json ./
# Build TypeScript
RUN npx tsc
# Production stage
FROM node:22-alpine
WORKDIR /app
# Copy only production dependencies and built code
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
# Set non-root user for security
RUN addgroup -g 1001 -S acb && \
adduser -S -D -H -u 1001 -s /sbin/nologin -G acb -g acb acb
USER acb
# Expose bot port
EXPOSE 8080
# Set required environment variable (default for dev)
ENV BOT_SECRET=dev-secret
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"
# Start the bot
CMD ["node", "dist/index.js"]

View file

@ -0,0 +1,147 @@
# acb-starter-typescript
TypeScript/Node.js starter kit for [AI Code Battle](https://aicodebattle.com) — a competitive bot programming platform.
Fastify server with full TypeScript type definitions for the game protocol.
## Quick Start
```bash
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run locally (requires BOT_SECRET)
BOT_SECRET=your-secret npm start
# Or run in development mode (builds and starts)
npm run dev
```
## Run with Docker
```bash
# Build image
docker build -t my-ts-bot .
# Run container
docker run -e BOT_SECRET=your-secret -p 8080:8080 my-ts-bot
```
## 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-ts-bot",
"endpoint_url": "https://my-bot.example.com",
"owner": "your-name",
"description": "My awesome TypeScript bot"
}'
```
Save the `bot_id` and `shared_secret` from the response — the secret is shown only once.
## Project Structure
```
src/
├── index.ts # Fastify HTTP server, HMAC auth, request handling
├── types.ts # Complete TypeScript type definitions for the protocol
├── auth.ts # HMAC signing/verification utilities
├── strategy.ts # Bot strategy - implement your logic here
└── grid.ts # Toroidal grid utilities (distance, BFS, neighbors)
package.json
tsconfig.json
Dockerfile
```
## Type Definitions
The `src/types.ts` file contains complete TypeScript definitions for:
- `GameState` - Fog-filtered game state sent each turn
- `GameConfig` - Match configuration parameters
- `PlayerInfo` - Your player information (energy, score)
- `Move` - Movement order for a single bot
- `MoveResponse` - Response format with optional debug telemetry
- `Direction` - Cardinal direction type ("N" | "E" | "S" | "W")
## Grid Helpers
`src/grid.ts` provides utility functions for the toroidal grid:
- `toroidalManhattan()` - Manhattan distance with wrap-around
- `toroidalChebyshev()` - Chebyshev distance with wrap-around
- `toroidalDistanceSquared()` - Squared Euclidean distance (faster for comparisons)
- `neighbors()` - 8-directional neighbors with wrap
- `cardinalNeighbors()` - 4 cardinal neighbors with direction labels
- `bfs()` - BFS pathfinding, returns path or null
- `findNearest()` - Find closest target from a list
## Customization
Edit `computeMoves()` in `src/strategy.ts` to implement your strategy.
The `state` object provides:
- `state.bots` - All visible bots (yours and enemies)
- `state.energy` - Visible energy pickup locations
- `state.cores` - Visible core positions
- `state.walls` - Visible wall positions
- `state.you.energy` - Your current energy count
- `state.you.score` - Your current score
- `state.config` - Match parameters (grid size, etc.)
Return an array of `Move` objects, each with:
- `row`, `col` - Your bot's current position
- `direction` - One of "N", "E", "S", or "W"
Bots not included in the response stay in place.
## Debug Telemetry
Optional debug info can be included in your response for replay visualization:
```typescript
const response: MoveResponse = {
moves: computedMoves,
debug: {
reasoning: "Moving toward energy at (15, 20)",
targets: [
{ row: 15, col: 20, label: "energy", priority: 0.9 }
],
values: {
energy_reserves: state.you.energy,
mode: "gathering"
}
}
};
```
Debug data is stored in replays but never parsed by the engine.
## Protocol
- **Endpoint:** `POST /turn` — receives game state JSON, returns moves JSON
- **Health:** `GET /health` — must return 200 (used during registration)
- **Timeout:** 3 seconds per turn
- **Auth:** HMAC-SHA256 via `X-ACB-Signature` header
## Development
```bash
# Type checking without emitting
npm run typecheck
# Build for production
npm run build
# Start production server
BOT_SECRET=dev-secret npm start
```

614
starters/typescript/package-lock.json generated Normal file
View file

@ -0,0 +1,614 @@
{
"name": "acb-starter-typescript",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "acb-starter-typescript",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"fastify": "^5.2.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"typescript": "^5.7.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@fastify/ajv-compiler": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz",
"integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^3.0.1",
"fast-uri": "^3.0.0"
}
},
"node_modules/@fastify/error": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
"integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/@fastify/fast-json-stringify-compiler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz",
"integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"fast-json-stringify": "^6.0.0"
}
},
"node_modules/@fastify/forwarded": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz",
"integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/@fastify/merge-json-schemas": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
"integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/@fastify/proxy-addr": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",
"integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"@fastify/forwarded": "^3.0.0",
"ipaddr.js": "^2.1.0"
}
},
"node_modules/@pinojs/redact": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="
},
"node_modules/@types/node": {
"version": "22.19.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz",
"integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==",
"dev": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/avvio": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz",
"integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"@fastify/error": "^4.0.0",
"fastq": "^1.17.1"
}
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-decode-uri-component": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
"integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-json-stringify": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz",
"integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"@fastify/merge-json-schemas": "^0.2.0",
"ajv": "^8.12.0",
"ajv-formats": "^3.0.1",
"fast-uri": "^3.0.0",
"json-schema-ref-resolver": "^3.0.0",
"rfdc": "^1.2.0"
}
},
"node_modules/fast-querystring": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
"integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
"dependencies": {
"fast-decode-uri-component": "^1.0.1"
}
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/fastify": {
"version": "5.8.5",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz",
"integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"@fastify/ajv-compiler": "^4.0.5",
"@fastify/error": "^4.0.0",
"@fastify/fast-json-stringify-compiler": "^5.0.0",
"@fastify/proxy-addr": "^5.0.0",
"abstract-logging": "^2.0.1",
"avvio": "^9.0.0",
"fast-json-stringify": "^6.0.0",
"find-my-way": "^9.0.0",
"light-my-request": "^6.0.0",
"pino": "^9.14.0 || ^10.1.0",
"process-warning": "^5.0.0",
"rfdc": "^1.3.1",
"secure-json-parse": "^4.0.0",
"semver": "^7.6.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/find-my-way": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz",
"integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-querystring": "^1.0.0",
"safe-regex2": "^5.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/ipaddr.js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
"integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
"engines": {
"node": ">= 10"
}
},
"node_modules/json-schema-ref-resolver": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
"integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/light-my-request": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
"integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"cookie": "^1.0.1",
"process-warning": "^4.0.0",
"set-cookie-parser": "^2.6.0"
}
},
"node_modules/light-my-request/node_modules/process-warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
"integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/pino": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
"dependencies": {
"@pinojs/redact": "^0.4.0",
"atomic-sleep": "^1.0.0",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^3.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^4.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-std-serializers": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ret": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
"integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
"engines": {
"node": ">=10"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"node_modules/safe-regex2": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz",
"integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"dependencies": {
"ret": "~0.5.0"
},
"bin": {
"safe-regex2": "bin/safe-regex2.js"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"engines": {
"node": ">=10"
}
},
"node_modules/secure-json-parse": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/thread-stream": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
"dependencies": {
"real-require": "^0.2.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/toad-cache": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
"integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
"engines": {
"node": ">=12"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
}
}
}

View file

@ -0,0 +1,25 @@
{
"name": "acb-starter-typescript",
"version": "1.0.0",
"description": "TypeScript/Node.js starter kit for AI Code Battle",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js",
"typecheck": "tsc --noEmit"
},
"keywords": ["ai-code-battle", "bot", "game", "typescript"],
"license": "MIT",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"fastify": "^5.2.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"typescript": "^5.7.2"
}
}

View file

@ -0,0 +1,115 @@
/**
* AI Code Battle - HMAC Authentication
*
* Implements HMAC-SHA256 signing and verification for the game protocol.
*/
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
/**
* Verify the HMAC signature on an incoming request.
*
* @param body - Raw request body as Buffer
* @param matchId - Match ID from header
* @param turn - Turn number from header
* @param timestamp - Timestamp from header
* @param signature - X-ACB-Signature header value
* @param secret - Your bot's shared secret
* @returns true if signature is valid
*/
export function verifySignature(
body: Buffer,
matchId: string,
turn: string,
timestamp: string,
signature: string,
secret: string
): boolean {
const bodyHash = createHash("sha256").update(body).digest("hex");
const signingString = `${matchId}.${turn}.${timestamp}.${bodyHash}`;
const expected = createHmac("sha256", secret)
.update(signingString)
.digest("hex");
// Constant-time comparison to prevent timing attacks
try {
return timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false;
}
}
/**
* Generate HMAC signature for a response.
*
* @param body - Response body as string or Buffer
* @param matchId - Match ID
* @param turn - Turn number
* @param secret - Your bot's shared secret
* @returns Hex-encoded signature
*/
export function signResponse(
body: string | Buffer,
matchId: string,
turn: number,
secret: string
): string {
const bodyStr = typeof body === "string" ? body : body.toString();
const bodyHash = createHash("sha256").update(bodyStr).digest("hex");
const signingString = `${matchId}.${turn}.${bodyHash}`;
return createHmac("sha256", secret).update(signingString).digest("hex");
}
/**
* Verify that a timestamp is within the allowed window.
* Prevents replay attacks.
*
* @param timestamp - Unix timestamp or ISO 8601 string
* @param windowSeconds - Allowed window (default: 30)
* @returns true if timestamp is valid
*/
export function verifyTimestamp(
timestamp: string,
windowSeconds: number = 30
): boolean {
let ts: Date;
// Try ISO 8601 first
const parsed = new Date(timestamp);
if (!isNaN(parsed.getTime())) {
ts = parsed;
} else {
// Try Unix timestamp (seconds since epoch)
const seconds = parseInt(timestamp, 10);
if (isNaN(seconds)) {
return false;
}
ts = new Date(seconds * 1000);
}
const now = new Date();
const diff = (now.getTime() - ts.getTime()) / 1000;
return diff >= -windowSeconds && diff <= windowSeconds;
}
/**
* Extract auth headers from a Fastify request.
*/
export function getAuthHeaders(headers: Record<string, string>): {
matchId: string;
turn: string;
timestamp: string;
botId: string;
signature: string;
} {
return {
matchId: headers["x-acb-match-id"] || "",
turn: headers["x-acb-turn"] || "0",
timestamp: headers["x-acb-timestamp"] || "",
botId: headers["x-acb-bot-id"] || "",
signature: headers["x-acb-signature"] || "",
};
}

View file

@ -0,0 +1,196 @@
/**
* AI Code Battle - Grid Utilities
*
* Toroidal grid distance calculations, neighbor enumeration,
* and BFS pathfinding.
*/
import type { Position } from "./types.js";
/**
* Toroidal Manhattan distance (shortest path with wrap-around).
*
* @returns Distance in grid cells
*/
export function toroidalManhattan(
r1: number,
c1: number,
r2: number,
c2: number,
cols: number,
rows: number
): number {
const dr = Math.min(Math.abs(r1 - r2), rows - Math.abs(r1 - r2));
const dc = Math.min(Math.abs(c1 - c2), cols - Math.abs(c1 - c2));
return dr + dc;
}
/**
* Toroidal Chebyshev distance (shortest path with wrap-around, 8-directional).
*
* @returns Distance in grid cells
*/
export function toroidalChebyshev(
r1: number,
c1: number,
r2: number,
c2: number,
cols: number,
rows: number
): number {
const dr = Math.min(Math.abs(r1 - r2), rows - Math.abs(r1 - r2));
const dc = Math.min(Math.abs(c1 - c2), cols - Math.abs(c1 - c2));
return Math.max(dr, dc);
}
/**
* Toroidal squared Euclidean distance.
* Faster than sqrt for comparisons.
*/
export function toroidalDistanceSquared(
r1: number,
c1: number,
r2: number,
c2: number,
cols: number,
rows: number
): number {
const dr = Math.min(Math.abs(r1 - r2), rows - Math.abs(r1 - r2));
const dc = Math.min(Math.abs(c1 - c2), cols - Math.abs(c1 - c2));
return dr * dr + dc * dc;
}
/**
* Get all 8 neighboring positions with toroidal wrap.
*
* @returns Array of [row, col] pairs
*/
export function neighbors(
row: number,
col: number,
rows: number,
cols: number
): Position[] {
const offsets = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 1],
[1, -1],
[1, 0],
[1, 1],
];
return offsets.map(([dr, dc]) => ({
row: (row + dr + rows) % rows,
col: (col + dc + cols) % cols,
}));
}
/**
* Get 4 cardinal neighboring positions (N, E, S, W).
*/
export function cardinalNeighbors(
row: number,
col: number,
rows: number,
cols: number
): Array<{ pos: Position; dir: "N" | "E" | "S" | "W" }> {
return [
{ pos: { row: (row - 1 + rows) % rows, col }, dir: "N" },
{ pos: { row, col: (col + 1) % cols }, dir: "E" },
{ pos: { row: (row + 1) % rows, col }, dir: "S" },
{ pos: { row, col: (col - 1 + cols) % cols }, dir: "W" },
];
}
/**
* BFS pathfinding on a toroidal grid.
*
* @param start - Starting position [row, col]
* @param goal - Goal position [row, col]
* @param passable - Function returning true if a tile is walkable
* @returns Path from start to goal (excluding start), or null if unreachable
*/
export function bfs(
start: Position,
goal: Position,
passable: (row: number, col: number) => boolean,
rows: number,
cols: number
): Position[] | null {
if (start.row === goal.row && start.col === goal.col) {
return [];
}
const key = (r: number, c: number) => `${r},${c}`;
const visited = new Set<string>([key(start.row, start.col)]);
interface QueueItem {
pos: Position;
path: Position[];
}
const queue: QueueItem[] = [{ pos: start, path: [] }];
while (queue.length > 0) {
const { pos, path } = queue.shift()!;
for (const next of neighbors(pos.row, pos.col, rows, cols)) {
const newPath = [...path, next];
if (next.row === goal.row && next.col === goal.col) {
return newPath;
}
const k = key(next.row, next.col);
if (!visited.has(k) && passable(next.row, next.col)) {
visited.add(k);
queue.push({ pos: next, path: newPath });
}
}
}
return null;
}
/**
* Find the nearest position from a list of targets.
*
* @returns The nearest target or null if targets is empty
*/
export function findNearest(
from: Position,
targets: Position[],
rows: number,
cols: number
): Position | null {
if (targets.length === 0) return null;
let nearest = targets[0];
let bestDist = toroidalDistanceSquared(
from.row,
from.col,
nearest.row,
nearest.col,
cols,
rows
);
for (let i = 1; i < targets.length; i++) {
const dist = toroidalDistanceSquared(
from.row,
from.col,
targets[i].row,
targets[i].col,
cols,
rows
);
if (dist < bestDist) {
bestDist = dist;
nearest = targets[i];
}
}
return nearest;
}

View file

@ -0,0 +1,155 @@
/**
* AI Code Battle - TypeScript Starter Bot
*
* Fastify HTTP server with HMAC authentication for the AI Code Battle platform.
*
* Environment variables:
* BOT_SECRET - Your bot's shared secret (required)
* BOT_PORT - Port to listen on (default: 8080)
*/
import Fastify, { FastifyRequest, FastifyReply } from "fastify";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { GameState, MoveResponse } from "./types.js";
import {
verifySignature,
signResponse,
verifyTimestamp,
getAuthHeaders,
} from "./auth.js";
import { computeMoves } from "./strategy.js";
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);
}
// Create Fastify instance with a custom content parser to capture raw body
const app = Fastify({
logger: false, // Set to true for HTTP request logging
});
// Add a custom parser to store raw body string for signature verification
app.addContentTypeParser(
"application/json",
{ parseAs: "string" },
async (
request: FastifyRequest,
body: string
) => {
// Store raw body for signature verification
(request as any).rawBody = body;
// Also return parsed JSON for normal use
return JSON.parse(body);
}
);
/**
* Health check endpoint - used during bot registration.
*/
app.get("/health", async (_request: FastifyRequest, reply: FastifyReply) => {
reply.type("text/plain").code(200);
return "OK";
});
/**
* Main game turn endpoint.
* Receives game state JSON, computes moves, returns moves JSON.
*/
app.post("/turn", async (request: FastifyRequest, reply: FastifyReply) => {
// Get raw body as string for signature verification
const rawBody = (request as any).rawBody;
if (typeof rawBody !== "string") {
reply.type("text/plain").code(400);
return "Invalid request body";
}
const bodyBuffer = Buffer.from(rawBody, "utf-8");
// Extract auth headers
const headers = request.headers as Record<string, string>;
const { matchId, turn, timestamp, signature } = getAuthHeaders(headers);
// Verify HMAC signature
if (
!signature ||
!verifySignature(bodyBuffer, matchId, turn, timestamp, signature, SECRET)
) {
reply.type("text/plain").code(401);
return "Invalid signature";
}
// Verify timestamp (prevent replay attacks)
if (!verifyTimestamp(timestamp)) {
reply.type("text/plain").code(401);
return "Invalid timestamp";
}
// Parse game state JSON (already parsed by our custom parser)
const state: GameState = request.body as GameState;
// Log match start (turn 0)
if (state.turn === 0) {
console.log(
`match=${state.match_id} ` +
`season_id=${state.config.season_id || "none"} ` +
`rules_version=${state.config.rules_version || "none"} ` +
`rows=${state.config.rows} cols=${state.config.cols}`
);
}
// Compute moves
const moves = computeMoves(state);
// Build response
const responseBody: MoveResponse = { moves };
const responseJson = JSON.stringify(responseBody);
// Sign response
const responseSig = signResponse(
responseJson,
matchId,
parseInt(turn, 10),
SECRET
);
// Send response with signature header
reply
.code(200)
.header("Content-Type", "application/json")
.header("X-ACB-Signature", responseSig);
return responseJson;
});
/**
* Read package.json for version info
*/
const __dirname = fileURLToPath(new URL(".", import.meta.url));
let version = "unknown";
try {
const pkg = JSON.parse(
await readFile(join(__dirname, "..", "package.json"), "utf-8")
);
version = pkg.version;
} catch {
// Ignore
}
/**
* Start the server
*/
const start = async () => {
try {
await app.listen({ port: PORT, host: "0.0.0.0" });
console.log(`acb-starter-typescript v${version} listening on port ${PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();

View file

@ -0,0 +1,132 @@
/**
* AI Code Battle - Strategy Implementation
*
* This is where you implement your bot's strategy.
* The computeMoves function is called each turn with the current game state.
*/
import type { GameState, Move, Direction } from "./types.js";
import { toroidalManhattan, cardinalNeighbors } from "./grid.js";
/**
* Compute moves for all your bots.
*
* This is a placeholder strategy that moves bots toward nearby energy.
* Replace this with your own strategy!
*
* @param state - Current game state (fog-filtered for your player)
* @returns Array of moves for your bots
*/
export function computeMoves(state: GameState): Move[] {
const moves: Move[] = [];
const { rows, cols } = state.config;
const myBotId = state.you.id;
// Find all my bots
const myBots = state.bots.filter((b) => b.owner === myBotId);
// For each bot, decide where to move
for (const bot of myBots) {
const move = decideBotMove(bot, state);
if (move) {
moves.push(move);
}
}
return moves;
}
/**
* Decide a single bot's move.
*
* @param bot - The bot to move
* @param state - Current game state
* @returns Move command, or undefined to hold position
*/
function decideBotMove(
bot: { row: number; col: number; owner: number },
state: GameState
): Move | undefined {
const { rows, cols } = state.config;
// If there's energy visible, move toward the nearest one
if (state.energy.length > 0) {
let bestDir: Direction | null = null;
let bestDist = Infinity;
for (const { pos, dir } of cardinalNeighbors(bot.row, bot.col, rows, cols)) {
// Skip if there's a wall
if (state.walls.some((w) => w.row === pos.row && w.col === pos.col)) {
continue;
}
// Find distance to nearest energy from this neighbor position
for (const e of state.energy) {
const dist = toroidalManhattan(pos.row, pos.col, e.row, e.col, cols, rows);
if (dist < bestDist) {
bestDist = dist;
bestDir = dir;
}
}
}
if (bestDir) {
return { row: bot.row, col: bot.col, direction: bestDir };
}
}
// No energy nearby - move randomly or hold
// You can replace this with more sophisticated logic
return undefined; // Hold position
}
/**
* Example: Check if a position is safe (no visible enemies nearby).
*/
export function isSafe(
row: number,
col: number,
state: GameState,
radius2: number = 5
): boolean {
const { rows, cols } = state.config;
const myBotId = state.you.id;
for (const enemy of state.bots) {
if (enemy.owner === myBotId) continue;
const dr = Math.min(Math.abs(row - enemy.row), rows - Math.abs(row - enemy.row));
const dc = Math.min(Math.abs(col - enemy.col), cols - Math.abs(col - enemy.col));
const dist2 = dr * dr + dc * dc;
if (dist2 <= radius2) {
return false; // Enemy nearby
}
}
return true;
}
/**
* Example: Find a position to gather energy safely.
*/
export function findSafeGatherTarget(
bot: { row: number; col: number },
state: GameState
): { row: number; col: number } | null {
const { rows, cols } = state.config;
for (const energy of state.energy) {
if (isSafe(energy.row, energy.col, state)) {
return energy;
}
}
// No safe energy found - go to nearest anyway
if (state.energy.length > 0) {
// Simple approach: first energy in list
return state.energy[0];
}
return null;
}

View file

@ -0,0 +1,144 @@
/**
* AI Code Battle - TypeScript Type Definitions
*
* Complete type definitions for the game protocol.
* All types match the JSON schema documented in the protocol spec.
*/
/**
* Cardinal movement directions.
*/
export type Direction = "N" | "E" | "S" | "W";
/** All four cardinal directions. */
export const ALL_DIRECTIONS: Direction[] = ["N", "E", "S", "W"];
/**
* Grid position (row, column).
*/
export interface Position {
row: number;
col: number;
}
/**
* Match configuration parameters.
* These are identical for all players and do not change between turns.
*/
export interface GameConfig {
rows: number;
cols: number;
max_turns: number;
vision_radius2: number;
attack_radius2: number;
spawn_cost: number;
energy_interval: number;
season_id?: string;
rules_version?: number;
special_tiles?: string[];
}
/**
* Information about the current player (you).
*/
export interface PlayerInfo {
id: number;
energy: number;
score: number;
}
/**
* A bot visible within fog of war.
*/
export interface VisibleBot {
row: number;
col: number;
owner: number;
}
/**
* A core (spawn point) visible within fog of war.
*/
export interface VisibleCore {
row: number;
col: number;
owner: number;
active: boolean;
}
/**
* Energy tile position.
*/
export interface EnergyTile {
row: number;
col: number;
}
/**
* Game state sent by the engine each turn.
* Contains only tiles visible within the player's fog of war.
*/
export interface GameState {
match_id: string;
turn: number;
config: GameConfig;
you: PlayerInfo;
bots: VisibleBot[];
energy: EnergyTile[];
cores: VisibleCore[];
walls: Position[];
dead: VisibleBot[];
}
/**
* A movement order for a single bot.
* References the bot's current position and the direction to move.
*/
export interface Move {
row: number;
col: number;
direction: Direction;
}
/**
* Optional debug telemetry for replay visualization.
* Stored in the replay but never parsed by the engine.
* Max 10KB per turn.
*/
export interface DebugTelemetry {
reasoning?: string;
targets?: DebugTarget[];
values?: Record<string, string | number>;
heatmap?: DebugHeatmap;
}
export interface DebugTarget {
row: number;
col: number;
label: string;
priority: number;
}
export interface DebugHeatmap {
name: string;
data: number[][];
}
/**
* Response sent back to the engine.
*/
export interface MoveResponse {
moves: Move[];
debug?: DebugTelemetry;
}
/**
* Authentication headers from incoming requests.
*/
export interface AuthHeaders {
"x-acb-match-id": string;
"x-acb-turn": string;
"x-acb-timestamp": string;
"x-acb-bot-id": string;
"x-acb-signature": string;
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"ts-node": {
"esm": true
}
}