/// ASCII comparison demo for MoonPath.
///
/// The sample deliberately places expensive terrain on the shortest route so
/// BFS, Dijkstra, and A* visibly make different decisions.
///|
/// Parses a rectangular ASCII map.
///
/// - `.` is normal terrain
/// - `#` is a wall
/// - `1` through `9` are weighted terrain
/// - `S` and `G` are the start and goal
fn parse_map(
map : String,
) -> (@moonpath.Grid, @moonpath.Point, @moonpath.Point) {
let lines = split_lines(map)
let height = lines.length()
let mut width = 0
for line in lines {
if line.length() > width {
width = line.length()
}
}
let updates : Array[(@moonpath.Point, @moonpath.Cell)] = []
let mut start = @moonpath.Point::new(0, 0)
let mut goal = @moonpath.Point::new(0, 0)
for y = 0; y < height; y = y + 1 {
let line = lines[y]
for x = 0; x < line.length(); x = x + 1 {
let character = line[x]
if character == '#' {
updates.push((@moonpath.Point::new(x, y), @moonpath.Cell::Blocked))
} else if character == 'S' {
start = @moonpath.Point::new(x, y)
} else if character == 'G' {
goal = @moonpath.Point::new(x, y)
} else if character >= '1' && character <= '9' {
updates.push(
(
@moonpath.Point::new(x, y),
@moonpath.Cell::Weighted(character.to_int() - '0'.to_int()),
),
)
}
}
}
let grid = @moonpath.Grid::new(width, height).set_cells(updates)
(grid, start, goal)
}
///|
fn split_lines(text : String) -> Array[String] {
let result : Array[String] = []
let mut current = ""
for character in text {
if character == '\n' || character == '\r' {
if current.length() > 0 {
result.push(current)
current = ""
}
} else {
current = current + character.to_string()
}
}
if current.length() > 0 {
result.push(current)
}
result
}
///|
fn print_grid(
grid : @moonpath.Grid,
start : @moonpath.Point,
goal : @moonpath.Point,
path : Array[@moonpath.Point],
) -> Unit {
let path_cells = Array::make(grid.size(), false)
for point in path {
path_cells[point.y * grid.width() + point.x] = true
}
for y = 0; y < grid.height(); y = y + 1 {
let mut row = ""
for x = 0; x < grid.width(); x = x + 1 {
let point = @moonpath.Point::new(x, y)
let index = y * grid.width() + x
let symbol = if point == start {
"S"
} else if point == goal {
"G"
} else if path_cells[index] {
"*"
} else if !grid.is_passable(point) {
"#"
} else if grid.cost_at(point) > 1.0 {
grid.cost_at(point).to_int().to_string()
} else {
"."
}
row = row + symbol
if x < grid.width() - 1 {
row = row + " "
}
}
println(row)
}
}
///|
fn terrain_cost(grid : @moonpath.Grid, path : Array[@moonpath.Point]) -> Double {
let mut cost = 0.0
for index = 1; index < path.length(); index = index + 1 {
cost = cost + grid.cost_at(path[index])
}
cost
}
///|
fn print_result(
name : String,
grid : @moonpath.Grid,
start : @moonpath.Point,
goal : @moonpath.Point,
result : @moonpath.SearchResult,
) -> Unit {
println("")
println("--- " + name + " ---")
match result.path {
None => println("No path found")
Some(path) => {
print_grid(grid, start, goal, path)
println("Steps: " + result.steps().to_string())
println("Search cost: " + result.total_cost.to_string())
println("Terrain cost: " + terrain_cost(grid, path).to_string())
println(
"Visited / expanded: " +
result.nodes_visited.to_string() +
" / " +
result.nodes_expanded.to_string(),
)
}
}
}
///|
fn main {
let map = "S99999G\n.#####.\n......."
let (grid, start, goal) = parse_map(map)
println("=== MoonPath ยท Pathfinding Comparison ===")
println("Legend: # wall, 1-9 terrain cost, * selected path")
println("")
println("Map:")
print_grid(grid, start, goal, [])
print_result(
"BFS (fewest steps; ignores weights)",
grid,
start,
goal,
@moonpath.bfs(grid, start, goal),
)
print_result(
"Dijkstra (lowest terrain cost)",
grid,
start,
goal,
@moonpath.dijkstra(grid, start, goal),
)
let options = @moonpath.SearchOptions::four_way()
print_result(
"A* (lowest cost, heuristic-guided)",
grid,
start,
goal,
@moonpath.astar(grid, start, goal, options),
)
println("")
println(
"BFS takes the short expensive route; Dijkstra and A* detour around it.",
)
}