ai-code-battle/wasm/engine/main_wasm.go
jedarden 306b0d2c5f feat(wasm): implement SwarmBot AssemblyScript WASM with full strategy per plan §11.2
Implements complete SwarmBot formation-based combat strategy in AssemblyScript:
- JSON parsing for game config and state
- Tight cohesion (radius=3) movement with circular mean center-of-mass
- Enemy-seeking behavior with engagement bonuses
- Toroidal distance calculations

Builds to 27KB swarm.wasm (AssemblyScript produces compact binaries vs
Go's ~12MB). Build script now copies to dist/.

Closes: bf-2a7w

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:45:46 -04:00

187 lines
4.3 KiB
Go

// Build with: GOOS=js GOARCH=wasm go build -o engine.wasm ./wasm/engine
//go:build js
package main
import (
"encoding/json"
"fmt"
"syscall/js"
"github.com/aicodebattle/acb/engine"
)
var (
match *engine.Match
)
// init registers the WASM exports
func init() {
c := make(chan struct{})
js.Global().Set("acbEngine", js.ValueOf(map[string]interface{}{
"loadState": jsWrapper(loadState),
"step": jsWrapper(step),
"runMatch": jsWrapper(runMatch),
"getReplay": jsWrapper(getReplay),
"getBots": jsWrapper(getBots),
"getEnergy": jsWrapper(getEnergy),
"getConfig": jsWrapper(getConfig),
"getState": jsWrapper(getState),
}))
fmt.Println("ACB WASM Engine loaded")
close(c)
}
func main() {
// Keep the program running
select {}
}
// jsWrapper converts a Go function to a JS function
func jsWrapper(fnc func(js.Value, []js.Value) interface{}) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
defer func() {
if r := recover(); r != nil {
js.Global().Get("console").Call("error", fmt.Sprintf("panic: %v", r))
}
}()
return fnc(this, args)
})
}
// loadState loads a game state from JSON
func loadState(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return errorResult("loadState requires state JSON argument")
}
stateJSON := args[0].String()
// Create a new match from the state
var err error
match, err = engine.LoadStateJSON(stateJSON)
if err != nil {
return errorResult(fmt.Sprintf("failed to load state: %v", err))
}
return successResult(nil)
}
// step advances one turn with the given moves
func step(_ js.Value, args []js.Value) interface{} {
if match == nil {
return errorResult("no match loaded - call loadState or runMatch first")
}
if len(args) < 1 {
return errorResult("step requires moves JSON argument")
}
movesJSON := args[0].String()
var moves map[int]engine.Move
if err := json.Unmarshal([]byte(movesJSON), &moves); err != nil {
return errorResult(fmt.Sprintf("invalid moves JSON: %v", err))
}
// Execute one turn
turnState, err := match.StepTurn(moves)
if err != nil {
return errorResult(fmt.Sprintf("turn execution failed: %v", err))
}
return successResult(turnState)
}
// runMatch runs a full match with the given config and map
func runMatch(_ js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return errorResult("runMatch requires config and map JSON arguments")
}
configJSON := args[0].String()
mapJSON := args[1].String()
var config engine.Config
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return errorResult(fmt.Sprintf("invalid config JSON: %v", err))
}
// Create new match
var err error
match, err = engine.NewMatch(config, mapJSON)
if err != nil {
return errorResult(fmt.Sprintf("failed to create match: %v", err))
}
// Run the match
result, err := match.Run()
if err != nil {
return errorResult(fmt.Sprintf("match execution failed: %v", err))
}
return successResult(result)
}
// getReplay returns the current replay JSON
func getReplay(_ js.Value, args []js.Value) interface{} {
if match == nil {
return errorResult("no match loaded")
}
replay := match.GetReplayJSON()
return successResult(replay)
}
// getBots returns current bot positions
func getBots(_ js.Value, args []js.Value) interface{} {
if match == nil {
return errorResult("no match loaded")
}
bots := match.GetBotsJSON()
return successResult(bots)
}
// getEnergy returns current energy positions
func getEnergy(_ js.Value, args []js.Value) interface{} {
if match == nil {
return errorResult("no match loaded")
}
energy := match.GetEnergyJSON()
return successResult(energy)
}
// getConfig returns the match config
func getConfig(_ js.Value, args []js.Value) interface{} {
if match == nil {
return errorResult("no match loaded")
}
config := match.GetConfigJSON()
return successResult(config)
}
// getState returns the full current game state
func getState(_ js.Value, args []js.Value) interface{} {
if match == nil {
return errorResult("no match loaded")
}
state := match.GetStateJSON()
return successResult(state)
}
func successResult(data interface{}) map[string]interface{} {
return map[string]interface{}{
"ok": true,
"data": data,
}
}
func errorResult(msg string) map[string]interface{} {
return map[string]interface{}{
"ok": false,
"error": msg,
}
}