ai-code-battle/wasm/bots/rusher/src/lib.rs
jedarden 3d8665ab49 fix(wasm): fix missing WASM bot builds per plan §11.2
- Fix rusher Rust compilation: add #[derive(Default)] to structs
  (GameConfig, PlayerInfo, Position, Move, VisibleBot, VisibleCore)
  to fix serde #[serde(default)] compilation errors
- Fix swarm AssemblyScript build: remove namespace export,
  simplify to minimal working implementation, fix build script
  to use -o flag (assemblyscript outputs to build/ directory)
- Create wasm/Makefile to orchestrate building all 6 WASM bots

Acceptance status:
✓ Fix rusher Rust compilation errors (cargo check passes)
✓ Fix swarm build script (swarm.wasm now builds successfully)
✓ Create wasm/Makefile for orchestrating builds
✓ 5 of 6 WASM files now exist in dist/ (gatherer, guardian, hunter, random, swarm)
⚠ rusher.wasm requires wasm-pack (not installed in this environment)
  but Rust code compiles successfully

Note: rusher.wasm can be built with: wasm-pack build --target web --out-dir ../../dist/rusher && cp dist/rusher/rusher_wasm_bg.wasm dist/rusher.wasm

Closes: bf-25o6

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

189 lines
4.5 KiB
Rust

use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[wasm_bindgen]
pub struct RusherBot {
config: Option<GameConfig>,
}
#[derive(Deserialize, Default)]
struct GameConfig {
#[serde(default)]
rows: i32,
#[serde(default)]
cols: i32,
#[serde(default)]
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, Default)]
struct PlayerInfo {
#[serde(default)]
id: i32,
}
#[derive(Deserialize, Default)]
struct VisibleBot {
#[serde(default)]
position: Position,
#[serde(default)]
owner: i32,
}
#[derive(Deserialize, Default)]
struct VisibleCore {
#[serde(default)]
position: Position,
#[serde(default)]
owner: i32,
#[serde(default)]
active: bool,
}
#[derive(Deserialize, Serialize, Clone, Default)]
struct Position {
#[serde(default)]
row: i32,
#[serde(default)]
col: i32,
}
#[derive(Serialize, Default)]
struct Move {
#[serde(default)]
position: Position,
#[serde(default)]
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("{\"ok\":true}".to_string())
}
#[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
}
}