moonnavkit

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

    pathfinding
    astar
    flow-field
    graph
    grid
    visualization
    Download zip
    Author
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    20

    #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