moonnavkit

Weighted path planning, multi-goal flow fields, and search visualization for MoonBit.

pathfinding
astar
flow-field
graph
grid
visualization
moon add cn-cheems/moonnavkit@0.3.0
Download zip
Author
Version
0.3.0
License
Apache-2.0
Last updated
last month
Downloads
13
README

#MoonNavKit

MoonNavKit is a backend-neutral MoonBit path planning, reusable flow-field, and search visualization toolkit.

It provides reusable grid and graph pathfinding primitives for games, simulation, teaching tools, and visualization systems.

#Status

This project is being prepared for the MoonBit open-source ecosystem contribution contest.

Current foundation:

  • Grid coordinates with Point
  • Weighted rectangular GridMap
  • Weighted directed/undirected Graph
  • Four-direction neighbors
  • BFS for unweighted shortest paths
  • Dijkstra for weighted shortest paths
  • A* with Manhattan, Euclidean, and Octile heuristics
  • Weighted multi-goal flow fields for many-agent routing
  • Admissibility-safe A* on arbitrary weighted graphs
  • Stable min-priority queue for open-set management
  • Search trace recording
  • JSON export for path results and trace data
  • SVG export for visual inspection
  • Graphviz DOT export for graph search results
  • Standalone HTML replay export
  • Deterministic random grid generation
  • Grid statistics for benchmark notes

#Quick Example

test {
let grid = GridMap::new(5, 5)
.set_blocked(Point::new(1, 0))
.set_blocked(Point::new(1, 1))
.set_weight(Point::new(3, 2), 5)

let result = grid.astar(
Point::new(0, 0),
Point::new(4, 4),
Heuristic::Manhattan,
)

assert_true(result.found)
let json = result.to_json()
assert_true(json.length() > 0)
}

#Graph Example

MoonNavKit also supports general weighted graphs. Node positions are optional for Dijkstra, but useful for A* heuristics and visualization-oriented exports.

test {
let graph = Graph::new()
let start = graph.add_node(Point::new(0, 0))
let mid = graph.add_node(Point::new(1, 0))
let goal = graph.add_node(Point::new(2, 0))

graph.add_directed_edge(start, mid, 1) |> ignore
graph.add_directed_edge(mid, goal, 1) |> ignore
graph.add_directed_edge(start, goal, 5) |> ignore

let result = graph.astar(start, goal, Heuristic::Manhattan)

assert_true(result.found)
assert_eq(result.cost, 2)
assert_true(result.nodes == [start, mid, goal])
}

#Many-Agent Flow Fields

When many agents share destinations, repeatedly running A* wastes the same search work. flow_field performs one reverse Dijkstra build, then reconstructs each agent route in O(path length).

test {
let grid = GridMap::new(8, 5)
.set_weight(Point::new(3, 2), 8)
.set_blocked(Point::new(4, 2))
let loading_bay = Point::new(7, 2)
let emergency_exit = Point::new(0, 4)
let field = grid.flow_field([loading_bay, emergency_exit])

let robot_route = field.path_from(Point::new(1, 0))
assert_true(robot_route.found)
assert_true(field.goal_for(Point::new(1, 0)) is Some(_))
}

This supports deterministic routing for warehouse robots, game units, evacuation simulations, and repeated “nearest service point” queries. Invalid or blocked goals are ignored, unreachable cells remain explicit, and the field can be exported as JSON for inspection.

Graph results can also be exported as Graphviz DOT:

test {
let graph = Graph::new()
let start = graph.add_node(Point::new(0, 0))
let goal = graph.add_node(Point::new(1, 0))
graph.add_directed_edge(start, goal, 2) |> ignore

let result = graph.dijkstra(start, goal)
let dot = graph.to_dot(result)

assert_true(dot.contains("digraph MoonNavKit"))
assert_true(dot.contains("cost=2"))
}

#Trace Export

Every search records expanded cells in order. This makes the core library useful for debugging, teaching, and visualization replay.

test {
let result = GridMap::new(3, 1).bfs(Point::new(0, 0), Point::new(2, 0))
assert_eq(result.trace.length(), 3)
assert_eq(
result.trace.to_json(),
"{\"steps\":[{\"order\":0,\"point\":{\"x\":0,\"y\":0},\"cost\":0,\"score\":0},{\"order\":1,\"point\":{\"x\":1,\"y\":0},\"cost\":1,\"score\":1},{\"order\":2,\"point\":{\"x\":2,\"y\":0},\"cost\":2,\"score\":2}]}",
)
}

#Visualization Export

The same result can be exported as SVG or a standalone HTML replay page.

test {
let grid = GridMap::new(4, 3)
.set_blocked(Point::new(1, 0))
.set_blocked(Point::new(1, 1))
.set_weight(Point::new(2, 1), 4)

let result = grid.astar(
Point::new(0, 0),
Point::new(3, 2),
Heuristic::Manhattan,
)

let svg = grid.to_svg(result, 24)
assert_true(svg.contains("<svg"))

let html = grid.to_html(result, 24)
assert_true(html.contains("MoonNavKit Replay"))
}

SVG uses consistent colors for the main states:

  • Dark cells are blocked.
  • Yellow cells have custom weights.
  • Blue overlays are expanded search steps.
  • Green lines are final paths.
  • Green and red circles mark start and goal.

#Random Grid Generation

Seeded maps are deterministic, so examples, tests, and benchmark notes can be reproduced exactly.

test {
let start = Point::new(0, 0)
let goal = Point::new(5, 5)
let config = RandomGridConfig::new(6, 6, 11)
.with_blocked_percent(5)
.with_weighted_percent(30)
.with_max_weight(7)

let grid = GridMap::random_with_clear_points(config, [start, goal])
let stats = grid.stats()
let result = grid.astar(start, goal, Heuristic::Manhattan)

assert_eq(stats.cells, 36)
assert_true(result.trace.length() > 0)
}

#Roadmap

  • More examples and benchmark notes
  • Additional replay controls for generated HTML
  • Benchmark notes for grid and graph search

See Roadmap for planned contest deliverables and non-goals. See Performance Notes for current complexity and reproducible benchmark scenarios. See Benchmark Scenarios for the deterministic moon run cmd/bench output used to track search behavior across commits. See Flow Fields for cost semantics, complexity, and many-agent use cases. See Related Work for the project boundary within the MoonBit ecosystem.

#Development Tracking

MoonNavKit uses issue templates, pull request templates, and CHANGELOG.md so ongoing work can be reviewed through public repository history.

#
Algorithm

pub(all) enum Algorithm {
BFS
Dijkstra
AStar(Heuristic)
} derive(Eq,
Debug
)

#
FlowField

pub(all) struct FlowField {
width : Int
height : Int
goals : Array[Point]
costs : Array[Int]
next : Array[Int]
target : Array[Int]
reachable_cells : Int
build_visited_count : Int
} derive(
Debug
)

A reusable weighted routing field built from one or more destinations.

Building the field runs one reverse Dijkstra search. Each later path query follows precomputed next steps and does not repeat graph search.

#
FlowField::cost_from

fn FlowField::cost_from(self : FlowField, start : Point) -> Int?

Returns the optimal remaining movement cost, or None when unreachable.

#
FlowField::goal_for

fn FlowField::goal_for(self : FlowField, start : Point) -> Point?

Returns the destination selected for a reachable cell.

#
FlowField::next_step

fn FlowField::next_step(self : FlowField, start : Point) -> Point?

Returns the next cell on an optimal route. Goals have no next step.

#
FlowField::path_from

fn FlowField::path_from(self : FlowField, start : Point) -> PathResult

Reconstructs a route by following the field, in O(path length).

#
FlowField::to_json

fn FlowField::to_json(self : FlowField) -> String

Exports compact metadata plus the cost raster for diagnostics and tooling.

#
Graph

pub(all) struct Graph {
nodes : Array[GraphNode]
edges : Array[GraphEdge]
} derive(Eq,
Debug
)

A small mutable weighted graph optimized for clear APIs and deterministic tests.

#
Graph::add_directed_edge

fn Graph::add_directed_edge(self : Graph, from : Int, to : Int, weight : Int) -> Graph

#
Graph::add_node

fn Graph::add_node(self : Graph, position : Point) -> Int

#
Graph::add_undirected_edge

fn Graph::add_undirected_edge(self : Graph, a : Int, b : Int, weight : Int) -> Graph

#
Graph::astar

fn Graph::astar(self : Graph, start : Int, goal : Int, heuristic : Heuristic) -> GraphPathResult

#
Graph::bfs

fn Graph::bfs(self : Graph, start : Int, goal : Int) -> GraphPathResult

#
Graph::dijkstra

fn Graph::dijkstra(self : Graph, start : Int, goal : Int) -> GraphPathResult

#
Graph::edge_count

fn Graph::edge_count(self : Graph) -> Int

#
Graph::find_path

fn Graph::find_path(self : Graph, start : Int, goal : Int, algorithm : Algorithm) -> GraphPathResult

#
Graph::is_valid_node

fn Graph::is_valid_node(self : Graph, node : Int) -> Bool

#
Graph::neighbors

fn Graph::neighbors(self : Graph, node : Int) -> Array[GraphEdge]

#
Graph::new

fn Graph::new() -> Graph

#
Graph::node_count

fn Graph::node_count(self : Graph) -> Int

#
Graph::position

fn Graph::position(self : Graph, node : Int) -> Point?

#
Graph::to_dot

fn Graph::to_dot(self : Graph, result : GraphPathResult) -> String

Exports a graph pathfinding result as Graphviz DOT.

#
GraphEdge

pub(all) struct GraphEdge {
from : Int
to : Int
weight : Int
} derive(Eq,
Debug
)

A weighted directed graph edge. All search algorithms require positive weight.

#
GraphNode

pub(all) struct GraphNode {
id : Int
position : Point
} derive(Eq,
Debug
)

A graph node with an integer id and a position for heuristic search/export.

#
GraphPathResult

pub(all) struct GraphPathResult {
found : Bool
nodes : Array[Int]
points : Array[Point]
cost : Int
visited_count : Int
trace : SearchTrace
} derive(Eq,
Debug
)

#
GraphPathResult::not_found

fn GraphPathResult::not_found(visited_count : Int) -> GraphPathResult

#
GraphPathResult::to_json

fn GraphPathResult::to_json(self : GraphPathResult) -> String

#
GridMap

pub(all) struct GridMap {
width : Int
height : Int
weights : Array[Int]
} derive(Eq,
Debug
)

A rectangular weighted grid. A cell cost below zero means blocked.

#
GridMap::astar

fn GridMap::astar(self : GridMap, start : Point, goal : Point, heuristic : Heuristic) -> PathResult

#
GridMap::bfs

fn GridMap::bfs(self : GridMap, start : Point, goal : Point) -> PathResult

#
GridMap::blocked_count

fn GridMap::blocked_count(self : GridMap) -> Int

#
GridMap::cell_count

fn GridMap::cell_count(self : GridMap) -> Int

#
GridMap::contains

fn GridMap::contains(self : GridMap, point : Point) -> Bool

#
GridMap::cost_at

fn GridMap::cost_at(self : GridMap, point : Point) -> Int?

#
GridMap::dijkstra

fn GridMap::dijkstra(self : GridMap, start : Point, goal : Point) -> PathResult

#
GridMap::find_path

fn GridMap::find_path(self : GridMap, start : Point, goal : Point, algorithm : Algorithm) -> PathResult

#
GridMap::flow_field

fn GridMap::flow_field(self : GridMap, goals : Array[Point]) -> FlowField

Builds a deterministic multi-goal flow field over four-way grid movement.

Invalid, blocked, and duplicate goals are ignored. Cell-entry costs use the same semantics as GridMap::dijkstra.

#
GridMap::index

fn GridMap::index(self : GridMap, point : Point) -> Int?

#
GridMap::is_blocked

fn GridMap::is_blocked(self : GridMap, point : Point) -> Bool

#
GridMap::neighbors4

fn GridMap::neighbors4(self : GridMap, point : Point) -> Array[Point]

#
GridMap::new

fn GridMap::new(width : Int, height : Int) -> GridMap

#
GridMap::point_at

fn GridMap::point_at(self : GridMap, index : Int) -> Point

#
GridMap::random

fn GridMap::random(config : RandomGridConfig) -> GridMap

Builds a deterministic pseudo-random grid from a seed.

#
GridMap::random_with_clear_points

fn GridMap::random_with_clear_points(config : RandomGridConfig, clear_points : Array[Point]) -> GridMap

Builds a deterministic grid and guarantees important cells stay open.

#
GridMap::set_blocked

fn GridMap::set_blocked(self : GridMap, point : Point) -> GridMap

#
GridMap::set_open

fn GridMap::set_open(self : GridMap, point : Point) -> GridMap

#
GridMap::set_weight

fn GridMap::set_weight(self : GridMap, point : Point, weight : Int) -> GridMap

#
GridMap::stats

fn GridMap::stats(self : GridMap) -> GridStats

#
GridMap::to_html

fn GridMap::to_html(self : GridMap, result : PathResult, cell_size : Int) -> String

Exports a pathfinding result as a minimal standalone HTML document.

#
GridMap::to_svg

fn GridMap::to_svg(self : GridMap, result : PathResult, cell_size : Int) -> String

Exports a grid pathfinding result as a standalone SVG string.

#
GridMap::weighted_count

fn GridMap::weighted_count(self : GridMap) -> Int

#
GridStats

pub(all) struct GridStats {
cells : Int
open_cells : Int
blocked_cells : Int
weighted_cells : Int
} derive(Eq,
Debug
)

#
Heuristic

pub(all) enum Heuristic {
Manhattan
Euclidean
Octile
} derive(Eq,
Debug
)

#
Heuristic::estimate

fn Heuristic::estimate(self : Heuristic, from : Point, to : Point) -> Int

#
MinPriorityQueue

pub(all) struct MinPriorityQueue {
entries : Array[QueueEntry]
next_order : Int
} derive(
Debug
)

Small stable min-priority queue used by shortest-path searches.

#
MinPriorityQueue::is_empty

fn MinPriorityQueue::is_empty(self : MinPriorityQueue) -> Bool

#
MinPriorityQueue::length

fn MinPriorityQueue::length(self : MinPriorityQueue) -> Int

#
MinPriorityQueue::new

#
MinPriorityQueue::peek

#
MinPriorityQueue::pop

#
MinPriorityQueue::push

fn MinPriorityQueue::push(self : MinPriorityQueue, item : Int, priority : Int) -> Unit

#
PathResult

pub(all) struct PathResult {
found : Bool
path : Array[Point]
cost : Int
visited_count : Int
trace : SearchTrace
} derive(Eq,
Debug
)

Result returned by path planning algorithms.

#
PathResult::not_found

fn PathResult::not_found(visited_count : Int) -> PathResult

#
PathResult::to_json

fn PathResult::to_json(self : PathResult) -> String

#
Point

pub(all) struct Point {
x : Int
y : Int
} derive(Eq,
Debug
)

A grid coordinate used by all path planning APIs.

#
Point::manhattan

fn Point::manhattan(self : Point, other : Point) -> Int

#
Point::new

fn Point::new(x : Int, y : Int) -> Point

#
Point::to_json

fn Point::to_json(self : Point) -> String

#
QueueEntry

pub(all) struct QueueEntry {
item : Int
priority : Int
order : Int
} derive(Eq,
Debug
)

A node id or cell index with its current search priority.

#
RandomGridConfig

pub(all) struct RandomGridConfig {
width : Int
height : Int
seed : Int
blocked_percent : Int
weighted_percent : Int
max_weight : Int
} derive(Eq,
Debug
)

Configuration for deterministic random grid generation.

#
RandomGridConfig::new

fn RandomGridConfig::new(width : Int, height : Int, seed : Int) -> RandomGridConfig

#
RandomGridConfig::with_blocked_percent

fn RandomGridConfig::with_blocked_percent(self : RandomGridConfig, blocked_percent : Int) -> RandomGridConfig

#
RandomGridConfig::with_max_weight

fn RandomGridConfig::with_max_weight(self : RandomGridConfig, max_weight : Int) -> RandomGridConfig

#
RandomGridConfig::with_weighted_percent

fn RandomGridConfig::with_weighted_percent(self : RandomGridConfig, weighted_percent : Int) -> RandomGridConfig

#
SearchStep

pub(all) struct SearchStep {
order : Int
point : Point
cost : Int
score : Int
} derive(Eq,
Debug
)

One expansion event produced by a search algorithm.

#
SearchStep::new

fn SearchStep::new(order : Int, point : Point, cost : Int, score : Int) -> SearchStep

#
SearchStep::to_json

fn SearchStep::to_json(self : SearchStep) -> String

#
SearchTrace

pub(all) struct SearchTrace {
steps : Array[SearchStep]
} derive(Eq,
Debug
)

Search trace is kept as data so it can feed JSON, SVG, HTML, or tests.

#
SearchTrace::length

fn SearchTrace::length(self : SearchTrace) -> Int

#
SearchTrace::new

#
SearchTrace::push_step

fn SearchTrace::push_step(self : SearchTrace, order : Int, point : Point, cost : Int, score : Int) -> Unit

#
SearchTrace::to_json

fn SearchTrace::to_json(self : SearchTrace) -> String