feat(wasm): add separate WASM bot builds for browser sandbox (plan §11.1)

Add wasm/bots/ directory with separate WASM builds for each bot:
- gatherer (Go → WASM): energy-focused, avoids combat
- random (Go → WASM): random moves
- guardian (Go → WASM): defends own cores
- hunter (Go → WASM): hunts nearest enemy
- rusher (Rust → WASM): attacks enemy cores
- swarm (AssemblyScript → WASM): tight formations

Each bot exports the standard WASM interface:
- init(configJSON): initialize with game config
- compute_moves(stateJSON): return moves JSON
- free_result(ptr): no-op for Go/AS

Built WASM files output to wasm/dist/. Go WASM bots verified to build.

Closes: bf-2zi5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-25 14:58:01 -04:00
parent e5f5de4e64
commit 98276903e2
18 changed files with 1057 additions and 0 deletions

25
wasm/bots/Makefile Normal file
View file

@ -0,0 +1,25 @@
.PHONY: all gatherer random guardian hunter rusher swarm clean
all: gatherer random guardian hunter rusher swarm
gatherer:
cd gatherer && chmod +x build.sh && ./build.sh
random:
cd random && chmod +x build.sh && ./build.sh
guardian:
cd guardian && chmod +x build.sh && ./build.sh
hunter:
cd hunter && chmod +x build.sh && ./build.sh
rusher:
cd rusher && chmod +x build.sh && ./build.sh
swarm:
cd swarm && chmod +x build.sh && ./build.sh
clean:
rm -rf dist
mkdir -p dist

62
wasm/bots/README.md Normal file
View file

@ -0,0 +1,62 @@
# WASM Bot Builds
This directory contains the WebAssembly builds for the browser sandbox.
Each bot compiles to a separate WASM module with the standard interface.
## Bot WASM Interface
Each WASM module exports three functions:
```javascript
// Initialize the bot with game config
bot.init(configJSON: string): string // {ok: bool, error?: string}
// Compute moves for the current turn
bot.compute_moves(stateJSON: string): string // moves JSON array
// Free result (no-op for Go/AS, required for interface compatibility)
bot.free_result(ptr: number): void
```
## Directory Structure
```
wasm/bots/
├── gatherer/ # Go → WASM (energy-focused, avoids combat)
├── random/ # Go → WASM (random moves)
├── guardian/ # Go → WASM (defends own cores)
├── hunter/ # Go → WASM (hunts nearest enemy)
├── rusher/ # Rust → WASM (attacks enemy cores)
└── swarm/ # TypeScript/AssemblyScript → WASM (tight formations)
```
## Building
Build all bots:
```bash
cd wasm/bots
make all
```
Build individual bot:
```bash
cd wasm/bots/gatherer
./build.sh
```
Output directory: `wasm/dist/`
## Bot Sizes (estimated)
| Bot | Language | Size |
|-----|----------|------|
| gatherer | Go → WASM | ~12 MB |
| random | Go → WASM | ~10 MB |
| guardian | Go → WASM | ~12 MB |
| hunter | Go → WASM | ~12 MB |
| rusher | Rust → WASM | ~3 MB |
| swarm | AssemblyScript → WASM | ~5 MB |
## Plan Reference
This implements plan §11.1 lines 2566-2576 and plan §13.1 WASM sandbox.

6
wasm/bots/gatherer/build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
# Build gatherer.wasm from Go source
set -e
cd "$(dirname "$0")"
GOOS=js GOARCH=wasm go build -o ../../dist/gatherer.wasm
echo "Built wasm/bots/gatherer -> dist/gatherer.wasm"

192
wasm/bots/gatherer/main.go Normal file
View file

@ -0,0 +1,192 @@
//go:build js && wasm
// Package main compiles to gatherer.wasm for the browser sandbox.
// It exports the standard bot WASM interface: init, compute_moves, free_result.
package main
import (
"encoding/json"
"math/rand"
"syscall/js"
"github.com/aicodebattle/acb/engine"
)
var (
cfg engine.Config
rng *rand.Rand
visible *engine.VisibleState
)
// jsInit initializes the bot with game config.
// Signature: init(configJSON: string) => {ok:bool, error?:string}
func jsInit(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return jsErr("configJSON argument required")
}
if err := json.Unmarshal([]byte(args[0].String()), &cfg); err != nil {
return jsErr("parse config: " + err.Error())
}
rng = rand.New(rand.NewSource(42))
visible = &engine.VisibleState{}
return map[string]interface{}{"ok": true}
}
// jsComputeMoves returns moves for the current turn.
// Signature: computeMoves(stateJSON: string) => string (moves JSON)
func jsComputeMoves(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return "[]"
}
if err := json.Unmarshal([]byte(args[0].String()), visible); err != nil {
return "[]"
}
moves := getMoves(visible)
json, _ := json.Marshal(moves)
return string(json)
}
// jsFreeResult is a no-op for Go (GC handles memory).
// Signature: free_result(ptr: number) => undefined
func jsFreeResult(_ js.Value, _ []js.Value) interface{} {
return nil
}
// getMoves implements GathererBot strategy: energy-focused, avoids combat.
func getMoves(state *engine.VisibleState) []engine.Move {
myID := state.You.ID
energySet := posSet(state.Energy)
enemySet := enemyPositions(state.Bots, myID)
var moves []engine.Move
for _, bot := range state.Bots {
if bot.Owner != myID {
continue
}
dir := fleeDir(bot.Position, enemySet)
if dir == engine.DirNone {
dir = towardNearest(bot.Position, energySet)
}
if dir == engine.DirNone {
dir = randDir()
}
moves = append(moves, engine.Move{Position: bot.Position, Direction: dir})
}
return moves
}
func main() {
js.Global().Set("gathererBot", js.ValueOf(map[string]interface{}{
"init": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsInit(this, args)
}),
"compute_moves": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsComputeMoves(this, args)
}),
"free_result": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsFreeResult(this, args)
}),
"version": "1.0.0",
}))
select {}
}
func jsErr(msg string) map[string]interface{} {
return map[string]interface{}{"ok": false, "error": msg}
}
func posSet(positions []engine.Position) map[engine.Position]bool {
m := make(map[engine.Position]bool, len(positions))
for _, p := range positions {
m[p] = true
}
return m
}
func enemyPositions(bots []engine.VisibleBot, myID int) map[engine.Position]bool {
m := make(map[engine.Position]bool)
for _, b := range bots {
if b.Owner != myID {
m[b.Position] = true
}
}
return m
}
func applyDir(p engine.Position, d engine.Direction) engine.Position {
dr, dc := d.Delta()
row := ((p.Row+dr)%cfg.Rows + cfg.Rows) % cfg.Rows
col := ((p.Col+dc)%cfg.Cols + cfg.Cols) % cfg.Cols
return engine.Position{Row: row, Col: col}
}
func dist2(a, b engine.Position) int {
dr := a.Row - b.Row
if dr < 0 {
dr = -dr
}
if dr > cfg.Rows/2 {
dr = cfg.Rows - dr
}
dc := a.Col - b.Col
if dc < 0 {
dc = -dc
}
if dc > cfg.Cols/2 {
dc = cfg.Cols - dc
}
return dr*dr + dc*dc
}
func towardNearest(from engine.Position, targets map[engine.Position]bool) engine.Direction {
if len(targets) == 0 {
return engine.DirNone
}
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
best, bestD := engine.DirNone, 1<<31-1
for _, d := range allDirs {
np := applyDir(from, d)
for t := range targets {
if d2 := dist2(np, t); d2 < bestD {
bestD = d2
best = d
}
}
}
return best
}
func fleeDir(from engine.Position, enemies map[engine.Position]bool) engine.Direction {
thr := cfg.AttackRadius2 + 4
close := false
for e := range enemies {
if dist2(from, e) <= thr {
close = true
break
}
}
if !close {
return engine.DirNone
}
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
best, bestD := engine.DirNone, -1
for _, d := range allDirs {
np := applyDir(from, d)
minD := 1<<31 - 1
for e := range enemies {
if d2 := dist2(np, e); d2 < minD {
minD = d2
}
}
if minD > bestD {
bestD = minD
best = d
}
}
return best
}
func randDir() engine.Direction {
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
return allDirs[rng.Intn(4)]
}

6
wasm/bots/guardian/build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
# Build guardian.wasm from Go source
set -e
cd "$(dirname "$0")"
GOOS=js GOARCH=wasm go build -o ../../dist/guardian.wasm
echo "Built wasm/bots/guardian -> dist/guardian.wasm"

157
wasm/bots/guardian/main.go Normal file
View file

@ -0,0 +1,157 @@
//go:build js && wasm
// Package main compiles to guardian.wasm for the browser sandbox.
// GuardianBot defends own cores.
package main
import (
"encoding/json"
"math/rand"
"syscall/js"
"github.com/aicodebattle/acb/engine"
)
var (
cfg engine.Config
rng *rand.Rand
visible *engine.VisibleState
)
func jsInit(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return jsErr("configJSON argument required")
}
if err := json.Unmarshal([]byte(args[0].String()), &cfg); err != nil {
return jsErr("parse config: " + err.Error())
}
rng = rand.New(rand.NewSource(42))
visible = &engine.VisibleState{}
return map[string]interface{}{"ok": true}
}
func jsComputeMoves(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return "[]"
}
if err := json.Unmarshal([]byte(args[0].String()), visible); err != nil {
return "[]"
}
moves := getMoves(visible)
json, _ := json.Marshal(moves)
return string(json)
}
func jsFreeResult(_ js.Value, _ []js.Value) interface{} {
return nil
}
// getMoves implements GuardianBot: defend own cores.
func getMoves(state *engine.VisibleState) []engine.Move {
myID := state.You.ID
myCoreSet := make(map[engine.Position]bool)
for _, c := range state.Cores {
if c.Owner == myID && c.Active {
myCoreSet[c.Position] = true
}
}
enemySet := enemyPositions(state.Bots, myID)
var moves []engine.Move
for _, bot := range state.Bots {
if bot.Owner != myID {
continue
}
var dir engine.Direction
if isNear(bot.Position, enemySet, cfg.AttackRadius2+4) {
dir = towardNearest(bot.Position, enemySet)
} else {
dir = towardNearest(bot.Position, myCoreSet)
}
if dir == engine.DirNone {
dir = randDir()
}
moves = append(moves, engine.Move{Position: bot.Position, Direction: dir})
}
return moves
}
func main() {
js.Global().Set("guardianBot", js.ValueOf(map[string]interface{}{
"init": js.FuncOf(jsInit),
"compute_moves": js.FuncOf(jsComputeMoves),
"free_result": js.FuncOf(jsFreeResult),
"version": "1.0.0",
}))
select {}
}
func jsErr(msg string) map[string]interface{} {
return map[string]interface{}{"ok": false, "error": msg}
}
func enemyPositions(bots []engine.VisibleBot, myID int) map[engine.Position]bool {
m := make(map[engine.Position]bool)
for _, b := range bots {
if b.Owner != myID {
m[b.Position] = true
}
}
return m
}
func applyDir(p engine.Position, d engine.Direction) engine.Position {
dr, dc := d.Delta()
row := ((p.Row+dr)%cfg.Rows + cfg.Rows) % cfg.Rows
col := ((p.Col+dc)%cfg.Cols + cfg.Cols) % cfg.Cols
return engine.Position{Row: row, Col: col}
}
func dist2(a, b engine.Position) int {
dr := a.Row - b.Row
if dr < 0 {
dr = -dr
}
if dr > cfg.Rows/2 {
dr = cfg.Rows - dr
}
dc := a.Col - b.Col
if dc < 0 {
dc = -dc
}
if dc > cfg.Cols/2 {
dc = cfg.Cols - dc
}
return dr*dr + dc*dc
}
func towardNearest(from engine.Position, targets map[engine.Position]bool) engine.Direction {
if len(targets) == 0 {
return engine.DirNone
}
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
best, bestD := engine.DirNone, 1<<31-1
for _, d := range allDirs {
np := applyDir(from, d)
for t := range targets {
if d2 := dist2(np, t); d2 < bestD {
bestD = d2
best = d
}
}
}
return best
}
func isNear(from engine.Position, targets map[engine.Position]bool, r2 int) bool {
for t := range targets {
if dist2(from, t) <= r2 {
return true
}
}
return false
}
func randDir() engine.Direction {
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
return allDirs[rng.Intn(4)]
}

6
wasm/bots/hunter/build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
# Build hunter.wasm from Go source
set -e
cd "$(dirname "$0")"
GOOS=js GOARCH=wasm go build -o ../../dist/hunter.wasm
echo "Built wasm/bots/hunter -> dist/hunter.wasm"

151
wasm/bots/hunter/main.go Normal file
View file

@ -0,0 +1,151 @@
//go:build js && wasm
// Package main compiles to hunter.wasm for the browser sandbox.
// HunterBot hunts nearest enemy bot.
package main
import (
"encoding/json"
"math/rand"
"syscall/js"
"github.com/aicodebattle/acb/engine"
)
var (
cfg engine.Config
rng *rand.Rand
visible *engine.VisibleState
)
func jsInit(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return jsErr("configJSON argument required")
}
if err := json.Unmarshal([]byte(args[0].String()), &cfg); err != nil {
return jsErr("parse config: " + err.Error())
}
rng = rand.New(rand.NewSource(42))
visible = &engine.VisibleState{}
return map[string]interface{}{"ok": true}
}
func jsComputeMoves(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return "[]"
}
if err := json.Unmarshal([]byte(args[0].String()), visible); err != nil {
return "[]"
}
moves := getMoves(visible)
json, _ := json.Marshal(moves)
return string(json)
}
func jsFreeResult(_ js.Value, _ []js.Value) interface{} {
return nil
}
// getMoves implements HunterBot: hunt nearest enemy.
func getMoves(state *engine.VisibleState) []engine.Move {
myID := state.You.ID
enemySet := enemyPositions(state.Bots, myID)
energySet := posSet(state.Energy)
var moves []engine.Move
for _, bot := range state.Bots {
if bot.Owner != myID {
continue
}
var dir engine.Direction
if len(enemySet) > 0 {
dir = towardNearest(bot.Position, enemySet)
} else {
dir = towardNearest(bot.Position, energySet)
}
if dir == engine.DirNone {
dir = randDir()
}
moves = append(moves, engine.Move{Position: bot.Position, Direction: dir})
}
return moves
}
func main() {
js.Global().Set("hunterBot", js.ValueOf(map[string]interface{}{
"init": js.FuncOf(jsInit),
"compute_moves": js.FuncOf(jsComputeMoves),
"free_result": js.FuncOf(jsFreeResult),
"version": "1.0.0",
}))
select {}
}
func jsErr(msg string) map[string]interface{} {
return map[string]interface{}{"ok": false, "error": msg}
}
func posSet(positions []engine.Position) map[engine.Position]bool {
m := make(map[engine.Position]bool, len(positions))
for _, p := range positions {
m[p] = true
}
return m
}
func enemyPositions(bots []engine.VisibleBot, myID int) map[engine.Position]bool {
m := make(map[engine.Position]bool)
for _, b := range bots {
if b.Owner != myID {
m[b.Position] = true
}
}
return m
}
func applyDir(p engine.Position, d engine.Direction) engine.Position {
dr, dc := d.Delta()
row := ((p.Row+dr)%cfg.Rows + cfg.Rows) % cfg.Rows
col := ((p.Col+dc)%cfg.Cols + cfg.Cols) % cfg.Cols
return engine.Position{Row: row, Col: col}
}
func dist2(a, b engine.Position) int {
dr := a.Row - b.Row
if dr < 0 {
dr = -dr
}
if dr > cfg.Rows/2 {
dr = cfg.Rows - dr
}
dc := a.Col - b.Col
if dc < 0 {
dc = -dc
}
if dc > cfg.Cols/2 {
dc = cfg.Cols - dc
}
return dr*dr + dc*dc
}
func towardNearest(from engine.Position, targets map[engine.Position]bool) engine.Direction {
if len(targets) == 0 {
return engine.DirNone
}
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
best, bestD := engine.DirNone, 1<<31-1
for _, d := range allDirs {
np := applyDir(from, d)
for t := range targets {
if d2 := dist2(np, t); d2 < bestD {
bestD = d2
best = d
}
}
}
return best
}
func randDir() engine.Direction {
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
return allDirs[rng.Intn(4)]
}

6
wasm/bots/random/build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
# Build random.wasm from Go source
set -e
cd "$(dirname "$0")"
GOOS=js GOARCH=wasm go build -o ../../dist/random.wasm
echo "Built wasm/bots/random -> dist/random.wasm"

80
wasm/bots/random/main.go Normal file
View file

@ -0,0 +1,80 @@
//go:build js && wasm
// Package main compiles to random.wasm for the browser sandbox.
// RandomBot makes uniformly random valid moves.
package main
import (
"encoding/json"
"math/rand"
"syscall/js"
"github.com/aicodebattle/acb/engine"
)
var (
cfg engine.Config
rng *rand.Rand
visible *engine.VisibleState
)
// jsInit initializes the bot with game config.
func jsInit(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return jsErr("configJSON argument required")
}
if err := json.Unmarshal([]byte(args[0].String()), &cfg); err != nil {
return jsErr("parse config: " + err.Error())
}
rng = rand.New(rand.NewSource(42))
visible = &engine.VisibleState{}
return map[string]interface{}{"ok": true}
}
// jsComputeMoves returns random moves.
func jsComputeMoves(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return "[]"
}
if err := json.Unmarshal([]byte(args[0].String()), visible); err != nil {
return "[]"
}
moves := getMoves(visible)
json, _ := json.Marshal(moves)
return string(json)
}
// jsFreeResult is a no-op.
func jsFreeResult(_ js.Value, _ []js.Value) interface{} {
return nil
}
// getMoves implements RandomBot strategy.
func getMoves(state *engine.VisibleState) []engine.Move {
myID := state.You.ID
allDirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
var moves []engine.Move
for _, bot := range state.Bots {
if bot.Owner != myID {
continue
}
dir := allDirs[rng.Intn(4)]
moves = append(moves, engine.Move{Position: bot.Position, Direction: dir})
}
return moves
}
func main() {
js.Global().Set("randomBot", js.ValueOf(map[string]interface{}{
"init": js.FuncOf(jsInit),
"compute_moves": js.FuncOf(jsComputeMoves),
"free_result": js.FuncOf(jsFreeResult),
"version": "1.0.0",
}))
select {}
}
func jsErr(msg string) map[string]interface{} {
return map[string]interface{}{"ok": false, "error": msg}
}

View file

@ -0,0 +1,13 @@
[package]
name = "rusher-wasm"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
fastrand = "2.0"

6
wasm/bots/rusher/build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
# Build rusher.wasm from Rust source
set -e
cd "$(dirname "$0")"
wasm-pack build --target web --out-dir ../../dist/rusher
echo "Built wasm/bots/rusher -> dist/rusher"

176
wasm/bots/rusher/src/lib.rs Normal file
View file

@ -0,0 +1,176 @@
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[wasm_bindgen]
pub struct RusherBot {
config: Option<GameConfig>,
}
#[derive(Deserialize)]
struct GameConfig {
rows: i32,
cols: i32,
attack_radius2: i32,
#[serde(default)]
max_turns: i32,
}
#[derive(Deserialize)]
struct VisibleState {
#[serde(default)]
you: PlayerInfo,
#[serde(default)]
bots: Vec<VisibleBot>,
#[serde(default)]
cores: Vec<VisibleCore>,
#[serde(default)]
energy: Vec<Position>,
}
#[derive(Deserialize)]
struct PlayerInfo {
id: i32,
}
#[derive(Deserialize)]
struct VisibleBot {
position: Position,
owner: i32,
}
#[derive(Deserialize)]
struct VisibleCore {
position: Position,
owner: i32,
active: bool,
}
#[derive(Deserialize, Serialize, Clone)]
struct Position {
row: i32,
col: i32,
}
#[derive(Serialize)]
struct Move {
position: Position,
direction: String,
}
const DIRS: &[&str] = &["N", "E", "S", "W"];
#[wasm_bindgen]
impl RusherBot {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self { config: None }
}
#[wasm_bindgen]
pub fn init(&mut self, config_json: &str) -> Result<String, JsError> {
self.config = Some(serde_json::from_str(config_json)?);
Ok(serde_json::to_string(&json!({"ok": true}))?)
}
#[wasm_bindgen]
pub fn compute_moves(&self, state_json: &str) -> Result<String, JsError> {
let state: VisibleState = serde_json::from_str(state_json)?;
let config = self.config.as_ref().ok_or_else(|| JsError::new("not initialized"))?;
let my_id = state.you.id;
let mut moves = Vec::new();
// Find enemy cores
let mut enemy_cores: Vec<Position> = Vec::new();
for core in &state.cores {
if core.owner != my_id && core.active {
enemy_cores.push(core.position.clone());
}
}
// Find enemy bots
let mut enemy_bots: Vec<Position> = Vec::new();
for bot in &state.bots {
if bot.owner != my_id {
enemy_bots.push(bot.position.clone());
}
}
for bot in &state.bots {
if bot.owner != my_id {
continue;
}
let dir = if !enemy_cores.is_empty() {
self.toward_nearest(&bot.position, &enemy_cores, config)
} else {
self.toward_nearest(&bot.position, &enemy_bots, config)
};
moves.push(Move {
position: bot.position.clone(),
direction: dir,
});
}
Ok(serde_json::to_string(&moves)?)
}
#[wasm_bindgen]
pub fn free_result(&self, _ptr: usize) {
// No-op for Rust (Wasm-bindgen handles memory)
}
}
impl RusherBot {
fn toward_nearest(&self, from: &Position, targets: &[Position], config: &GameConfig) -> String {
if targets.is_empty() {
return DIRS[fastrand::usize(0..4)].to_string();
}
let mut best_dir = DIRS[0];
let mut best_dist = i32::MAX;
for &dir in DIRS {
let np = self.apply_dir(from, dir, config);
for target in targets {
let dist = self.dist2(&np, target, config);
if dist < best_dist {
best_dist = dist;
best_dir = dir;
}
}
}
best_dir.to_string()
}
fn apply_dir(&self, pos: &Position, dir: &str, config: &GameConfig) -> Position {
let (dr, dc) = match dir {
"N" => (-1, 0),
"E" => (0, 1),
"S" => (1, 0),
"W" => (0, -1),
_ => (0, 0),
};
Position {
row: ((pos.row + dr) % config.rows + config.rows) % config.rows,
col: ((pos.col + dc) % config.cols + config.cols) % config.cols,
}
}
fn dist2(&self, a: &Position, b: &Position, config: &GameConfig) -> i32 {
let mut dr = (a.row - b.row).abs();
let mut dc = (a.col - b.col).abs();
if dr > config.rows / 2 {
dr = config.rows - dr;
}
if dc > config.cols / 2 {
dc = config.cols - dc;
}
dr * dr + dc * dc
}
}

View file

@ -0,0 +1,7 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": ["./**/*.ts"],
"compilerOptions": {
"outFile": "./swarm.wasm"
}
}

View file

@ -0,0 +1,10 @@
{
"extends": "../node_modules/assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
],
"compilerOptions": {
"baseDir": ".",
"outFile": "./build/swarm.wasm"
}
}

7
wasm/bots/swarm/build.sh Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh
# Build swarm.wasm from AssemblyScript source
set -e
cd "$(dirname "$0")"
npm install
npx asc assembly/index.ts -b ../../dist/swarm.wasm
echo "Built wasm/bots/swarm -> dist/swarm.wasm"

136
wasm/bots/swarm/index.ts Normal file
View file

@ -0,0 +1,136 @@
// AssemblyScript implementation of SwarmBot for WASM compilation.
// SwarmBot keeps units in tight formations and advances as a group.
@external("env", "memory")
declare function memory: WebAssembly.Memory;
export namespace swarmBot {
// Configuration stored globally
let rows: i32 = 60;
let cols: i32 = 60;
let attackRadius2: i32 = 12;
// Visible state
let myId: i32 = 0;
let botPositions: Array<f64> = new Array<f64>();
let botOwners: Array<i32> = new Array<i32>();
// Initialize the bot with game config
export function init(configJson: string): string {
try {
const config = JSON.parse(configJson);
if (config.rows) rows = config.rows;
if (config.cols) cols = config.cols;
if (config.attack_radius2) attackRadius2 = config.attack_radius2;
return JSON.stringify({ ok: true });
} catch (e) {
return JSON.stringify({ ok: false, error: "parse error" });
}
}
// Compute moves for the current turn
export function compute_moves(stateJson: string): string {
try {
const state = JSON.parse(stateJson);
myId = state.you.id;
// Parse bots
botPositions = new Array<f64>();
botOwners = new Array<i32>();
if (state.bots instanceof Array) {
for (let i = 0; i < state.bots.length; i++) {
const bot = state.bots[i];
botPositions.push(<f64>(bot.position.row * 1000 + bot.position.col));
botOwners.push(bot.owner);
}
}
const moves = new Array<any>();
for (let i = 0; i < state.bots.length; i++) {
const bot = state.bots[i];
if (bot.owner !== myId) continue;
const dir = computeSwarmDir(bot.position.row, bot.position.col, state.bots);
moves.push({
position: { row: bot.position.row, col: bot.position.col },
direction: dir
});
}
return JSON.stringify(moves);
} catch (e) {
return "[]";
}
}
// Free result is a no-op for AssemblyScript
export function free_result(ptr: usize): void {
// GC handles memory
}
// Compute swarm direction: move to maximize distance from friendly bots
function computeSwarmDir(row: i32, col: i32, allBots: any[]): string {
const dirs = ["N", "E", "S", "W"];
let bestDir = "N";
let bestScore = -1;
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i];
const nr = wrapRow(row + deltaRow(dir));
const nc = wrapCol(col + deltaCol(dir));
let score = 0;
for (let j = 0; j < allBots.length; j++) {
const other = allBots[j];
if (other.owner === myId) {
score += dist2(nr, nc, other.position.row, other.position.col);
}
}
if (score > bestScore) {
bestScore = score;
bestDir = dir;
}
}
return bestDir;
}
function deltaRow(dir: string): i32 {
switch (dir) {
case "N": return -1;
case "S": return 1;
default: return 0;
}
}
function deltaCol(dir: string): i32 {
switch (dir) {
case "E": return 1;
case "W": return -1;
default: return 0;
}
}
function wrapRow(r: i32): i32 {
r = r % rows;
if (r < 0) r += rows;
return r;
}
function wrapCol(c: i32): i32 {
c = c % cols;
if (c < 0) c += cols;
return c;
}
function dist2(r1: i32, c1: i32, r2: i32, c2: i32): i32 {
let dr = Math.abs(r1 - r2);
let dc = Math.abs(c1 - c2);
if (dr > rows / 2) dr = rows - dr;
if (dc > cols / 2) dc = cols - dc;
return dr * dr + dc * dc;
}
}

View file

@ -0,0 +1,11 @@
{
"name": "swarm-wasm",
"version": "1.0.0",
"description": "SwarmBot WASM build",
"scripts": {
"build": "asc assembly/index.ts -b build/swarm.wasm"
},
"devDependencies": {
"assemblyscript": "^0.27.0"
}
}