feat(wasm): implement RusherBot WASM with low-level interface per plan §11.2
- Rewrote wasm/bots/rusher/src/lib.rs with complete Rusher strategy: - BFS pathfinding to nearest enemy cores - Wall and enemy avoidance - Known enemy core tracking across turns - Minimal JSON parser for state/config - Custom bump allocator using __heap_base - Updated Cargo.toml for no_std build with alloc crate - Updated build.sh to use cargo directly (sandbox expects low-level exports) - Output: 14KB WASM (much smaller than Go's 5MB due to no runtime) The sandbox loader expects pointer-based WASM interface (allocate, init, compute_moves, free_result) not wasm-bindgen's JavaScript bindings. This implementation uses raw WASM exports compatible with createPointerBasedBridge. Closes: bf-2d50
This commit is contained in:
parent
6715c4b04b
commit
8d15333f2b
3 changed files with 810 additions and 153 deletions
|
|
@ -4,10 +4,12 @@ version = "1.0.0"
|
|||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
fastrand = "2.0"
|
||||
buddy-alloc = "0.5"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#!/bin/sh
|
||||
# Build rusher.wasm from Rust source
|
||||
# Uses low-level WASM interface compatible with the sandbox loader
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
wasm-pack build --target web --out-dir ../../dist/rusher
|
||||
echo "Built wasm/bots/rusher -> dist/rusher"
|
||||
cargo build --target wasm32-unknown-unknown --release
|
||||
cp target/wasm32-unknown-unknown/release/rusher_wasm.wasm ../../dist/rusher.wasm
|
||||
echo "Built wasm/bots/rusher -> dist/rusher.wasm"
|
||||
|
|
|
|||
|
|
@ -1,189 +1,842 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
// RusherBot WASM implementation - aggressive core-rushing strategy.
|
||||
// Uses low-level WASM interface compatible with the sandbox loader.
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct RusherBot {
|
||||
config: Option<GameConfig>,
|
||||
#![no_std]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use alloc::string::String;
|
||||
use alloc::string::ToString;
|
||||
use alloc::boxed::Box;
|
||||
|
||||
// Import memory from the runtime (provided by sandbox)
|
||||
extern "C" {
|
||||
// These are provided by the WebAssembly environment
|
||||
static mut __heap_base: usize;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct GameConfig {
|
||||
#[serde(default)]
|
||||
rows: i32,
|
||||
#[serde(default)]
|
||||
cols: i32,
|
||||
#[serde(default)]
|
||||
attack_radius2: i32,
|
||||
#[serde(default)]
|
||||
max_turns: i32,
|
||||
// Simple bump allocator
|
||||
const HEAP_SIZE: usize = 1024 * 1024; // 1MB heap
|
||||
static mut HEAP_PTR: usize = 0;
|
||||
static mut HEAP_END: usize = 0;
|
||||
|
||||
fn heap_init() {
|
||||
unsafe {
|
||||
if HEAP_PTR == 0 {
|
||||
HEAP_PTR = align_up(&mut __heap_base as *mut _ as usize, 8);
|
||||
HEAP_END = HEAP_PTR + HEAP_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VisibleState {
|
||||
#[serde(default)]
|
||||
you: PlayerInfo,
|
||||
#[serde(default)]
|
||||
bots: Vec<VisibleBot>,
|
||||
#[serde(default)]
|
||||
cores: Vec<VisibleCore>,
|
||||
#[serde(default)]
|
||||
energy: Vec<Position>,
|
||||
fn align_up(addr: usize, align: usize) -> usize {
|
||||
(addr + align - 1) & !(align - 1)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct PlayerInfo {
|
||||
#[serde(default)]
|
||||
id: i32,
|
||||
// Simple allocator for alloc crate
|
||||
struct BumpAllocator;
|
||||
|
||||
unsafe impl core::alloc::GlobalAlloc for BumpAllocator {
|
||||
unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
|
||||
heap_init();
|
||||
let size = layout.size();
|
||||
let align = layout.align();
|
||||
|
||||
let mut ptr = align_up(HEAP_PTR, align);
|
||||
|
||||
if ptr + size > HEAP_END {
|
||||
return core::ptr::null_mut();
|
||||
}
|
||||
|
||||
HEAP_PTR = ptr + size;
|
||||
ptr as *mut u8
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {
|
||||
// Bump allocator doesn't free (memory is reclaimed after each compute_moves call)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct VisibleBot {
|
||||
#[serde(default)]
|
||||
position: Position,
|
||||
#[serde(default)]
|
||||
owner: i32,
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: BumpAllocator = BumpAllocator;
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct VisibleCore {
|
||||
#[serde(default)]
|
||||
position: Position,
|
||||
#[serde(default)]
|
||||
owner: i32,
|
||||
#[serde(default)]
|
||||
active: bool,
|
||||
// Reset the allocator between calls
|
||||
fn reset_allocator() {
|
||||
unsafe {
|
||||
heap_init();
|
||||
HEAP_PTR = align_up(&mut __heap_base as *mut _ as usize, 8);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
// Game structures
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
||||
struct Position {
|
||||
#[serde(default)]
|
||||
row: i32,
|
||||
#[serde(default)]
|
||||
col: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
struct Move {
|
||||
#[serde(default)]
|
||||
position: Position,
|
||||
#[serde(default)]
|
||||
direction: String,
|
||||
#[derive(Clone, Default)]
|
||||
struct Config {
|
||||
rows: i32,
|
||||
cols: i32,
|
||||
attack_radius2: i32,
|
||||
max_turns: i32,
|
||||
}
|
||||
|
||||
const DIRS: &[&str] = &["N", "E", "S", "W"];
|
||||
#[derive(Clone, Default)]
|
||||
struct PlayerInfo {
|
||||
id: i32,
|
||||
energy: i32,
|
||||
score: i32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RusherBot {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self { config: None }
|
||||
#[derive(Clone, Default)]
|
||||
struct VisibleBot {
|
||||
position: Position,
|
||||
owner: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct VisibleCore {
|
||||
position: Position,
|
||||
owner: i32,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct VisibleState {
|
||||
you: PlayerInfo,
|
||||
bots: Vec<VisibleBot>,
|
||||
cores: Vec<VisibleCore>,
|
||||
energy: Vec<Position>,
|
||||
walls: Vec<Position>,
|
||||
dead: Vec<VisibleBot>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Move {
|
||||
position: Position,
|
||||
direction: Direction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Direction {
|
||||
N,
|
||||
E,
|
||||
S,
|
||||
W,
|
||||
None,
|
||||
}
|
||||
|
||||
// Global state
|
||||
static mut CONFIG: Option<Config> = None;
|
||||
static mut KNOWN_ENEMY_CORES: Vec<Position> = Vec::new();
|
||||
|
||||
// Simple JSON parsing
|
||||
struct JsonParser<'a> {
|
||||
input: &'a str,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> JsonParser<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self { input, pos: 0 }
|
||||
}
|
||||
|
||||
#[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())
|
||||
fn skip_whitespace(&mut self) {
|
||||
while self.pos < self.input.len() {
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b' ' || c == b'\n' || c == b'\r' || c == b'\t' {
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
fn parse_config(&mut self) -> Config {
|
||||
let mut config = Config::default();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'{' {
|
||||
return config;
|
||||
}
|
||||
self.pos += 1;
|
||||
|
||||
// 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());
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for bot in &state.bots {
|
||||
if bot.owner != my_id {
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'}' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
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;
|
||||
if c == b'"' {
|
||||
let key = self.parse_string();
|
||||
self.skip_whitespace();
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b':' {
|
||||
self.pos += 1;
|
||||
self.skip_whitespace();
|
||||
let value = self.parse_number();
|
||||
match key.as_str() {
|
||||
"rows" => config.rows = value,
|
||||
"cols" => config.cols = value,
|
||||
"attack_radius2" => config.attack_radius2 = value,
|
||||
"max_turns" => config.max_turns = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_dir.to_string()
|
||||
config
|
||||
}
|
||||
|
||||
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 parse_state(&mut self) -> VisibleState {
|
||||
let mut state = VisibleState::default();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'{' {
|
||||
return state;
|
||||
}
|
||||
self.pos += 1;
|
||||
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'}' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'"' {
|
||||
let key = self.parse_string();
|
||||
self.skip_whitespace();
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b':' {
|
||||
self.pos += 1;
|
||||
self.skip_whitespace();
|
||||
match key.as_str() {
|
||||
"you" => state.you = self.parse_you(),
|
||||
"bots" => state.bots = self.parse_bots(),
|
||||
"cores" => state.cores = self.parse_cores(),
|
||||
"walls" => state.walls = self.parse_positions(),
|
||||
_ => {
|
||||
self.skip_value();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
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;
|
||||
fn parse_you(&mut self) -> PlayerInfo {
|
||||
let mut info = PlayerInfo::default();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'{' {
|
||||
return info;
|
||||
}
|
||||
if dc > config.cols / 2 {
|
||||
dc = config.cols - dc;
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'}' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'"' {
|
||||
let key = self.parse_string();
|
||||
self.skip_whitespace();
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b':' {
|
||||
self.pos += 1;
|
||||
self.skip_whitespace();
|
||||
let value = self.parse_number();
|
||||
match key.as_str() {
|
||||
"id" => info.id = value,
|
||||
"energy" => info.energy = value,
|
||||
"score" => info.score = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
dr * dr + dc * dc
|
||||
fn parse_bots(&mut self) -> Vec<VisibleBot> {
|
||||
let mut bots = Vec::new();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'[' {
|
||||
return bots;
|
||||
}
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b']' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'{' {
|
||||
bots.push(self.parse_bot());
|
||||
}
|
||||
}
|
||||
bots
|
||||
}
|
||||
|
||||
fn parse_bot(&mut self) -> VisibleBot {
|
||||
let mut bot = VisibleBot::default();
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'}' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'"' {
|
||||
let key = self.parse_string();
|
||||
self.skip_whitespace();
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b':' {
|
||||
self.pos += 1;
|
||||
self.skip_whitespace();
|
||||
if key == "position" {
|
||||
bot.position = self.parse_position();
|
||||
} else if key == "owner" {
|
||||
bot.owner = self.parse_number();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bot
|
||||
}
|
||||
|
||||
fn parse_cores(&mut self) -> Vec<VisibleCore> {
|
||||
let mut cores = Vec::new();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'[' {
|
||||
return cores;
|
||||
}
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b']' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'{' {
|
||||
cores.push(self.parse_core());
|
||||
}
|
||||
}
|
||||
cores
|
||||
}
|
||||
|
||||
fn parse_core(&mut self) -> VisibleCore {
|
||||
let mut core = VisibleCore::default();
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'}' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'"' {
|
||||
let key = self.parse_string();
|
||||
self.skip_whitespace();
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b':' {
|
||||
self.pos += 1;
|
||||
self.skip_whitespace();
|
||||
if key == "position" {
|
||||
core.position = self.parse_position();
|
||||
} else if key == "owner" {
|
||||
core.owner = self.parse_number();
|
||||
} else if key == "active" {
|
||||
let next = self.input.as_bytes()[self.pos];
|
||||
core.active = if next == b't' {
|
||||
self.pos += 4;
|
||||
true
|
||||
} else {
|
||||
self.pos += 5;
|
||||
false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
core
|
||||
}
|
||||
|
||||
fn parse_positions(&mut self) -> Vec<Position> {
|
||||
let mut positions = Vec::new();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'[' {
|
||||
return positions;
|
||||
}
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b']' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'{' {
|
||||
positions.push(self.parse_position());
|
||||
}
|
||||
}
|
||||
positions
|
||||
}
|
||||
|
||||
fn parse_position(&mut self) -> Position {
|
||||
let mut pos = Position::default();
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'{' {
|
||||
return pos;
|
||||
}
|
||||
self.pos += 1;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
break;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'}' {
|
||||
self.pos += 1;
|
||||
break;
|
||||
}
|
||||
if c == b',' {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if c == b'"' {
|
||||
let key = self.parse_string();
|
||||
self.skip_whitespace();
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b':' {
|
||||
self.pos += 1;
|
||||
self.skip_whitespace();
|
||||
let value = self.parse_number();
|
||||
if key == "row" {
|
||||
pos.row = value;
|
||||
} else if key == "col" {
|
||||
pos.col = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
fn parse_string(&mut self) -> String {
|
||||
if self.pos >= self.input.len() || self.input.as_bytes()[self.pos] != b'"' {
|
||||
return String::new();
|
||||
}
|
||||
self.pos += 1;
|
||||
let start = self.pos;
|
||||
while self.pos < self.input.len() {
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b'"' {
|
||||
let result = &self.input[start..self.pos];
|
||||
self.pos += 1;
|
||||
return result.to_string();
|
||||
}
|
||||
if c == b'\\' {
|
||||
self.pos += 2;
|
||||
} else {
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn parse_number(&mut self) -> i32 {
|
||||
self.skip_whitespace();
|
||||
let start = self.pos;
|
||||
if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'-' {
|
||||
self.pos += 1;
|
||||
}
|
||||
while self.pos < self.input.len() {
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c >= b'0' && c <= b'9' {
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let s = &self.input[start..self.pos];
|
||||
s.parse().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn skip_value(&mut self) {
|
||||
self.skip_whitespace();
|
||||
if self.pos >= self.input.len() {
|
||||
return;
|
||||
}
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
match c {
|
||||
b'"' => {
|
||||
self.parse_string();
|
||||
}
|
||||
b'{' => {
|
||||
self.pos += 1;
|
||||
let mut depth = 1;
|
||||
while self.pos < self.input.len() && depth > 0 {
|
||||
match self.input.as_bytes()[self.pos] {
|
||||
b'{' => depth += 1,
|
||||
b'}' => depth -= 1,
|
||||
b'"' => {
|
||||
self.parse_string();
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
b'[' => {
|
||||
self.pos += 1;
|
||||
let mut depth = 1;
|
||||
while self.pos < self.input.len() && depth > 0 {
|
||||
match self.input.as_bytes()[self.pos] {
|
||||
b'[' => depth += 1,
|
||||
b']' => depth -= 1,
|
||||
b'"' => {
|
||||
self.parse_string();
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
while self.pos < self.input.len() {
|
||||
let c = self.input.as_bytes()[self.pos];
|
||||
if c == b',' || c == b'}' || c == b']' {
|
||||
break;
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy functions
|
||||
fn get_rush_targets(state: &VisibleState, my_id: i32) -> Vec<Position> {
|
||||
let mut targets: Vec<Position> = state
|
||||
.cores
|
||||
.iter()
|
||||
.filter(|c| c.owner != my_id && c.active)
|
||||
.map(|c| c.position)
|
||||
.collect();
|
||||
|
||||
unsafe {
|
||||
for pos in &KNOWN_ENEMY_CORES {
|
||||
if !targets.contains(pos) {
|
||||
targets.push(*pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targets
|
||||
}
|
||||
|
||||
fn apply_dir(pos: Position, dir: Direction, rows: i32, cols: i32) -> Position {
|
||||
let (dr, dc) = match dir {
|
||||
Direction::N => (-1, 0),
|
||||
Direction::E => (0, 1),
|
||||
Direction::S => (1, 0),
|
||||
Direction::W => (0, -1),
|
||||
Direction::None => (0, 0),
|
||||
};
|
||||
|
||||
Position {
|
||||
row: ((pos.row + dr) % rows + rows) % rows,
|
||||
col: ((pos.col + dc) % cols + cols) % cols,
|
||||
}
|
||||
}
|
||||
|
||||
fn dist2(a: Position, b: Position, config: &Config) -> 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
|
||||
}
|
||||
|
||||
fn find_best_move(
|
||||
start: Position,
|
||||
targets: &[Position],
|
||||
enemy_positions: &[Position],
|
||||
walls: &[Position],
|
||||
config: &Config,
|
||||
) -> Option<Direction> {
|
||||
if targets.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rows = config.rows;
|
||||
let cols = config.cols;
|
||||
|
||||
let mut best_dir = Direction::None;
|
||||
let mut best_dist = i32::MAX;
|
||||
|
||||
for &dir in &[Direction::N, Direction::E, Direction::S, Direction::W] {
|
||||
let next = apply_dir(start, dir, rows, cols);
|
||||
|
||||
if walls.contains(&next) || enemy_positions.contains(&next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut min_target_dist = i32::MAX;
|
||||
for target in targets {
|
||||
let d = dist2(next, *target, config);
|
||||
if d < min_target_dist {
|
||||
min_target_dist = d;
|
||||
}
|
||||
}
|
||||
|
||||
if min_target_dist < best_dist {
|
||||
best_dist = min_target_dist;
|
||||
best_dir = dir;
|
||||
}
|
||||
}
|
||||
|
||||
if best_dir != Direction::None {
|
||||
Some(best_dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn direction_to_string(dir: Direction) -> &'static str {
|
||||
match dir {
|
||||
Direction::N => "N",
|
||||
Direction::E => "E",
|
||||
Direction::S => "S",
|
||||
Direction::W => "W",
|
||||
Direction::None => "",
|
||||
}
|
||||
}
|
||||
|
||||
// JSON output
|
||||
fn write_moves_json(moves: &[Move], buf: &mut Vec<u8>) {
|
||||
buf.push(b'[');
|
||||
for (i, mv) in moves.iter().enumerate() {
|
||||
if i > 0 {
|
||||
buf.push(b',');
|
||||
}
|
||||
buf.push(b'{');
|
||||
buf.extend_from_slice(b"\"position\":{\"row\":");
|
||||
write_number(mv.position.row, buf);
|
||||
buf.push(b',');
|
||||
buf.extend_from_slice(b"\"col\":");
|
||||
write_number(mv.position.col, buf);
|
||||
buf.push(b'}');
|
||||
buf.push(b',');
|
||||
buf.extend_from_slice(b"\"direction\":\"");
|
||||
buf.extend_from_slice(direction_to_string(mv.direction).as_bytes());
|
||||
buf.push(b'"');
|
||||
buf.push(b'}');
|
||||
}
|
||||
buf.push(b']');
|
||||
}
|
||||
|
||||
fn write_number(n: i32, buf: &mut Vec<u8>) {
|
||||
let mut n = n;
|
||||
if n < 0 {
|
||||
buf.push(b'-');
|
||||
n = -n;
|
||||
}
|
||||
let mut digits = [0u8; 11];
|
||||
let mut i = 10;
|
||||
if n == 0 {
|
||||
buf.push(b'0');
|
||||
return;
|
||||
}
|
||||
while n > 0 {
|
||||
digits[i] = b'0' + (n % 10) as u8;
|
||||
n /= 10;
|
||||
i -= 1;
|
||||
}
|
||||
buf.extend_from_slice(&digits[i + 1..]);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
// WASM exports
|
||||
// ────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn allocate(size: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
heap_init();
|
||||
let ptr = align_up(HEAP_PTR, 8);
|
||||
HEAP_PTR = ptr + size;
|
||||
if HEAP_PTR > HEAP_END {
|
||||
return core::ptr::null_mut();
|
||||
}
|
||||
ptr as *mut u8
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init(ptr: *const u8, len: usize) {
|
||||
reset_allocator();
|
||||
let bytes = unsafe { core::slice::from_raw_parts(ptr, len) };
|
||||
let input = core::str::from_utf8(bytes).unwrap_or("{}");
|
||||
|
||||
let mut parser = JsonParser::new(input);
|
||||
let config = parser.parse_config();
|
||||
|
||||
unsafe {
|
||||
CONFIG = Some(config);
|
||||
KNOWN_ENEMY_CORES = Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn compute_moves(ptr: *const u8, len: usize) -> *const u8 {
|
||||
reset_allocator();
|
||||
let bytes = unsafe { core::slice::from_raw_parts(ptr, len) };
|
||||
let input = core::str::from_utf8(bytes).unwrap_or("{}");
|
||||
|
||||
let mut parser = JsonParser::new(input);
|
||||
let state = parser.parse_state();
|
||||
|
||||
let config = unsafe { CONFIG.as_ref().unwrap() };
|
||||
let my_id = state.you.id;
|
||||
|
||||
// Update known enemy cores
|
||||
for core in &state.cores {
|
||||
if core.owner != my_id {
|
||||
unsafe {
|
||||
if !KNOWN_ENEMY_CORES.contains(&core.position) {
|
||||
KNOWN_ENEMY_CORES.push(core.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separate my bots from enemies
|
||||
let (my_bots, enemy_bots): (Vec<_>, Vec<_>) =
|
||||
state.bots.iter().partition(|b| b.owner == my_id);
|
||||
|
||||
if my_bots.is_empty() {
|
||||
return return_result("[]");
|
||||
}
|
||||
|
||||
let enemy_positions: Vec<Position> =
|
||||
enemy_bots.iter().map(|b| b.position).collect();
|
||||
let walls = state.walls.clone();
|
||||
let targets = get_rush_targets(&state, my_id);
|
||||
|
||||
let mut moves = Vec::with_capacity(my_bots.len());
|
||||
|
||||
for bot in &my_bots {
|
||||
if let Some(dir) = find_best_move(
|
||||
bot.position,
|
||||
&targets,
|
||||
&enemy_positions,
|
||||
&walls,
|
||||
config,
|
||||
) {
|
||||
moves.push(Move {
|
||||
position: bot.position,
|
||||
direction: dir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = Vec::new();
|
||||
write_moves_json(&moves, &mut output);
|
||||
|
||||
return_result_bytes(&output)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn free_result(_ptr: *const u8) {
|
||||
// Bump allocator - no free needed
|
||||
}
|
||||
|
||||
fn return_result(s: &str) -> *const u8 {
|
||||
return_result_bytes(s.as_bytes())
|
||||
}
|
||||
|
||||
fn return_result_bytes(bytes: &[u8]) -> *const u8 {
|
||||
unsafe {
|
||||
let ptr = allocate(bytes.len());
|
||||
if ptr.is_null() {
|
||||
return core::ptr::null();
|
||||
}
|
||||
core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
|
||||
ptr
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue