Sign in

    moonbit-pixelkit

    A MoonBit toolkit for ASCII, CSV, and Tiled JSON grid maps with pathfinding.

    game
    grid
    pathfinding
    tilemap
    tiled
    Download zip
    Version
    0.2.0
    License
    MIT
    Last updated
    2 months ago
    Downloads
    25

    #moonbit-pixelkit

    A MoonBit toolkit for 2D pixel game map parsing and pathfinding.

    moonbit-pixelkit is built for small 2D pixel games, grid-based demos, and algorithm teaching examples. It provides a compact tile map model, ASCII/CSV/Tiled JSON import, walkability queries, Dijkstra reachability, and A* path search.

    #Showcase

    Open the browser showcase:

    showcase.html

    The page renders the same tactical movement preview as a color grid. Click an open tile to preview a new target, switch between the current-turn route and the full route to the goal, or paste an ASCII map into the built-in map lab to validate it and recalculate the preview.

    Run the tactical preview:

    moon run examples/tactical_preview

    It renders a battlefield, the cells an actor can reach this turn, and the planned path:

    moonbit-pixelkit tactical preview Legend: S actor, G goal, # wall, + reachable this turn, * planned path Turn 1 movement range, budget 8: S++++++#...G +####++#.... +++#++..... +##+#+####.. +++#....... ++###..##... +++......... Selected target: (6, 2) Selected target reachable: true This-turn path cost: 8 This-turn path steps: 8 S******#...G +####+*#.... +++#+*..... +##+#+####.. +++#....... ++###..##... +++.........

    #Status

    This project is being developed for the 2026 MoonBit open-source ecosystem contest. The current milestone focuses on a small but usable core package:

    • rectangular ASCII map parsing
    • CSV tile map parsing
    • orthogonal Tiled JSON import with collision and terrain layers
    • structured parse and path errors with row, column, field, layer, point, and step context
    • tile lookup and walkability checks
    • movement costs
    • four-way and eight-way neighbor helpers
    • Dijkstra reachable-area search with accumulated movement costs
    • priority-queue A* path search with weighted-terrain routing
    • start/goal lookup helpers
    • path movement-cost summaries
    • high-level movement previews for tactical target selection
    • ASCII overlays for reachable cells and planned paths
    • game-style examples for pathfinding, turn previews, and tactical movement
    • CI with formatting, moon check, moon build, moon test, and benchmark compilation
    • reproducible A* and Dijkstra performance baselines

    #Acceptance Checklist

    The repository currently includes the minimum assets expected for a reusable MoonBit package:

    • public repository with MIT license
    • MoonBit module metadata in moon.mod
    • runnable examples under examples/
    • unit tests for parser behavior, map queries, weighted reachability, A*, diagonal movement, Tiled JSON import, and error paths
    • GitHub Actions workflow for moon check, moon build, and moon test
    • project proposal document: moonbit-pixelkit-project-proposal.md
    • acceptance guide: ACCEPTANCE.md
    • changelog: CHANGELOG.md
    • browser showcase: showcase.html

    #Quick Start

    Install the package in another MoonBit project:

    moon add ttxiangshang/moonbit-pixelkit

    Then import it from your moon.pkg:

    import {
    "ttxiangshang/moonbit-pixelkit" @pixelkit,
    }

    Run from this repository:

    git clone https://github.com/zhenghao493-netizen/moonbit-pixelkit.git cd moonbit-pixelkit moon check moon build moon test

    Run the checks:

    moon check moon build moon test

    Run the demo:

    moon run cmd/main moon run examples/ascii_maze moon run examples/reachable_area moon run examples/weighted_grid moon run examples/game_loop_stub moon run examples/turn_based_movement moon run examples/tactical_preview moon run examples/tiled_tactical_preview moon run examples/parse_diagnostics

    Build the publish archive:

    moon package

    Run the release-mode performance baseline:

    moon bench benchmarks/pathfinding --release --deny-warn

    See ACCEPTANCE.md for reviewer-oriented verification notes. See CHANGELOG.md for release notes. See PERFORMANCE.md for benchmark methodology and a local baseline. Open showcase.html for a browser-based visual preview.

    #Example

    let source = (
    #|S..
    #|##.
    #|..G
    )

    let map = parse_ascii_map(source).unwrap()
    let path = astar(map, point(0, 0), point(2, 2)).unwrap().unwrap()

    println("path length: \{path.length()}")

    #API Overview

    • point(x, y) creates a Point.
    • parse_ascii_map(text, options~) parses #, ., S, and G maps by default.
    • parse_ascii_map_detailed(text, options~) returns ParseError values with ASCII tile coordinates.
    • parse_csv_map(text) parses CSV tile ids; 1 is treated as a wall.
    • parse_csv_map_detailed(text) returns structured CSV map errors using default options.
    • parse_csv_map_with_options_detailed(text, options) returns structured CSV and option errors.
    • parse_tiled_json(text, options~) imports an orthogonal Tiled JSON map from named collision and optional terrain layers, including unsigned GIDs with flip flags and uncompressed Base64 data.
    • parse_tiled_json_detailed(text, options~) returns structured JSON field, orientation, and layer import errors.
    • tiled_options(...) configures Tiled collision GIDs, terrain layer, and movement-cost mapping.
    • ParseError::message() converts a structured parser error to concise display text while legacy parser APIs continue returning Result[..., String].
    • TileMap::in_bounds(point) checks map bounds.
    • TileMap::tile_at(point) returns a tile when the coordinate is valid.
    • TileMap::is_walkable(point) returns whether movement is allowed.
    • TileMap::movement_cost(point) returns the tile movement cost.
    • TileMap::first_point_with_id(id) returns the first matching tile coordinate.
    • TileMap::single_point_with_id(id) validates that exactly one matching tile exists.
    • TileMap::path_cost(path) sums movement cost after the starting cell.
    • TileMap::path_cost_detailed(path) returns a PathError when a path contains an invalid point.
    • TileMap::validate_path(path, options~) checks for walkable, step-by-step legal routes and returns a path report.
    • TileMap::validate_path_detailed(path, options~) returns structured empty-path, point, and illegal-step errors.
    • TileMap::render_ascii_overlay(reachable=..., path=...) renders movement range and planned paths.
    • neighbors4(point) returns cardinal neighbors.
    • neighbors8(point) returns cardinal plus diagonal neighbors.
    • search_options(allow_diagonal=true) enables diagonal movement while still preventing wall-corner cutting by default; use allow_corner_cutting=true only when that behavior is intended.
    • bfs_reachable(map, start, max_cost, options~) returns reachable cells.
    • bfs_reachable_detailed(map, start, max_cost, options~) returns structured start and movement-budget errors.
    • bfs_reachable_with_costs(map, start, max_cost, options~) uses a Dijkstra frontier to return reachable cells with minimum accumulated movement costs.
    • movement_preview(map, start, target, max_cost, options~) returns range, target reachability, target cost, and target path in one call.
    • movement_preview_detailed(...) returns a PathError for invalid starts, targets, or budgets.
    • astar(map, start, goal, options~) returns a path or None.
    • astar_detailed(map, start, goal, options~) returns structured endpoint errors.

    #Turn-Based Movement Use Case

    moonbit-pixelkit is designed to cover a compact tactical-game loop:

    1. Parse an ASCII, CSV, or Tiled JSON grid.
    2. Find named points such as start and goal.
    3. Compute the cells an actor can reach this turn.
    4. Plan a path to a selected target.
    5. Render a terminal overlay for debugging or examples.

    let map = parse_ascii_map(source).unwrap()
    let actor = map.single_point_with_id("start").unwrap()
    let target = point(4, 1)
    let preview = movement_preview(map, actor, target, 5).unwrap()
    let reachable = preview.reachable_points()
    let path = preview.path().unwrap()

    println("cost: \{preview.path_cost().unwrap()}")
    println("steps: \{map.validate_path(path).unwrap().steps()}")
    println(map.render_ascii_overlay(reachable=reachable, path=path))

    #Tiled JSON Import

    Export an orthogonal map from Tiled using its JSON format with inline tile-layer data. By default, the importer reads a Collision layer and treats gid 1 as a wall. Configure a second terrain layer when individual GIDs carry movement costs:

    let options = tiled_options(
    terrain_layer=Some("Terrain"),
    terrain_costs=[terrain_cost("2", 3), terrain_cost("3", 5)],
    )
    let map = parse_tiled_json(tiled_json, options~).unwrap()

    Run the complete import-to-movement-preview example:

    moon run examples/tiled_tactical_preview

    This intentionally supports the portable core of Tiled JSON: orthogonal maps, named tilelayer entries, native integer data arrays, and uncompressed Base64 little-endian GID data. Infinite maps, chunked layers, compressed Base64 data, TMX, and image assets remain outside the package boundary. Tiled documents both native arrays and optional Base64 layer data in its JSON map format.

    Tiled stores horizontal, vertical, diagonal, and legacy rotation state in the high four bits of an unsigned GID. The importer clears those display flags before matching wall_gids or terrain costs, so configure those options with the base GID such as 1 or 3. This follows Tiled's Global Tile IDs guidance.

    #Roadmap

    • Additional gameplay helpers for turn previews and editor tooling.
    • More package examples after the next Mooncakes release.
    • Broader Tiled import support such as chunked layers and encoded data, based on real user demand.

    #Release

    Version 0.2.0 is published on Mooncakes:

    https://mooncakes.io/docs/ttxiangshang/moonbit-pixelkit

    The v0.2.0 tag matches the current Mooncakes release.

    Local packaging is verified with:

    moon package

    Recommended release tag:

    git tag v0.2.0 git push origin v0.2.0 git push gitlink v0.2.0

    #Development Notes

    Useful verification commands:

    moon check moon build moon test moon run cmd/main moon run examples/ascii_maze moon run examples/reachable_area moon run examples/weighted_grid moon run examples/game_loop_stub moon run examples/turn_based_movement moon run examples/tactical_preview moon package

    Git remotes used during contest development:

    git push origin main git push gitlink main

    #License

    MIT

    AsciiOptions

    pub struct AsciiOptions {
    wall : Char
    floor : Char
    start : Char
    goal : Char
    } derive(Eq,
    Debug
    )

    Options for ASCII map parsing.

    AsciiOptions::default

    fn AsciiOptions::default() -> AsciiOptions

    Default ASCII conventions: # walls, . floors, S start, G goal.

    CsvOptions

    pub struct CsvOptions {
    wall_ids : Array[String]
    terrain_costs : Array[TerrainCost]
    default_cost : Int
    } derive(
    Debug
    )

    Options for CSV map parsing.

    CsvOptions::default

    fn CsvOptions::default() -> CsvOptions

    Default CSV conventions: tile id 1 is a wall and other ids cost 1.

    MovementPreview

    pub struct MovementPreview {
    start : Point
    target : Point
    max_cost : Int
    reachable : Array[ReachableCell]
    target_cost : Int?
    path : Array[Point]?
    } derive(
    Debug
    )

    A complete movement preview for selecting a target in a grid game.

    MovementPreview::max_cost

    fn MovementPreview::max_cost(self : MovementPreview) -> Int

    Return the movement budget used for this preview.

    MovementPreview::path

    Return the planned path when the target is reachable within the budget.

    MovementPreview::path_cost

    fn MovementPreview::path_cost(self : MovementPreview) -> Int?

    Return the planned path cost when the target is reachable within the budget.

    MovementPreview::reachable

    Return reachable cells and their accumulated costs.

    MovementPreview::reachable_points

    fn MovementPreview::reachable_points(self : MovementPreview) -> Array[Point]

    Return only the points from the reachable cells, useful for rendering.

    MovementPreview::start

    Return the movement preview start point.

    MovementPreview::target

    fn MovementPreview::target(self : MovementPreview) -> Point

    Return the selected target point.

    MovementPreview::target_cost

    fn MovementPreview::target_cost(self : MovementPreview) -> Int?

    Return the target's accumulated movement cost when it is reachable.

    MovementPreview::target_reachable

    fn MovementPreview::target_reachable(self : MovementPreview) -> Bool

    Return whether the target can be reached within the movement budget.

    ParseError

    pub enum ParseError {
    EmptyMap
    EmptyFirstRow
    NonRectangularRow(Int, Int, Int)
    UnknownAsciiTile(Int, Int, Char)
    InvalidDefaultCost
    InvalidTerrainCost(String, Int)
    TiledInvalidJson
    TiledRootMustBeObject
    TiledMissingField(String)
    TiledFieldMustBeInteger(String)
    TiledFieldMustBeString(String)
    TiledFieldMustBeIntegerArray(String)
    TiledUnsupportedEncoding(String)
    TiledUnsupportedCompression(String)
    TiledInvalidBase64Data(String)
    TiledBase64SizeMismatch(String, Int, Int)
    TiledInvalidDimensions
    TiledUnsupportedOrientation(String)
    TiledLayersMustBeArray
    TiledLayersMustBeObjects
    TiledLayerNotFound(String)
    TiledLayerNotTileLayer(String)
    TiledLayerSizeMismatch(String, Int, Int)
    } derive(Eq,
    Debug
    )

    A structured failure returned by the detailed map parsers.

    ParseError::message

    fn ParseError::message(self : ParseError) -> String

    Render a structured parser error as a concise user-facing message.

    PathError

    pub enum PathError {
    PathEmpty
    PathPointOutOfBounds(Int, Point)
    PathPointNotWalkable(Int, Point)
    IllegalPathStep(Int, Point, Point)
    NegativeMaxCost(Int)
    StartNotWalkable(Point)
    GoalNotWalkable(Point)
    TargetNotWalkable(Point)
    } derive(Eq,
    Debug
    )

    A structured failure returned by detailed pathfinding and path-validation APIs.

    PathError::message

    fn PathError::message(self : PathError) -> String

    Render a structured path error as a concise user-facing message.

    PathReport

    pub struct PathReport {
    steps : Int
    cost : Int
    } derive(Eq,
    Debug
    )

    A validated movement path with step count and movement cost.

    PathReport::cost

    fn PathReport::cost(self : PathReport) -> Int

    Return the movement cost of the validated path.

    PathReport::steps

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

    Return the number of movement steps in the validated path.

    Point

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

    A point in a 2D tile grid.

    Point::x

    fn Point::x(self : Point) -> Int

    Return the x coordinate.

    Point::y

    fn Point::y(self : Point) -> Int

    Return the y coordinate.

    ReachableCell

    pub struct ReachableCell {
    point : Point
    cost : Int
    } derive(Eq,
    Debug
    )

    A walkable point reached by BFS together with its accumulated movement cost.

    ReachableCell::cost

    fn ReachableCell::cost(self : ReachableCell) -> Int

    Return the accumulated movement cost from the BFS start point.

    ReachableCell::point

    fn ReachableCell::point(self : ReachableCell) -> Point

    Return the reached point.

    SearchOptions

    pub struct SearchOptions {
    allow_diagonal : Bool
    allow_corner_cutting : Bool
    } derive(Eq,
    Debug
    )

    Options shared by path search helpers. Diagonal movement does not cut corners by default.

    SearchOptions::default

    fn SearchOptions::default() -> SearchOptions

    Default path search settings: four-direction movement.

    TerrainCost

    pub struct TerrainCost {
    id : String
    cost : Int
    } derive(Eq,
    Debug
    )

    A per-tile movement cost override for CSV maps.

    Tile

    pub struct Tile {
    id : String
    wall : Bool
    cost : Int
    } derive(Eq,
    Debug
    )

    A parsed tile. wall means the tile blocks movement.

    Tile::cost

    fn Tile::cost(self : Tile) -> Int

    Return the tile movement cost.

    Tile::id

    fn Tile::id(self : Tile) -> String

    Return the tile id.

    Tile::is_wall

    fn Tile::is_wall(self : Tile) -> Bool

    Return whether the tile blocks movement.

    TileMap

    pub struct TileMap {
    width : Int
    height : Int
    tiles : Array[Tile]
    } derive(
    Debug
    )

    A rectangular 2D tile map stored in row-major order.

    TileMap::first_point_with_id

    fn TileMap::first_point_with_id(self : TileMap, id : String) -> Point?

    Return the first point whose tile id equals id, or None when absent.

    TileMap::height

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

    Return the map height.

    TileMap::in_bounds

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

    Return true when p is inside the map rectangle.

    TileMap::is_walkable

    fn TileMap::is_walkable(self : TileMap, p : Point) -> Bool

    Return true when a coordinate is inside the map and not a wall.

    TileMap::movement_cost

    fn TileMap::movement_cost(self : TileMap, p : Point) -> Int?

    Return the movement cost for p, or None for out-of-bounds cells.

    TileMap::path_cost

    fn TileMap::path_cost(self : TileMap, path : Array[Point]) -> Result[Int, String]

    Return the movement cost of a path, excluding the starting cell.

    TileMap::path_cost_detailed

    fn TileMap::path_cost_detailed(self : TileMap, path : Array[Point]) -> Result[Int, PathError]

    Return a path movement cost with structured point-validation errors.

    TileMap::points_with_id

    fn TileMap::points_with_id(self : TileMap, id : String) -> Array[Point]

    Return every point whose tile id equals id.

    TileMap::render_ascii

    fn TileMap::render_ascii(self : TileMap, path? : Array[Point]) -> String

    Render a map as ASCII, optionally overlaying a path with *.

    TileMap::render_ascii_overlay

    fn TileMap::render_ascii_overlay(self : TileMap, reachable? : Array[Point], path? : Array[Point]) -> String

    Render a map with reachable cells marked as + and path cells as *.

    TileMap::single_point_with_id

    fn TileMap::single_point_with_id(self : TileMap, id : String) -> Result[Point, String]

    Return one required point for id.

    TileMap::tile_at

    fn TileMap::tile_at(self : TileMap, p : Point) -> Tile?

    Return the tile at p, or None when the coordinate is out of bounds.

    TileMap::validate_path

    fn TileMap::validate_path(self : TileMap, path : Array[Point], options? : SearchOptions) -> Result[PathReport, String]

    Validate that a path stays walkable and uses legal neighbor-to-neighbor steps.

    TileMap::validate_path_detailed

    fn TileMap::validate_path_detailed(self : TileMap, path : Array[Point], options? : SearchOptions) -> Result[PathReport, PathError]

    Validate a path with structured errors for empty paths, tiles, and steps.

    TileMap::width

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

    Return the map width.

    TiledOptions

    pub struct TiledOptions {
    collision_layer : String
    terrain_layer : String?
    wall_gids : Array[Int]
    terrain_costs : Array[TerrainCost]
    default_cost : Int
    } derive(
    Debug
    )

    Options for importing an orthogonal, uncompressed Tiled JSON map.

    TiledOptions::default

    fn TiledOptions::default() -> TiledOptions

    Default Tiled conventions: a Collision layer with gid 1 as walls.

    astar

    fn astar(map : TileMap, start : Point, goal : Point, options? : SearchOptions) -> Result[Array[Point]?, String]

    Find a path from start to goal with a priority-queue A*. Returns None when unreachable.

    astar_detailed

    fn astar_detailed(map : TileMap, start : Point, goal : Point, options? : SearchOptions) -> Result[Array[Point]?, PathError]

    Find a priority-queue A* path with structured start and goal validation errors.

    bfs_reachable

    fn bfs_reachable(map : TileMap, start : Point, max_cost : Int, options? : SearchOptions) -> Result[Array[Point], String]

    Return all walkable cells reachable from start within max_cost.

    bfs_reachable_detailed

    fn bfs_reachable_detailed(map : TileMap, start : Point, max_cost : Int, options? : SearchOptions) -> Result[Array[Point], PathError]

    Return reachable cells with structured start and budget errors.

    bfs_reachable_with_costs

    fn bfs_reachable_with_costs(map : TileMap, start : Point, max_cost : Int, options? : SearchOptions) -> Result[Array[ReachableCell], String]

    Return all walkable cells reachable from start, including accumulated cost. Uses a Dijkstra frontier for weighted terrain.

    bfs_reachable_with_costs_detailed

    fn bfs_reachable_with_costs_detailed(map : TileMap, start : Point, max_cost : Int, options? : SearchOptions) -> Result[Array[ReachableCell], PathError]

    Return reachable cells and structured start or movement-budget errors.

    csv_options

    fn csv_options(wall_ids? : Array[String], terrain_costs? : Array[TerrainCost], default_cost? : Int) -> CsvOptions

    Create CSV parsing options.

    movement_preview

    fn movement_preview(map : TileMap, start : Point, target : Point, max_cost : Int, options? : SearchOptions) -> Result[MovementPreview, String]

    Build a game-style movement preview for a selected target.

    movement_preview_detailed

    fn movement_preview_detailed(map : TileMap, start : Point, target : Point, max_cost : Int, options? : SearchOptions) -> Result[MovementPreview, PathError]

    Build a movement preview with structured target, start, and budget errors.

    neighbors4

    fn neighbors4(p : Point) -> Array[Point]

    Four-direction neighbors in right, left, down, up order.

    neighbors8

    fn neighbors8(p : Point) -> Array[Point]

    Eight-direction neighbors, including diagonals.

    parse_ascii_map

    fn parse_ascii_map(input : String, options? : AsciiOptions) -> Result[TileMap, String]

    Parse an ASCII tile map. Every row must have the same character length.

    parse_ascii_map_detailed

    fn parse_ascii_map_detailed(input : String, options? : AsciiOptions) -> Result[TileMap, ParseError]

    Parse an ASCII tile map and return structured errors with row and column details.

    parse_csv_map

    fn parse_csv_map(input : String) -> Result[TileMap, String]

    Parse a CSV tile map. 1 is treated as a wall; all other ids are walkable.

    parse_csv_map_detailed

    fn parse_csv_map_detailed(input : String) -> Result[TileMap, ParseError]

    Parse a default CSV tile map and return structured errors.

    parse_csv_map_with_options

    fn parse_csv_map_with_options(input : String, options : CsvOptions) -> Result[TileMap, String]

    Parse a CSV tile map with configurable wall ids and terrain costs.

    parse_csv_map_with_options_detailed

    fn parse_csv_map_with_options_detailed(input : String, options : CsvOptions) -> Result[TileMap, ParseError]

    Parse a CSV tile map and return structured parser and option errors.

    parse_tiled_json

    fn parse_tiled_json(input : String, options? : TiledOptions) -> Result[TileMap, String]

    Parse an orthogonal, uncompressed Tiled JSON map with collision and terrain layers.

    parse_tiled_json_detailed

    fn parse_tiled_json_detailed(input : String, options? : TiledOptions) -> Result[TileMap, ParseError]

    Parse a Tiled JSON map and return structured import errors.

    point

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

    Create a point.

    search_options

    fn search_options(allow_diagonal? : Bool, allow_corner_cutting? : Bool) -> SearchOptions

    Create search options. Set allow_corner_cutting only for games that permit diagonal wall cutting.

    terrain_cost

    fn terrain_cost(id : String, cost : Int) -> TerrainCost

    Create a CSV terrain cost override.

    tiled_options

    fn tiled_options(collision_layer? : String, terrain_layer? : String?, wall_gids? : Array[Int], terrain_costs? : Array[TerrainCost], default_cost? : Int) -> TiledOptions

    Create Tiled JSON import options for collision and optional terrain layers.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io