moonpath

    MoonPath: A Grid Pathfinding Toolkit for MoonBit. Provides BFS, Dijkstra, and A* pathfinding on 2D grid maps with configurable movement, heuristics, and terrain costs.

    pathfinding
    algorithm
    grid
    bfs
    dijkstra
    astar
    game-dev
    navigation
    moonbit
    Download zip
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    3 months ago
    Downloads
    46

    No dependencies

    #MoonPath

    CI MoonBit License

    MoonBit 原生的二维栅格寻路内核:用统一的数据模型、结果接口和可观测指标,对比 BFS、Dijkstra 与 A*。

    MoonPath is a zero-dependency grid pathfinding toolkit written in MoonBit. It supports obstacles, positive weighted terrain, four/eight-way movement, configurable A* heuristics, deterministic path selection, and search statistics.

    #评审速览

    维度已完成内容
    核心能力BFS、Dijkstra、A*,权重地形,四/八向移动,自定义启发式
    正确性65 / 65 测试通过,覆盖不可达、越界、溢出、非法权重和禁止穿角等边界
    覆盖率库核心源码 355 / 381 个插桩点,约 93.2%
    工程质量format、全目标静态检查、零警告、build、test、coverage、Demo 全部进入 CI
    可复测性Demo 与 benchmark 均为仓库内可执行入口,CI 会记录实际安装的 MoonBit 版本

    #30 秒体验

    git clone https://github.com/Burnling-gx/moonpath.git cd moonpath moon test moon run src/cmd/demo

    #Why MoonPath

    • 统一接口:三种算法都返回路径、代价、发现节点数和展开节点数。
    • 正确处理权重:Dijkstra 与 A* 按进入目标格的地形代价计算;非法的零/负权不会进入网格。
    • 安全八向移动:斜向代价为 sqrt(2) × terrain_cost,且不会从两个障碍之间穿角。
    • 适合批量建图:set_cells 只复制网格一次,加载 K 个更新的复杂度为 O(N + K)。
    • 可复现:65 个测试、基准入口、严格零警告检查、覆盖率、构建和 Demo 均进入 CI。

    项目价值不在重新发明经典算法,而在于为 MoonBit 生态提供一套可复用、可比较、边界语义明确的寻路基础设施。

    #Why MoonBit

    MoonPath 不只是把既有算法翻译成另一种语法,而是用 MoonBit 的语言与工具链能力组织完整工程:

    • 使用 enum 与模式匹配表达网格单元和可选路径,减少无效状态。
    • 使用强类型公开 API 明确移动方式、启发式约束、搜索结果和失败语义。
    • 使用 MoonBit 原生测试、黑盒测试、benchmark 和 coverage 命令形成质量闭环。
    • 通过 moon check --target all 持续验证多目标兼容性,并以 Wasm-GC release 构建复测性能。

    #Demo

    示例故意把高代价地形放在步数最少的直线上:

    S 9 9 9 9 9 G . # # # # # . . . . . . . .

    两类最优路径直观对比(* 表示路径):

    BFS(最少步数) Dijkstra / A*(最低代价) S * * * * * G S 9 9 9 9 9 G . # # # # # . * # # # # # * . . . . . . . * * * * * * *

    运行结果:

    AlgorithmStepsReported costActual terrain costVisited / expanded
    BFS664614 / 12
    Dijkstra10101013 / 11
    A* + Manhattan10101012 / 10

    BFS 找到步数最少但昂贵的直线路径;Dijkstra 与 A* 找到代价最低的绕行路径,A* 在本例中展开的节点更少。

    #Benchmark snapshot

    80 × 80 空网格、起点 (0, 40)、终点 (79, 40),本地 Wasm-GC release 构建的一次参考结果:

    AlgorithmMean time
    BFS385.29 µs
    Dijkstra995.29 µs
    A* + Manhattan44.89 µs

    该场景有利于目标导向启发式,仅用于验证基准入口和展示搜索聚焦效果,不代表所有地图上的通用倍率。运行 moon bench --release 可在当前机器复测。

    #Quick start

    要求近期版本的 MoonBit toolchain。CI 通过官方安装器获取当前可用版本并输出实际版本号;最近一次本地完整验证使用 moon 0.1.20260703。

    git clone https://github.com/Burnling-gx/moonpath.git cd moonpath moon check --target all --deny-warn moon test moon build moon run src/cmd/demo # Optional: run the benchmark suite moon bench --release

    预期测试摘要:

    Total tests: 65, passed: 65, failed: 0.

    当前测试覆盖库核心源码 355 / 381 个插桩点(约 93.2%);Demo 不计入单元测试覆盖率,但会在 CI 中单独构建和执行。

    #Install and use

    从 mooncakes.io 添加依赖:

    moon add Burnling-gx/moonpath

    在调用方的 moon.pkg 中导入:

    import {
    "Burnling-gx/moonpath",
    }

    然后通过包别名 @moonpath 使用公开 API:

    let grid = @moonpath.Grid::new(10, 10).set_cells([
    (@moonpath.Point::new(3, 3), @moonpath.Cell::Blocked),
    (@moonpath.Point::new(3, 4), @moonpath.Cell::Blocked),
    (@moonpath.Point::new(4, 2), @moonpath.Cell::Weighted(5)),
    ])

    let start = @moonpath.Point::new(0, 0)
    let goal = @moonpath.Point::new(9, 9)
    let result = @moonpath.astar(
    grid,
    start,
    goal,
    @moonpath.SearchOptions::four_way(),
    )

    match result.path {
    Some(path) => {
    println("Steps: " + result.steps().to_string())
    println("Cost: " + result.total_cost.to_string())
    }
    None => println("No path found")
    }

    #Algorithm guide

    AlgorithmWeighted terrainMovementOptimality
    bfsIgnoredFour-wayFewest steps
    dijkstraYesFour-wayLowest cost for positive weights
    dijkstra_with_movementYesFour/eight-wayLowest cost for positive weights
    astarYesFour/eight-wayLowest cost when its heuristic contract holds

    Recommended A* options:

    • SearchOptions::four_way() — Manhattan heuristic.
    • SearchOptions::eight_way() — Octile heuristic and no corner cutting.
    • SearchOptions::new(movement, heuristic) — custom configuration.

    Built-in heuristics are manhattan, euclidean, chebyshev, octile, and zero. A custom heuristic must be deterministic, finite, non-negative, zero at the goal, and must not overestimate the remaining cost. Negative or non-finite values are treated as zero defensively.

    #Cost and statistics semantics

    • Moving into Empty costs 1; moving into Weighted(n) costs n.
    • The start cell is not charged. A four-way path's cost is the sum of entered cells.
    • Eight-way diagonal movement multiplies the entered cell's cost by sqrt(2).
    • nodes_visited counts distinct discovered nodes, including the start.
    • nodes_expanded counts neighbor-generation events; the goal is not expanded, and a reopened node may count again.
    • On failure, path is None, total_cost is -1, and steps() returns -1.

    #Safety guarantees

    • Search rejects out-of-bounds or blocked start/goal points.
    • get_cell returns Blocked outside the grid; try_get_cell returns None.
    • Lenient setters ignore invalid updates; try_set_cell and try_set_weighted report them as None.
    • from_cells rejects invalid dimensions, invalid weights, and mismatched cell counts.
    • Grid dimensions are checked before multiplication to prevent cell-count overflow.
    • Grid storage is private and copied at construction boundaries, so callers cannot break invariants through array aliasing.

    #Public API at a glance

    Grid new / try_new / from_cells width / height / size / in_bounds get_cell / try_get_cell / cost / is_passable set_cell / try_set_cell / set_cells / set_wall set_weighted / try_set_weighted / clear Search bfs dijkstra / dijkstra_with_movement astar + SearchOptions Result path / total_cost / nodes_visited / nodes_expanded found / steps

    #Project structure

    moon.mod module metadata (source root: src) src/ moon.pkg library package configuration types.mbt public data model and result semantics grid.mbt validated immutable-style grid neighbors.mbt four/eight-way movement and costs priority_queue.mbt O(log n) deterministic binary min-heap bfs.mbt dijkstra.mbt astar.mbt heuristics.mbt *_test.mbt black-box tests and benchmark cases cmd/demo/ moon.pkg main.mbt weighted ASCII comparison demo .github/workflows/ci.yml format, check, build, test, coverage, demo

    #Roadmap

    • 持续发布带变更记录和可复测结果的版本。
    • 增加浏览器交互式路径可视化与更多地图密度的可复测基准。
    • 探索双向搜索、Jump Point Search 与 moon prove 形式化性质。

    #License

    Heuristic

    type Heuristic = (Point, Point) -> Double

    A heuristic function estimates the cost from a point to the goal.

    Common choices are Manhattan, Euclidean, and Chebyshev distance.

    Cell

    pub(all) enum Cell {
    Empty
    Blocked
    Weighted(Int)
    } derive(Eq,
    Debug
    )

    Terrain type for each grid cell.

    Cell::cost

    fn Cell::cost(self : Cell) -> Double

    Returns the traversal cost for this cell. Empty = 1.0, Blocked = -1.0 (sentinel), Weighted(c) = c as Double.

    Cell::is_passable

    fn Cell::is_passable(self : Cell) -> Bool

    Returns true if this cell is passable (not blocked).

    Cell::is_valid

    fn Cell::is_valid(self : Cell) -> Bool

    Returns whether this value is a valid grid cell. Weighted terrain must have a strictly positive traversal cost.

    Grid

    pub struct Grid {
    // private fields
    }

    A 2D grid map for pathfinding.

    Cells are stored in row-major order: index(x, y) = y * width + x

    Grid::clear

    fn Grid::clear(self : Grid) -> Grid

    Returns a copy of the grid with all cells reset to Empty.

    Grid::cost

    fn Grid::cost(self : Grid, x : Int, y : Int) -> Double

    Returns the traversal cost at (x, y). Returns -1.0 for blocked cells and out-of-bounds positions.

    Grid::cost_at

    fn Grid::cost_at(self : Grid, p : Point) -> Double

    Returns the traversal cost at a point. Returns -1.0 for blocked cells.

    Grid::from_cells

    fn Grid::from_cells(width : Int, height : Int, cells : Array[Cell]) -> Grid?

    Creates a Grid from an existing array of cells.

    Returns None for invalid dimensions, non-positive terrain weights, or a cell array length that does not match width * height.

    Grid::get_cell

    fn Grid::get_cell(self : Grid, x : Int, y : Int) -> Cell

    Returns the cell at (x, y), or Blocked when out of bounds.

    Grid::get_cell_at

    fn Grid::get_cell_at(self : Grid, p : Point) -> Cell

    Returns the cell at a point, or Blocked when out of bounds.

    Grid::height

    fn Grid::height(self : Grid) -> Int

    Returns the number of rows in the grid.

    Grid::in_bounds

    fn Grid::in_bounds(self : Grid, p : Point) -> Bool

    Checks if a point is within the grid bounds.

    Grid::is_passable

    fn Grid::is_passable(self : Grid, p : Point) -> Bool

    Checks if a cell is passable (exists in bounds and is not Blocked).

    Grid::is_passable_xy

    fn Grid::is_passable_xy(self : Grid, x : Int, y : Int) -> Bool

    Checks if a cell is passable by raw coordinates.
    fn Grid::is_valid_search(self : Grid, start : Point, goal : Point) -> Bool

    Checks if start and goal are both valid for pathfinding.

    Grid::new

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

    Creates an empty Grid where all cells are traversable (Cell::Empty).

    Arguments

    • width - number of columns (must be > 0)
    • height - number of rows (must be > 0)

    Returns a 1x1 grid when dimensions are non-positive or their cell count would overflow Int. Use try_new when invalid input must be reported.

    Grid::num_passable

    fn Grid::num_passable(self : Grid) -> Int

    Counts the number of passable cells in the grid.

    Grid::num_walls

    fn Grid::num_walls(self : Grid) -> Int

    Counts the number of blocked cells in the grid.

    Grid::set_cell

    fn Grid::set_cell(self : Grid, x : Int, y : Int, cell : Cell) -> Grid

    Sets the Cell at (x, y). Returns a new Grid (immutable style). Invalid coordinates and non-positive weighted cells leave the grid unchanged.

    Grid::set_cells

    fn Grid::set_cells(self : Grid, updates : Array[(Point, Cell)]) -> Grid

    Applies multiple cell updates with a single grid copy.

    Out-of-bounds points and invalid weighted cells are ignored. Prefer this method when loading maps or placing many obstacles: it runs in O(N + K) for N grid cells and K updates, while K chained set_cell calls copy the grid K times.

    Grid::set_wall

    fn Grid::set_wall(self : Grid, x : Int, y : Int) -> Grid

    Sets a wall (Blocked cell) at (x, y).

    Grid::set_weighted

    fn Grid::set_weighted(self : Grid, x : Int, y : Int, weight : Int) -> Grid

    Sets a positive weighted cell at (x, y); invalid input leaves the grid unchanged.

    Grid::size

    fn Grid::size(self : Grid) -> Int

    Returns the total number of cells in the grid.

    Grid::try_get_cell

    fn Grid::try_get_cell(self : Grid, x : Int, y : Int) -> Cell?

    Returns the cell at (x, y), or None when the coordinates are out of bounds.

    Grid::try_new

    fn Grid::try_new(width : Int, height : Int) -> Grid?

    Creates a Grid, returning None for non-positive or overflowing dimensions.

    Grid::try_set_cell

    fn Grid::try_set_cell(self : Grid, x : Int, y : Int, cell : Cell) -> Grid?

    Tries to set one cell, returning None for invalid coordinates or terrain.

    Grid::try_set_weighted

    fn Grid::try_set_weighted(self : Grid, x : Int, y : Int, weight : Int) -> Grid?

    Tries to set positive weighted terrain and reports invalid input as None.

    Grid::width

    fn Grid::width(self : Grid) -> Int

    Returns the number of columns in the grid.

    Movement

    pub(all) enum Movement {
    FourWay
    EightWay
    } derive(Eq,
    Debug
    )

    Movement mode for neighbor generation.

    Point

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

    A 2D coordinate point on the grid. Origin (0, 0) is top-left; x increases right, y increases down.

    Point::new

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

    Creates a new Point.

    Point::to_string

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

    Returns the string representation "(x, y)".

    SearchOptions

    pub struct SearchOptions {
    movement : Movement
    heuristic : (Point, Point) -> Double
    }

    Configuration for a pathfinding search.

    SearchOptions::default

    fn SearchOptions::default() -> SearchOptions

    Creates default A* options (four-way movement with Manhattan distance).

    SearchOptions::eight_way

    fn SearchOptions::eight_way() -> SearchOptions

    Creates the recommended options for eight-way movement.

    SearchOptions::four_way

    fn SearchOptions::four_way() -> SearchOptions

    Creates the recommended options for four-way movement.

    SearchOptions::new

    fn SearchOptions::new(movement : Movement, heuristic : (Point, Point) -> Double) -> SearchOptions

    Creates custom search options.

    For optimal paths, the heuristic must be deterministic, finite, non-negative, zero at the goal, and admissible for the movement model.

    SearchResult

    pub struct SearchResult {
    path : Array[Point]?
    total_cost : Double
    nodes_visited : Int
    nodes_expanded : Int
    } derive(
    Debug
    )

    Result of a pathfinding search.

    SearchResult::found

    fn SearchResult::found(self : SearchResult) -> Bool

    Returns true if a valid path was found.

    SearchResult::steps

    fn SearchResult::steps(self : SearchResult) -> Int

    Returns the number of moves in the path, or -1 when no path was found.

    astar

    fn astar(grid : Grid, start : Point, goal : Point, options : SearchOptions) -> SearchResult

    Performs A* search to find the shortest path from start to goal using a configurable heuristic.

    A* combines Dijkstra's algorithm with a heuristic function that estimates the remaining distance to the goal. This allows A* to focus the search toward the goal, typically expanding fewer nodes than Dijkstra while still finding optimal paths (if the heuristic is admissible).

    Heuristic

    The heuristic function h(current, goal) must be deterministic, non-negative, zero at the goal, and admissible (never overestimate the true cost) for A* to guarantee optimality. Non-finite and negative values are defensively treated as zero. Common choices:
    • manhattan for four-way movement
    • chebyshev or octile for eight-way movement
    • euclidean for any-direction movement

    Movement

    The options.movement field controls whether four-way or eight-way neighbors are generated, including appropriate move costs.

    Errors

    Returns a failure result if:
    • start or goal is out of bounds
    • start or goal is blocked
    • No path exists

    bfs

    fn bfs(grid : Grid, start : Point, goal : Point) -> SearchResult

    Performs BFS (Breadth-First Search) to find the shortest path on an unweighted grid from start to goal.

    BFS ignores cell weights (all unblocked cells have cost 1) and finds the path with the fewest steps. Uses four-way movement.

    Returns a SearchResult with the path, total steps, and statistics.

    Errors

    Returns a failure result if:
    • start or goal is out of bounds
    • start or goal is blocked
    • No path exists from start to goal

    chebyshev

    fn chebyshev(a : Point, b : Point) -> Double

    Chebyshev distance: max(|dx|, |dy|)

    Admissible for eight-way movement on grid maps where diagonal moves have the same cost as cardinal moves.

    dijkstra

    fn dijkstra(grid : Grid, start : Point, goal : Point) -> SearchResult

    Performs Dijkstra's shortest path search on a weighted grid from start to goal.

    Unlike BFS, Dijkstra respects terrain weights: moving through a Weighted(c) cell costs c instead of 1. This makes it suitable for maps with varying terrain costs.

    Uses a min-heap priority queue (binary heap) to always expand the node with the lowest accumulated cost first.

    Movement

    Uses four-way movement by default. Neighbor generation includes cell costs.

    Errors

    Returns a failure result if:
    • start or goal is out of bounds
    • start or goal is blocked
    • No path exists from start to goal

    dijkstra_with_movement

    fn dijkstra_with_movement(grid : Grid, start : Point, goal : Point, movement : Movement) -> SearchResult

    Performs Dijkstra search with configurable four-way or eight-way movement. Eight-way diagonal movement costs sqrt(2) times the destination terrain cost, matching the movement model used by A*.

    eight_way_neighbors

    fn eight_way_neighbors(grid : Grid, p : Point) -> Array[Point]

    Returns the list of passable neighbors for a point using eight-way movement.

    Includes diagonal moves without cutting through blocked corners. A diagonal destination and both adjacent cardinal cells must be passable.

    eight_way_neighbors_with_cost

    fn eight_way_neighbors_with_cost(grid : Grid, p : Point) -> Array[(Point, Double)]

    Returns the list of passable neighbors as (Point, cost) pairs for eight-way movement.

    Diagonal moves use cost * sqrt(2) for the Euclidean distance. Diagonal corner cutting is not allowed.

    euclidean

    fn euclidean(a : Point, b : Point) -> Double

    Euclidean distance: sqrt(dx^2 + dy^2)

    Admissible for both four-way and eight-way movement on grid maps. More accurate than Manhattan for diagonal movement but slower due to the square root calculation.

    four_way_neighbors

    fn four_way_neighbors(grid : Grid, p : Point) -> Array[Point]

    Returns the list of passable neighbors for a point using four-way movement.

    Skips out-of-bounds cells, blocked cells, and cells with negative cost.

    four_way_neighbors_with_cost

    fn four_way_neighbors_with_cost(grid : Grid, p : Point) -> Array[(Point, Double)]

    Returns the list of passable neighbors as (Point, cost) pairs.

    Useful for weighted pathfinding where different moves may have different traversal costs.

    manhattan

    fn manhattan(a : Point, b : Point) -> Double

    Manhattan distance: |dx| + |dy|

    Admissible for four-way movement on grid maps. This is the most commonly used heuristic for grid-based pathfinding.

    octile

    fn octile(a : Point, b : Point) -> Double

    Octile distance: |dx| + |dy| + (sqrt(2) - 2) * min(|dx|, |dy|)

    Admissible for eight-way movement where diagonal moves cost √2 and cardinal moves cost 1. This is the most accurate admissible heuristic for standard eight-way grid movement.

    zero

    fn zero(_a : Point, _b : Point) -> Double

    Zero heuristic — turns A* into Dijkstra's algorithm.

    Useful as a baseline for comparison. Always admissible but provides no guidance toward the goal.