astar-mumu 0.2.0-rc.4

A* algorithm plugin for the Lava language
Documentation
// FILE: astar/src/boulders.rs
//
// Lava bridge-function: astar:boulders (KEYED ARRAY VERSION)
// ----------------------------------------------------------
// Expects a single keyed array argument.
// 
// Usage: astar:boulders([
//   seed:   42,
//   width:  30,
//   height: 30,
//   count:  8,
//   start:  [1,1],     // optional
//   end:    [28,28],   // optional
// ])
//
// Output is compatible with astar:maze, astar:png, astar:path, etc.

use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::Value;
use indexmap::IndexMap;
use fastrand;

pub fn astar_boulders_bridge(
    _interp: &mut Interpreter,
    args: Vec<Value>,
) -> Result<Value, String> {
    if args.len() != 1 {
        return Err("astar:boulders ⇒ expects a single keyed array argument".into());
    }

    let map = match &args[0] {
        Value::KeyedArray(m) => m,
        _ => return Err("astar:boulders ⇒ first argument must be keyed array".into()),
    };

    let seed = get_i64(map, "seed")? as u64;
    let width = get_i32(map, "width")?;
    let height = get_i32(map, "height")?;
    let count = get_i32(map, "count")?;

    let (sx, sy) = match map.get("start") {
        Some(Value::IntArray(xs)) if xs.len() == 2 => (xs[0], xs[1]),
        _ => (0, 0),
    };
    let (ex, ey) = match map.get("end") {
        Some(Value::IntArray(xs)) if xs.len() == 2 => (xs[0], xs[1]),
        _ => (width - 1, height - 1),
    };

    if width < 3 || height < 3 {
        return Err("astar:boulders ⇒ width and height must be >= 3".into());
    }
    if sx < 0 || sx >= width || sy < 0 || sy >= height {
        return Err("astar:boulders ⇒ (start) must be within the grid".into());
    }
    if ex < 0 || ex >= width || ey < 0 || ey >= height {
        return Err("astar:boulders ⇒ (end) must be within the grid".into());
    }

    // Deterministic RNG for this boulders field:
    fastrand::seed(seed);

    // -- Collect all obstacle cells:
    let mut obstacle_set = std::collections::HashSet::<(i32, i32)>::new();

    for _ in 0..count {
        // Pick a random center and radius
        let cx = fastrand::i32(2..width-2);
        let cy = fastrand::i32(2..height-2);
        let r_major = fastrand::i32(2..=(width/8).max(2));
        let r_minor = fastrand::i32(2..=(height/8).max(2));

        for dy in -r_minor..=r_minor {
            for dx in -r_major..=r_major {
                let fx = dx as f32 / r_major as f32;
                let fy = dy as f32 / r_minor as f32;
                if fx * fx + fy * fy <= 1.0 {
                    let x = cx + dx;
                    let y = cy + dy;
                    if x >= 0 && x < width && y >= 0 && y < height {
                        obstacle_set.insert((x, y));
                    }
                }
            }
        }
    }

    // Remove the start and end locations if present:
    obstacle_set.remove(&(sx, sy));
    obstacle_set.remove(&(ex, ey));

    // Collect into Int2DArray:
    let mut obstacles: Vec<Vec<i32>> = obstacle_set
        .into_iter()
        .map(|(x, y)| vec![x, y])
        .collect();

    obstacles.sort(); // for deterministic output

    // Build output:
    let mut out = IndexMap::new();
    out.insert("dimension".into(), Value::IntArray(vec![width, height]));
    out.insert("start".into(),     Value::IntArray(vec![sx, sy]));
    out.insert("end".into(),       Value::IntArray(vec![ex, ey]));
    out.insert("obj".into(),       Value::Int2DArray(obstacles));

    Ok(Value::KeyedArray(out))
}

// Helpers:
fn get_i32(map: &IndexMap<String, Value>, key: &str) -> Result<i32, String> {
    match map.get(key) {
        Some(Value::Int(i)) => Ok(*i),
        Some(Value::Long(l)) => Ok(*l as i32),
        Some(v) => Err(format!("boulders ⇒ field '{}' must be int/long, got {:?}", key, v)),
        None => Err(format!("boulders ⇒ field '{}' is required", key)),
    }
}
fn get_i64(map: &IndexMap<String, Value>, key: &str) -> Result<i64, String> {
    match map.get(key) {
        Some(Value::Int(i)) => Ok(*i as i64),
        Some(Value::Long(l)) => Ok(*l),
        Some(v) => Err(format!("boulders ⇒ field '{}' must be int/long, got {:?}", key, v)),
        None => Err(format!("boulders ⇒ field '{}' is required", key)),
    }
}