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:
parent
e5f5de4e64
commit
98276903e2
18 changed files with 1057 additions and 0 deletions
25
wasm/bots/Makefile
Normal file
25
wasm/bots/Makefile
Normal 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
62
wasm/bots/README.md
Normal 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
6
wasm/bots/gatherer/build.sh
Executable 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
192
wasm/bots/gatherer/main.go
Normal 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
6
wasm/bots/guardian/build.sh
Executable 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
157
wasm/bots/guardian/main.go
Normal 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
6
wasm/bots/hunter/build.sh
Executable 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
151
wasm/bots/hunter/main.go
Normal 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
6
wasm/bots/random/build.sh
Executable 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
80
wasm/bots/random/main.go
Normal 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}
|
||||
}
|
||||
13
wasm/bots/rusher/Cargo.toml
Normal file
13
wasm/bots/rusher/Cargo.toml
Normal 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
6
wasm/bots/rusher/build.sh
Executable 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
176
wasm/bots/rusher/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
||||
7
wasm/bots/swarm/asconfig.json
Normal file
7
wasm/bots/swarm/asconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": ["./**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outFile": "./swarm.wasm"
|
||||
}
|
||||
}
|
||||
10
wasm/bots/swarm/assembly.json
Normal file
10
wasm/bots/swarm/assembly.json
Normal 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
7
wasm/bots/swarm/build.sh
Executable 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
136
wasm/bots/swarm/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
11
wasm/bots/swarm/package.json
Normal file
11
wasm/bots/swarm/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue