diff --git a/wasm/bots/Makefile b/wasm/bots/Makefile new file mode 100644 index 0000000..5e9adfe --- /dev/null +++ b/wasm/bots/Makefile @@ -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 diff --git a/wasm/bots/README.md b/wasm/bots/README.md new file mode 100644 index 0000000..96aa348 --- /dev/null +++ b/wasm/bots/README.md @@ -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. diff --git a/wasm/bots/gatherer/build.sh b/wasm/bots/gatherer/build.sh new file mode 100755 index 0000000..ab3c799 --- /dev/null +++ b/wasm/bots/gatherer/build.sh @@ -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" diff --git a/wasm/bots/gatherer/main.go b/wasm/bots/gatherer/main.go new file mode 100644 index 0000000..773f8de --- /dev/null +++ b/wasm/bots/gatherer/main.go @@ -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)] +} diff --git a/wasm/bots/guardian/build.sh b/wasm/bots/guardian/build.sh new file mode 100755 index 0000000..62ade6a --- /dev/null +++ b/wasm/bots/guardian/build.sh @@ -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" diff --git a/wasm/bots/guardian/main.go b/wasm/bots/guardian/main.go new file mode 100644 index 0000000..f8ce50a --- /dev/null +++ b/wasm/bots/guardian/main.go @@ -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)] +} diff --git a/wasm/bots/hunter/build.sh b/wasm/bots/hunter/build.sh new file mode 100755 index 0000000..31cfe1d --- /dev/null +++ b/wasm/bots/hunter/build.sh @@ -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" diff --git a/wasm/bots/hunter/main.go b/wasm/bots/hunter/main.go new file mode 100644 index 0000000..0726a75 --- /dev/null +++ b/wasm/bots/hunter/main.go @@ -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)] +} diff --git a/wasm/bots/random/build.sh b/wasm/bots/random/build.sh new file mode 100755 index 0000000..03dfd3c --- /dev/null +++ b/wasm/bots/random/build.sh @@ -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" diff --git a/wasm/bots/random/main.go b/wasm/bots/random/main.go new file mode 100644 index 0000000..80bb63a --- /dev/null +++ b/wasm/bots/random/main.go @@ -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} +} diff --git a/wasm/bots/rusher/Cargo.toml b/wasm/bots/rusher/Cargo.toml new file mode 100644 index 0000000..97458d9 --- /dev/null +++ b/wasm/bots/rusher/Cargo.toml @@ -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" diff --git a/wasm/bots/rusher/build.sh b/wasm/bots/rusher/build.sh new file mode 100755 index 0000000..1c757bf --- /dev/null +++ b/wasm/bots/rusher/build.sh @@ -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" diff --git a/wasm/bots/rusher/src/lib.rs b/wasm/bots/rusher/src/lib.rs new file mode 100644 index 0000000..512243b --- /dev/null +++ b/wasm/bots/rusher/src/lib.rs @@ -0,0 +1,176 @@ +use wasm_bindgen::prelude::*; +use serde::{Deserialize, Serialize}; + +#[wasm_bindgen] +pub struct RusherBot { + config: Option, +} + +#[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, + #[serde(default)] + cores: Vec, + #[serde(default)] + energy: Vec, +} + +#[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 { + 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 { + 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 = 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 = 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 + } +} diff --git a/wasm/bots/swarm/asconfig.json b/wasm/bots/swarm/asconfig.json new file mode 100644 index 0000000..242cfa2 --- /dev/null +++ b/wasm/bots/swarm/asconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": ["./**/*.ts"], + "compilerOptions": { + "outFile": "./swarm.wasm" + } +} diff --git a/wasm/bots/swarm/assembly.json b/wasm/bots/swarm/assembly.json new file mode 100644 index 0000000..139b68f --- /dev/null +++ b/wasm/bots/swarm/assembly.json @@ -0,0 +1,10 @@ +{ + "extends": "../node_modules/assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ], + "compilerOptions": { + "baseDir": ".", + "outFile": "./build/swarm.wasm" + } +} diff --git a/wasm/bots/swarm/build.sh b/wasm/bots/swarm/build.sh new file mode 100755 index 0000000..7315e12 --- /dev/null +++ b/wasm/bots/swarm/build.sh @@ -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" diff --git a/wasm/bots/swarm/index.ts b/wasm/bots/swarm/index.ts new file mode 100644 index 0000000..9e1678e --- /dev/null +++ b/wasm/bots/swarm/index.ts @@ -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 = new Array(); + let botOwners: Array = new Array(); + + // 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(); + botOwners = new Array(); + if (state.bots instanceof Array) { + for (let i = 0; i < state.bots.length; i++) { + const bot = state.bots[i]; + botPositions.push((bot.position.row * 1000 + bot.position.col)); + botOwners.push(bot.owner); + } + } + + const moves = new Array(); + 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; + } +} diff --git a/wasm/bots/swarm/package.json b/wasm/bots/swarm/package.json new file mode 100644 index 0000000..f6dd4b3 --- /dev/null +++ b/wasm/bots/swarm/package.json @@ -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" + } +}