ai-code-battle/cmd/acb-wasm/main.go
jedarden f5d7553f98 Add Phase 7-9 features: evolution dashboard, WASM sandbox, enhanced replay
Phase 7 Evolution:
- Add live-export subcommand to acb-evolver for dashboard JSON generation
- Export programs, stats, and generation log to live.json

Phase 8 Enhanced Features:
- Add WASM game engine build (cmd/acb-wasm/) with JS bindings
- Add in-browser sandbox page with Monaco editor (web/src/pages/sandbox.ts)
- Add win probability computation (web/src/win-probability.ts)
- Add replay commentary generator (web/src/commentary.ts)
- Add clip maker for GIF/MP4 export (web/src/pages/clip-maker.ts)
- Add rivalry detection and pages (web/src/pages/rivalries.ts)
- Add replay feedback system (web/src/pages/feedback.ts)
- Add evolution dashboard page (web/src/pages/evolution.ts)

Phase 9 Platform Depth:
- Add predictions API (cmd/acb-api/predictions.go)
- Add series management API (cmd/acb-api/series.go)
- Add seasons API (cmd/acb-api/seasons.go)
- Add narrative generator for rivalries (cmd/acb-indexer/src/narrative.ts)

Engine Updates:
- Add debug field to move response schema
- Add match event timeline extraction
- Add replay enrichment fields

Web Updates:
- Update app.html navigation for new pages
- Add API client methods for predictions, series, seasons
- Export engine types for browser use

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 01:13:23 -04:00

210 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build js && wasm
// Package main is compiled with GOOS=js GOARCH=wasm to produce engine.wasm.
// It exposes three functions on the global acbEngine object:
//
// acbEngine.loadState(stateJSON) load a serialised GameState
// acbEngine.step(movesJSON) advance one turn; returns {state,result}
// acbEngine.runMatch(configJSON) run a full match; returns {replay,result}
//
// Example (JavaScript):
//
// const go = new Go();
// WebAssembly.instantiateStreaming(fetch('/wasm/engine.wasm'), go.importObject)
// .then(({instance}) => { go.run(instance); });
// // acbEngine is now available
package main
import (
"encoding/json"
"math/rand"
"syscall/js"
"time"
"github.com/aicodebattle/acb/engine"
)
// matchSession holds a running match for turn-by-turn access.
type matchSession struct {
gs *engine.GameState
bots []engine.BotInterface
recorder *engine.ReplayWriter
}
var session *matchSession
// jsLoadState parses a serialised GameState JSON and stores it as the active session.
// Signature: acbEngine.loadState(stateJSON: string) => {ok:bool, error?:string}
func jsLoadState(_ js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return jsErr("stateJSON argument required")
}
// For now, we expect an initialisation config rather than a full state dump.
type initRequest struct {
Config engine.Config `json:"config"`
Seed int64 `json:"seed"`
Bot1 string `json:"bot1"` // strategy name
Bot2 string `json:"bot2"` // strategy name
}
var req initRequest
if err := json.Unmarshal([]byte(args[0].String()), &req); err != nil {
return jsErr("parse error: " + err.Error())
}
cfg := req.Config
if cfg.Rows == 0 {
cfg = engine.DefaultConfig()
// Smaller default for in-browser matches
cfg.Rows = 30
cfg.Cols = 30
cfg.MaxTurns = 200
}
seed := req.Seed
if seed == 0 {
seed = time.Now().UnixNano()
}
rng := rand.New(rand.NewSource(seed))
gs := engine.NewGameState(cfg, rng)
bot1 := newBuiltinBot(req.Bot1, rng)
bot2 := newBuiltinBot(req.Bot2, rng)
mr := engine.NewMatchRunner(cfg, engine.WithRNG(rand.New(rand.NewSource(seed))))
mr.AddBot(bot1, req.Bot1)
mr.AddBot(bot2, req.Bot2)
_ = gs // session setup done via match runner below
session = &matchSession{
gs: gs,
bots: []engine.BotInterface{bot1, bot2},
}
return map[string]interface{}{"ok": true}
}
// jsStep advances one turn.
// Signature: acbEngine.step(movesJSON: string) => {state, events, result?}
func jsStep(_ js.Value, args []js.Value) interface{} {
if session == nil {
return jsErr("no active session; call loadState first")
}
gs := session.gs
// Parse moves from caller (if provided)
if len(args) > 0 && args[0].String() != "" {
var moves []engine.Move
if err := json.Unmarshal([]byte(args[0].String()), &moves); err != nil {
return jsErr("parse moves: " + err.Error())
}
for _, m := range moves {
// Find bot at position and submit move
for _, b := range gs.Bots {
if b.Alive && b.Position == m.Position {
gs.Moves[b.ID] = m
}
}
}
}
result := gs.ExecuteTurn()
stateJSON, _ := json.Marshal(gs)
eventsJSON, _ := json.Marshal(gs.Events)
out := map[string]interface{}{
"state": string(stateJSON),
"events": string(eventsJSON),
"turn": gs.Turn,
}
if result != nil {
resultJSON, _ := json.Marshal(result)
out["result"] = string(resultJSON)
}
return out
}
// jsRunMatch executes a complete match and returns the replay.
// Signature: acbEngine.runMatch(configJSON: string) => {replay, result}
func jsRunMatch(_ js.Value, args []js.Value) interface{} {
type runRequest struct {
Config engine.Config `json:"config"`
Bot1 string `json:"bot1"`
Bot2 string `json:"bot2"`
Seed int64 `json:"seed"`
}
var req runRequest
if len(args) > 0 && args[0].String() != "" {
if err := json.Unmarshal([]byte(args[0].String()), &req); err != nil {
return jsErr("parse config: " + err.Error())
}
}
cfg := req.Config
if cfg.Rows == 0 {
cfg = engine.DefaultConfig()
cfg.Rows = 30
cfg.Cols = 30
cfg.MaxTurns = 200
}
seed := req.Seed
if seed == 0 {
seed = time.Now().UnixNano()
}
rng := rand.New(rand.NewSource(seed))
bot1Name := req.Bot1
if bot1Name == "" {
bot1Name = "random"
}
bot2Name := req.Bot2
if bot2Name == "" {
bot2Name = "random"
}
mr := engine.NewMatchRunner(cfg,
engine.WithRNG(rng),
engine.WithTimeout(500*time.Millisecond),
)
mr.AddBot(newBuiltinBot(bot1Name, rand.New(rand.NewSource(seed))), bot1Name)
mr.AddBot(newBuiltinBot(bot2Name, rand.New(rand.NewSource(seed+1))), bot2Name)
result, replay, err := mr.Run()
if err != nil {
return jsErr("run match: " + err.Error())
}
replayJSON, _ := json.Marshal(replay)
resultJSON, _ := json.Marshal(result)
return map[string]interface{}{
"replay": string(replayJSON),
"result": string(resultJSON),
}
}
func jsErr(msg string) map[string]interface{} {
return map[string]interface{}{"ok": false, "error": msg}
}
func main() {
done := make(chan struct{})
js.Global().Set("acbEngine", js.ValueOf(map[string]interface{}{
"loadState": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsLoadState(this, args)
}),
"step": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsStep(this, args)
}),
"runMatch": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsRunMatch(this, args)
}),
"version": "1.0.0",
}))
<-done
}