Tab/space alignment consistency from running gofmt on all packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
197 lines
4.5 KiB
Go
197 lines
4.5 KiB
Go
//go:build js && wasm
|
|
|
|
// Package main implements a WASM bot for the AI Code Battle sandbox.
|
|
// Compile with: GOOS=js GOARCH=wasm go build -o mybot.wasm .
|
|
//
|
|
// The bot exports an 'acbBot' global object with:
|
|
//
|
|
// init(configJSON: string) - called once at match start
|
|
// compute_moves(stateJSON: string) - called each turn, returns moves JSON
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"syscall/js"
|
|
|
|
"github.com/aicodebattle/acb/engine"
|
|
)
|
|
|
|
// botState holds persistent state across turns (e.g., pathfinding cache).
|
|
type botState struct {
|
|
config engine.Config
|
|
myID int
|
|
knownPos map[string]bool // positions we've seen
|
|
}
|
|
|
|
var state = &botState{
|
|
knownPos: make(map[string]bool),
|
|
}
|
|
|
|
// jsInit is called once at match start with the game config.
|
|
func jsInit(_ js.Value, args []js.Value) interface{} {
|
|
if len(args) < 1 {
|
|
return map[string]interface{}{"ok": false, "error": "configJSON required"}
|
|
}
|
|
|
|
var cfg engine.Config
|
|
if err := json.Unmarshal([]byte(args[0].String()), &cfg); err != nil {
|
|
return map[string]interface{}{"ok": false, "error": err.Error()}
|
|
}
|
|
|
|
state.config = cfg
|
|
return map[string]interface{}{"ok": true}
|
|
}
|
|
|
|
// jsComputeMoves is called each turn with the visible game state.
|
|
func jsComputeMoves(_ js.Value, args []js.Value) interface{} {
|
|
if len(args) < 1 {
|
|
return "[]"
|
|
}
|
|
|
|
var visible engine.VisibleState
|
|
if err := json.Unmarshal([]byte(args[0].String()), &visible); err != nil {
|
|
return "[]"
|
|
}
|
|
|
|
state.myID = visible.You.ID
|
|
moves := computeMoves(&visible)
|
|
|
|
jsonBytes, _ := json.Marshal(moves)
|
|
return string(jsonBytes)
|
|
}
|
|
|
|
// computeMoves contains your bot logic. This is a simple example:
|
|
// move each bot toward the nearest energy, avoiding enemies if close.
|
|
func computeMoves(visible *engine.VisibleState) []engine.Move {
|
|
var moves []engine.Move
|
|
|
|
energySet := make(map[engine.Position]bool)
|
|
for _, e := range visible.Energy {
|
|
energySet[e] = true
|
|
}
|
|
|
|
enemySet := make(map[engine.Position]bool)
|
|
for _, b := range visible.Bots {
|
|
if b.Owner != state.myID {
|
|
enemySet[b.Position] = true
|
|
}
|
|
}
|
|
|
|
for _, bot := range visible.Bots {
|
|
if bot.Owner != state.myID {
|
|
continue
|
|
}
|
|
|
|
dir := fleeFromEnemies(bot.Position, enemySet)
|
|
if dir == engine.DirNone {
|
|
dir = towardNearest(bot.Position, energySet)
|
|
}
|
|
if dir == engine.DirNone {
|
|
dir = randomDir()
|
|
}
|
|
|
|
moves = append(moves, engine.Move{
|
|
Position: bot.Position,
|
|
Direction: dir,
|
|
})
|
|
}
|
|
|
|
return moves
|
|
}
|
|
|
|
func fleeFromEnemies(from engine.Position, enemies map[engine.Position]bool) engine.Direction {
|
|
thr := state.config.AttackRadius2 + 4
|
|
for e := range enemies {
|
|
if dist2(from, e) <= thr {
|
|
return bestFleeDir(from, enemies)
|
|
}
|
|
}
|
|
return engine.DirNone
|
|
}
|
|
|
|
func bestFleeDir(from engine.Position, enemies map[engine.Position]bool) engine.Direction {
|
|
bestDir := engine.DirNone
|
|
bestDist := -1
|
|
|
|
for _, d := range []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW} {
|
|
dr, dc := d.Delta()
|
|
np := engine.Position{
|
|
Row: ((from.Row+dr)%state.config.Rows + state.config.Rows) % state.config.Rows,
|
|
Col: ((from.Col+dc)%state.config.Cols + state.config.Cols) % state.config.Cols,
|
|
}
|
|
|
|
minDist := 1 << 30
|
|
for e := range enemies {
|
|
if d2 := dist2(np, e); d2 < minDist {
|
|
minDist = d2
|
|
}
|
|
}
|
|
|
|
if minDist > bestDist {
|
|
bestDist = minDist
|
|
bestDir = d
|
|
}
|
|
}
|
|
|
|
return bestDir
|
|
}
|
|
|
|
func towardNearest(from engine.Position, targets map[engine.Position]bool) engine.Direction {
|
|
if len(targets) == 0 {
|
|
return engine.DirNone
|
|
}
|
|
|
|
bestDir := engine.DirNone
|
|
bestDist := 1 << 30
|
|
|
|
for _, d := range []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW} {
|
|
dr, dc := d.Delta()
|
|
np := engine.Position{
|
|
Row: ((from.Row+dr)%state.config.Rows + state.config.Rows) % state.config.Rows,
|
|
Col: ((from.Col+dc)%state.config.Cols + state.config.Cols) % state.config.Cols,
|
|
}
|
|
|
|
for t := range targets {
|
|
if d2 := dist2(np, t); d2 < bestDist {
|
|
bestDist = d2
|
|
bestDir = d
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestDir
|
|
}
|
|
|
|
func dist2(a, b engine.Position) int {
|
|
dr := a.Row - b.Row
|
|
if dr < 0 {
|
|
dr = -dr
|
|
}
|
|
if dr > state.config.Rows/2 {
|
|
dr = state.config.Rows - dr
|
|
}
|
|
dc := a.Col - b.Col
|
|
if dc < 0 {
|
|
dc = -dc
|
|
}
|
|
if dc > state.config.Cols/2 {
|
|
dc = state.config.Cols - dc
|
|
}
|
|
return dr*dr + dc*dc
|
|
}
|
|
|
|
func randomDir() engine.Direction {
|
|
dirs := []engine.Direction{engine.DirN, engine.DirE, engine.DirS, engine.DirW}
|
|
return dirs[(state.config.Rows+state.config.Cols)%4]
|
|
}
|
|
|
|
func main() {
|
|
done := make(chan struct{})
|
|
|
|
js.Global().Set("acbBot", js.ValueOf(map[string]interface{}{
|
|
"init": js.FuncOf(jsInit),
|
|
"compute_moves": js.FuncOf(jsComputeMoves),
|
|
}))
|
|
|
|
<-done
|
|
}
|