use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::Value;
use indexmap::IndexMap;
use fastrand;
use image::{ImageBuffer, Rgba};
use std::env;
pub fn astar_maze_bridge(
_interp: &mut Interpreter,
args: Vec<Value>,
) -> Result<Value, String> {
let debug_verbose = _interp.is_verbose() || env::var("MAZE_DEBUG").is_ok();
let (seed, width, height, sx, sy, ex, ey) = parse_maze_args(&args, false)?;
let (cells, tries, path_len, open_count) =
generate_solvable_maze(seed, width, height, sx, sy, ex, ey, debug_verbose)?;
if debug_verbose {
eprintln!(
"[maze] Success after {} attempt(s); open cells: {}, path_len: {}",
tries, open_count, path_len
);
}
let obstacles = cells_to_obstacles(&cells, width, height);
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))
}
pub fn astar_maze_png_bridge(
_interp: &mut Interpreter,
args: Vec<Value>,
) -> Result<Value, String> {
let debug_verbose = _interp.is_verbose() || env::var("MAZE_DEBUG").is_ok();
if args.len() < 2 {
return Err("astar:maze_png ⇒ needs at least a seed and filename".into());
}
let (seed, width, height, sx, sy, ex, ey) = parse_maze_args(&args[..args.len() - 1], true)?;
let filename = parse_filename_arg(&args[args.len() - 1])?;
let (cells, tries, path_len, open_count) =
generate_solvable_maze(seed, width, height, sx, sy, ex, ey, debug_verbose)?;
if debug_verbose {
eprintln!(
"[maze_png] Success after {} attempt(s); open cells: {}, path_len: {}",
tries, open_count, path_len
);
}
let obstacles = cells_to_obstacles(&cells, width, height);
let mut img = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width as u32, height as u32);
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize;
let pixel = if cells[idx] { Rgba([255, 255, 255, 255]) }
else { Rgba([0, 0, 0, 255]) };
img.put_pixel(x as u32, y as u32, pixel);
}
}
img.save(&filename)
.map_err(|e| format!("astar:maze_png ⇒ cannot write '{}': {}", filename, e))?;
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))
}
fn generate_solvable_maze(
user_seed: u64,
width: i32,
height: i32,
sx: i32,
sy: i32,
ex: i32,
ey: i32,
debug_verbose: bool,
) -> Result<(Vec<bool>, i32, usize, usize), String> {
let max_tries = 1_000;
let base_seed = user_seed
.wrapping_add(((sx as u64) << 32) | ((sy as u64) << 16));
for tries in 1..=max_tries {
let maze_seed = base_seed.wrapping_add(tries as u64);
fastrand::seed(maze_seed);
let mut cells = vec![false; (width * height) as usize];
carve_passages_dfs(sx, sy, width, height, &mut cells);
let sidx = (sy * width + sx) as usize;
let eidx = (ey * width + ex) as usize;
cells[sidx] = true;
cells[eidx] = true;
let open_count = cells.iter().filter(|b| **b).count();
let (solved, path_len) = match find_path(&cells, width, height, sx, sy, ex, ey) {
Some(path) => (true, path.len()),
None => (false, 0),
};
if debug_verbose {
eprintln!(
"[maze attempt {}/{}] seed={} open={}/{} {} path={} ({:.1} %)",
tries,
max_tries,
maze_seed,
open_count,
width * height,
if solved { "✓" } else { "✗" },
if solved { path_len.to_string() } else { "-".into() },
open_count as f64 * 100.0 / (width * height) as f64
);
}
if solved {
return Ok((cells, tries, path_len, open_count));
}
}
Err(format!(
"astar:maze ⇒ could not generate a solvable maze in {} attempts",
max_tries
))
}
fn carve_passages_dfs(x: i32, y: i32, w: i32, h: i32, cells: &mut [bool]) {
let idx = (y * w + x) as usize;
cells[idx] = true;
let mut dirs = vec![(1, 0), (-1, 0), (0, 1), (0, -1)];
fastrand::shuffle(&mut dirs);
for &(dx, dy) in &dirs {
let nx = x + dx * 2;
let ny = y + dy * 2;
if nx >= 0 && nx < w && ny >= 0 && ny < h {
let nidx = (ny * w + nx) as usize;
if !cells[nidx] {
let mx = x + dx;
let my = y + dy;
let midx = (my * w + mx) as usize;
cells[midx] = true;
carve_passages_dfs(nx, ny, w, h, cells);
}
}
}
}
fn find_path(
cells: &[bool],
w: i32,
h: i32,
sx: i32,
sy: i32,
ex: i32,
ey: i32,
) -> Option<Vec<(i32, i32)>> {
use std::collections::VecDeque;
let mut visited = vec![false; (w * h) as usize];
let mut prev = vec![None; (w * h) as usize];
let start_idx = (sy * w + sx) as usize;
let end_idx = (ey * w + ex) as usize;
let mut q = VecDeque::new();
q.push_back(start_idx);
visited[start_idx] = true;
while let Some(idx) = q.pop_front() {
if idx == end_idx { break; }
let x = (idx as i32) % w;
let y = (idx as i32) / w;
for &(dx, dy) in &[(1,0), (-1,0), (0,1), (0,-1)] {
let nx = x + dx;
let ny = y + dy;
if nx < 0 || nx >= w || ny < 0 || ny >= h { continue; }
let nidx = (ny * w + nx) as usize;
if cells[nidx] && !visited[nidx] {
visited[nidx] = true;
prev[nidx] = Some(idx);
q.push_back(nidx);
}
}
}
if !visited[end_idx] { return None; }
let mut path = Vec::<(i32, i32)>::new();
let mut cur = end_idx;
loop {
let x = (cur as i32) % w;
let y = (cur as i32) / w;
path.push((x, y));
if cur == start_idx { break; }
cur = prev[cur]?; }
path.reverse();
Some(path)
}
fn cells_to_obstacles(cells: &[bool], w: i32, h: i32) -> Vec<Vec<i32>> {
let mut out = Vec::new();
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) as usize;
if !cells[idx] {
out.push(vec![x, y]);
}
}
}
out
}
fn parse_maze_args(
args: &[Value],
is_png: bool,
) -> Result<(u64, i32, i32, i32, i32, i32, i32), String> {
if args.is_empty() {
return Err("astar:maze ⇒ must pass at least a seed".into());
}
let max_n = if is_png { 7 } else { 7 };
let n = args.len().min(max_n);
let seed = match &args[0] {
Value::Int(i) => *i as u64,
Value::Long(l) => *l as u64,
other => return Err(format!("maze ⇒ seed must be int/long, got {:?}", other)),
};
let mut width = 21;
let mut height = 21;
let mut sx = 0;
let mut sy = 0;
let mut ex = -1;
let mut ey = -1;
if n >= 2 { width = parse_int_arg(&args[1], "width")?; }
if n >= 3 { height = parse_int_arg(&args[2], "height")?; }
if n >= 4 { sx = parse_int_arg(&args[3], "startX")?; }
if n >= 5 { sy = parse_int_arg(&args[4], "startY")?; }
if n >= 6 { ex = parse_int_arg(&args[5], "endX")?; }
if n >= 7 { ey = parse_int_arg(&args[6], "endY")?; }
if ex < 0 { ex = width - 1; }
if ey < 0 { ey = height - 1; }
if width < 3 || height < 3 || width % 2 == 0 || height % 2 == 0 {
return Err("maze ⇒ width and height must be odd numbers ≥ 3".into());
}
if sx < 0 || sx >= width || sy < 0 || sy >= height {
return Err("maze ⇒ (sx,sy) must be within the grid".into());
}
if ex < 0 || ex >= width || ey < 0 || ey >= height {
return Err("maze ⇒ (ex,ey) must be within the grid".into());
}
Ok((seed, width, height, sx, sy, ex, ey))
}
fn parse_int_arg(v: &Value, name: &str) -> Result<i32, String> {
match v {
Value::Int(i) => Ok(*i),
Value::Long(l) => Ok(*l as i32),
other => Err(format!("maze ⇒ {} must be int/long, got {:?}", name, other)),
}
}
fn parse_filename_arg(v: &Value) -> Result<String, String> {
match v {
Value::SingleString(s) => Ok(s.clone()),
Value::StrArray(a) if a.len() == 1 => Ok(a[0].clone()),
o => Err(format!("maze_png ⇒ filename must be a single string, got {:?}", o)),
}
}