moonbit-pixelkit

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

game
grid
pathfinding
tilemap
tiled
moon add ttxiangshang/moonbit-pixelkit@0.2.0
Download zip
Version
0.2.0
License
MIT
Last updated
last month
Downloads
15
README

#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