use extism_pdk::*; use serde::{Deserialize, Serialize}; const ABI_VERSION: u16 = 1; const BOUNDARY_TERRAIN: i32 = 3; #[derive(Deserialize)] struct DecisionInput { #[allow(dead_code)] abi_version: u16, tick: u64, x: u16, y: u16, adjacent_terrain: [i32; 4], } #[derive(Serialize)] struct DecisionOutput { abi_version: u16, action: i32, } /// Deterministic decision function called once per simulation tick. /// /// The host provides the full observation as JSON input and expects a JSON /// decision back; no filesystem, network, clock, or RNG access is available. /// Movement here is a reproducible function of (tick, x, y) alone, so replay /// with the same seed and inputs always yields the same actions. #[plugin_fn] pub fn decide(input: Json) -> FnResult> { let input = input.0; // Action codes: 1=North, 2=East, 3=South, 4=West, matching adjacent_terrain order. let open_directions: Vec = input .adjacent_terrain .iter() .enumerate() .filter(|(_, terrain)| **terrain != BOUNDARY_TERRAIN) .map(|(index, _)| (index + 1) as i32) .collect(); let action = if open_directions.is_empty() { 0 // Wait } else { let seed = deterministic_seed(input.tick, input.x, input.y); open_directions[(seed as usize) % open_directions.len()] }; Ok(Json(DecisionOutput { abi_version: ABI_VERSION, action, })) } /// SplitMix64-style mix so the same (tick, x, y) always yields the same choice. fn deterministic_seed(tick: u64, x: u16, y: u16) -> u64 { let mut state = tick .wrapping_mul(0x9E37_79B9_7F4A_7C15) .wrapping_add(u64::from(x).wrapping_mul(0xBF58_476D_1CE4_E5B9)) .wrapping_add(u64::from(y).wrapping_mul(0x94D0_49BB_1331_11EB)); state ^= state >> 33; state = state.wrapping_mul(0xFF51_AFD7_ED55_8CCD); state ^= state >> 33; state }