GridMap module
A GridMap is a grid of cells in which each cell holds a number from 0 to 255. It is the map of a game: which tile is drawn in each cell (with draw.tilemap), which cells are walls for the sprites, how to get from one place to another and where each thing is.

This module is especially useful for developing arcade-style games, grid-based puzzles, map simulations, and any application that needs intelligent navigation between points.
use GridMap
What a GridMap is
Cells and values
Each cell is named by its column x and its row y, counted from 0: in a 20 × 15 map, x goes from 0 to 19 and y from 0 to 14. Each cell holds a value from 0 to 255, which means whatever you decide:
| Value | Typical use |
|---|---|
0 | Empty floor / walkable |
1 | Wall / obstacle |
2-254 | Different terrain types or states |
255 | In a tilemap, an empty cell that is not drawn; in a dMap distance map, unreachable |
To draw the map and to make it collide with sprites, the map also needs the pixel size of its tile (tSize) and its position on the screen (x, y). A map you only use to find paths does not need them.
Solids and platforms
The solids and platforms lists say which cell values are walls and which are platforms that only stop from above. They are used by:
- collisions with sprites:
hitsandsprite.hitsGrid; - pathfinding:
path,dMapandnearestgo through the cells that are not insolids.
Toroidal movement (wrap)
When wrapX or wrapY are true, the grid's edges connect:
wrapX: moving right from the last column leads to the first column.wrapY: moving down from the last row leads to the first row.
This is useful for maze games where the character can go through the map edges. It is taken into account by get, collisions, draw.tilemap (which repeats the map) and pathfinding.
map.wrapX = true
map.wrapY = true
var route = map.path(0, 5, 19, 5) // Can wrap around the edges
path, dMap and nearest do not tell the axes apart: as soon as wrapX or wrapY is true, they connect the edges horizontally and vertically. The allowWrap parameter of dMap and nearest does the same. If you only want one axis, close the other one with a row or a column of walls.
Cost
- A grid takes one byte per cell: a 100 × 100 one, about 10 KB.
path,dMapandnearestgo through the whole map (breadth-first search, cost O(width × height)); keep the map size in mind in a game that calls them often.- To send several enemies towards the same thing, compute
dMaponce and calldironce per enemy every frame.
Functions
| Function | What it does | Returns |
|---|---|---|
GridMap.new(width, height) | Creates a map with all the cells at 0 | A GridMap |
GridMap.load(file) | Loads a map from a .gmap file | A GridMap |
The methods and functions of GridMap throw an exception when something fails (an argument of another type, a cell outside the map…), which you catch with try (see Errors and exceptions).
new
The GridMap.new(width, height) function creates a new grid with the specified dimensions. All cells are initialized to 0.
use GridMap
// Create a 20x15 cell grid
var map = GridMap.new(20, 15)
| Parameter | Type | Description |
|---|---|---|
width | integer | Number of columns in the grid |
height | integer | Number of rows in the grid |
The new map has no tile size (tSize() is [0, 0]), no tileset and no solids. With a width or a height of 0 it throws ERR_INVALID_VALUE; with decimals, ERR_TYPE_MISMATCH.
load
The GridMap.load(file) function loads a map from a .gmap file.
var map = GridMap.load("Maps/Level1_cells.gmap")
| Parameter | Type | Description |
|---|---|---|
file | string | Path to the .gmap file |
It returns a GridMap with the file's cells and its tile size. It brings neither the tileset, nor solids, nor platforms: set them yourself.
The IDE creates the .gmap files when you build, from the maps of the tilemap editor: Maps/<map>_tiles.gmap with the tiles and Maps/<map>_cells.gmap with the cell values. What each one holds and what the file looks like is explained in What it saves and what it generates.
| Error | When |
|---|---|
ERR_FILE_NOT_FOUND | The file is not there. |
ERR_INVALID_FORMAT | It is not a .gmap, it is from another version or it lacks cells. |
ERR_INVALID_VALUE | The map in the file is 0 wide or 0 high. |
ERR_TYPE_MISMATCH | The path is not a string. |
Properties
Properties are accessed and assigned directly as fields of the object.
| Property | Type | Default | What for |
|---|---|---|---|
x, y | decimal | 0 | Where cell (0, 0) goes, in pixels |
wrapX, wrapY | yes/no | false | Connecting the edges |
solids | list or nil | nil | The wall values |
platforms | list or nil | nil | The platform values |
tileset | Bitmap or nil | nil | The image of the tiles |
bouncy | yes/no | false | Bouncing against the map |
elasticity | decimal, from 0 to 1 | 0 | How much it bounces |
x / y
Position of the map in screen space, in pixels: where the top left corner of cell (0, 0) goes. It is used by draw.tilemap, draw.scene and collisions with the map; draw.gridBitmap does not use it, since it receives its own position.
map.x = 32
map.y = 16
println(map.x) // 32
| Property | Type | Description |
|---|---|---|
x | float | Horizontal position of the map |
y | float | Vertical position of the map |
To scroll without a scene, change them: with negative values, the map moves left and up. Assigning them something that is not a number throws ERR_INVALID_VALUE.
wrapX / wrapY
Enable toroidal movement (the map's edges connect). When enabled, pathfinding and collision operations take them into account automatically (see Toroidal movement).
map.wrapX = true // Left and right edges connect
map.wrapY = false
println(map.wrapX) // true
| Property | Type | Description |
|---|---|---|
wrapX | boolean | Connects the right edge with the left one |
wrapY | boolean | Connects the bottom edge with the top one |
solids
List of cell values considered solid (obstacles). The path, dMap, nearest, and hits functions use this list automatically when no additional parameters are given.
map.solids = [1, 2, 3] // Tiles 1, 2 and 3 are solid
println(map.solids) // [1, 2, 3]
map.solids = nil // No solids defined
| Property | Type | Description |
|---|---|---|
solids | list | nil | List of solid cell values |
The map keeps the same list you give it, not a copy: if you change it afterwards, the solids change. With something that is neither a list nor nil (an array, for example), it throws ERR_TYPE_MISMATCH.
platforms
List of cell values that act as one-way platforms (they only block from above). Used by hits and by sprite.hitsGrid.
map.platforms = [4]
| Property | Type | Description |
|---|---|---|
platforms | list | nil | List of cell values for one-way platforms |
tileset
Bitmap linked to the map, used by draw.tilemap to automatically render the tiles, and by draw.scene for the scene's map.
var ts = Bitmap.load("Images/tileset.bmp")
map.tileset = ts
println(map.tileset) // <Obj Bitmap>
map.tileset = nil // Unlink
| Property | Type | Description |
|---|---|---|
tileset | Bitmap | nil | Tileset associated with the map |
bouncy / elasticity
They control how sprites bounce when their collisions with the map are resolved through hits.
map.bouncy = true
map.elasticity = 0.8
| Property | Type | Description |
|---|---|---|
bouncy | boolean | Enables bouncing on collision |
elasticity | float | Bounce factor (0.0 = no bounce, 1.0 = full bounce) |
With bouncy = false, the sprite does not bounce: it stops against the wall. With true, the bounce combines the map's elasticity and the sprite's. A value outside 0–1 stays at the limit.
Map methods
| Method | What it does | Returns |
|---|---|---|
set(x, y, value) | Changes the value of a cell | nil |
get(x, y) | Reads the value of a cell | An integer |
width(), height() | The size in cells | An integer |
clear(value) | Sets all the cells to a value | nil |
clone() | Makes a copy | A GridMap |
tSize() / tSize(width, height) | Reads or changes the tile size | [width, height] / nil |
tEmpty() / tEmpty(value) | Reads or changes the tile that is not drawn | An integer / true |
count(value) | How many cells have a value | An integer |
has(value) | Whether any cell has a value | true or false |
find(value) | Where the cells with a value are | A list [x, y, …] |
around(x, y [, use8 [, allowWrap]]) | The values of the neighboring cells | A list |
see(x1, y1, x2, y2, blockers) | Whether there is a line of sight between two cells | true or false |
path(sx, sy, gx, gy) | The shortest path between two cells | A list [x, y, …], or nil |
dMap(targets [, walkable [, allowWrap]]) | A map of distances to some cells | A GridMap |
nearest(targets, sx, sy [, walkable [, allowWrap]]) | The path to the nearest target cell | A list [x, y, …], or nil |
dir(x, y [, use8]) | On a distance map, which way to go | An integer from 0 to 8 |
hits(sprite) | Pushes one or more sprites out of the walls | A list of values |
set
The set(x, y, value) method sets the value of a specific cell.
map.set(5, 3, 1) // Wall
map.set(0, 0, 0) // Empty floor
map.set(2, 0, 5) // Point of interest
| Parameter | Type | Description |
|---|---|---|
x | integer | X coordinate (column) |
y | integer | Y coordinate (row) |
value | integer | Value to assign (0-255) |
| Error | When |
|---|---|
ERR_INVALID_VALUE | value is not between 0 and 255. |
ERR_INDEX_OUT_OF_RANGE | The cell is outside the map. set does not use wrap. |
ERR_TYPE_MISMATCH | An argument is not an integer (decimals too). |
get
The get(x, y) method gets the value of a specific cell.
var value = map.get(5, 3)
println("Value at (5,3): ", value)
| Parameter | Type | Description |
|---|---|---|
x | number | X coordinate (column) |
y | number | Y coordinate (row) |
Returns the integer value (0-255) stored in the cell.
- It accepts decimals, and rounds them down:
get(1.9, 0.2)is cell(1, 0), and-0.5is-1. - With
wrapXorwrapY, a coordinate outside the map wraps around on that axis: in a map 4 wide withwrapX,get(5, 0)isget(1, 0). - Outside the map, without wrap on that axis, it throws
ERR_INDEX_OUT_OF_RANGE.
width / height
The width() and height() methods return the grid dimensions.
println("Width: ", map.width())
println("Height: ", map.height())
clear
The clear(value) method sets all cells to the specified value.
map.clear(0) // Clear the entire map
map.clear(1) // Fill everything with walls
| Parameter | Type | Description |
|---|---|---|
value | integer | Value to assign to all cells (0-255) |
A value outside 0–255 throws ERR_INVALID_VALUE.
draw.tilemap does not see the clearOn Windows, after clear, draw.tilemap keeps drawing the old map until you change a cell with set. Do a set of any cell after the clear, for example map.set(0, 0, map.get(0, 0)).
clone
The clone() method creates a deep copy of the grid.
var copy = map.clone()
copy.set(0, 0, 99)
println(map.get(0, 0)) // Original value unchanged
println(copy.get(0, 0)) // 99
It returns a new GridMap with the same values as the original, and also its position, its tile size, wrap, bounce, empty tile and its tileset (the same image). solids and platforms are copied: changing the copy's does not touch the original's.
tSize
The tSize() / tSize(width, height) method works as a getter/setter for the size in pixels of each tile. It is used by draw.tilemap, draw.scene, hits and sprite.hitsGrid: without it, they neither draw nor collide.
map.tSize(16, 16) // Set 16x16 px tiles
var size = map.tSize() // Get -> [16, 16]
println(size[0], "x", size[1])
| Usage | Parameters | Return |
|---|---|---|
| Getter | none | [tileWidth, tileHeight] (list) |
| Setter | width:integer, height:integer (1-255) | nil |
A new map has [0, 0], and one loaded from a .gmap, the file's. Outside 1–255 it throws ERR_INVALID_VALUE.
tEmpty
The tEmpty() / tEmpty(value) method works as a getter/setter for the empty tile index. Cells with that value are not drawn when rendering the map.
map.tEmpty(0) // Tile 0 is the "empty" one (default: 255)
println(map.tEmpty()) // 0
| Usage | Parameters | Return |
|---|---|---|
| Getter | none | integer (0-255) |
| Setter | value:integer | true |
Cells with 255 are never drawn, whatever tEmpty is.
count
The count(value) method counts how many cells have a specific value.
var wallCount = map.count(1)
println("Walls: ", wallCount)
| Parameter | Type | Description |
|---|---|---|
value | integer | Cell value to search for (0-255) |
Returns an integer with the number of cells that have that value.
has
The has(value) method checks whether at least one cell has the given value.
if (map.has(5)) then
println("There are still dots on the map")
end
| Parameter | Type | Description |
|---|---|---|
value | integer | Cell value to search for (0-255) |
Returns true if at least one cell has that value, false otherwise.
find
The find(value) method searches for all cells with a specific value and returns their coordinates.
var positions = map.find(5) // All cells with value 5
for (var i = 0; i < positions.size(); i += 2)
var x = positions[i]
var y = positions[i + 1]
println("Dot at (", x, ", ", y, ")")
end
| Parameter | Type | Description |
|---|---|---|
value | integer | Cell value to search for (0-255) |
Returns a flat list [x1, y1, x2, y2, ...] with the coordinates of all matches (in row-by-row order). Empty list if none found. It is useful, for example, to place the enemies where the tilemap editor marked their cells.
around
The around(x, y [, use8 [, allowWrap]]) method gets the values of the cells neighboring a position.
var neighbors4 = map.around(5, 3) // 4 cardinal neighbors
var neighbors8 = map.around(5, 3, true) // 8 neighbors (+ diagonals)
var neighborsW = map.around(0, 0, false, true) // 4 neighbors with toroidal wrap
| Parameter | Type | Description |
|---|---|---|
x | integer | X coordinate of the center cell |
y | integer | Y coordinate of the center cell |
use8 | boolean | (Optional) If true, includes the 4 diagonals. Default: false |
allowWrap | boolean | (Optional) If true, edges connect. Default: false |
Returns a list with the values of the valid neighboring cells, in this order: up, down, left, right and, with use8, up-left, up-right, down-left and down-right. Without wrap, the cells outside the bounds are skipped (the list may have fewer than 4/8 elements), so on an edge the position in the list does not tell which neighbor it is. It uses the allowWrap argument, not wrapX or wrapY.
see
The see(x1, y1, x2, y2, blockers) method checks whether there is a clear line of sight between two cells using Bresenham's algorithm.
// Can the player see the ghost? Walls (value 1) block the view
if (map.see(playerX, playerY, ghostX, ghostY, 1)) then
println("The ghost can see you")
end
// Multiple blocking values
if (map.see(playerX, playerY, ghostX, ghostY, [1, 2, 3])) then
println("Line of sight is clear")
end
| Parameter | Type | Description |
|---|---|---|
x1, y1 | integer | Origin coordinates |
x2, y2 | integer | Destination coordinates |
blockers | integer | list | array | Cell values that block the line of sight |
Returns true if the path between origin and destination does not pass through any blocking cell, false otherwise. The origin and destination cells do not count: even if they are walls, they do not block. It does not use wrap.
path
The path(sx, sy, gx, gy) method finds the shortest path between two points, moving in the four directions (BFS).
Pathfinding methods automatically derive walkable cells from the map's solids property (walkable = NOT solid). Toroidal wrap is taken from the wrapX and wrapY properties. If different behavior is needed, dMap and nearest accept optional parameters to manually specify walkable cells and wrap.
use GridMap
var map = GridMap.new(8, 5)
map.solids = [1]
// A small map with two walls
// . . . . . . . .
// . # # # # # . .
// . . . . . . . .
// . . # # # # # .
// . . . . . . . .
for (var x = 1; x <= 5; x++) map.set(x, 1, 1) end
for (var x = 2; x <= 6; x++) map.set(x, 3, 1) end
var route = map.path(0, 0, 7, 4)
if (route != nil) then
println(len(route) div 2, " cells")
println(route)
else
println("No path")
end
12 cells
[0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 7, 1, 7, 2, 7, 3, 7, 4]
| Parameter | Type | Description |
|---|---|---|
sx | integer | Starting X coordinate |
sy | integer | Starting Y coordinate |
gx | integer | Goal X coordinate |
gy | integer | Goal Y coordinate |
Returns a flat list [x0, y0, x1, y1, ..., gx, gy] with the path coordinates, starting with the origin, or nil if no path exists or if the start/goal are not walkable. If the origin is already the goal, the path is that cell: [x, y]. With an origin or a goal outside the map it throws ERR_INDEX_OUT_OF_RANGE, and with decimals, ERR_TYPE_MISMATCH.
Walkable cells are automatically calculated as the complement of map.solids. Make sure to assign map.solids before calling path. Wrap is taken from map.wrapX and map.wrapY.
dMap
The dMap(targets [, walkable [, allowWrap]]) method builds a BFS distance map from all cells that have the target values.
// Distance map to cell type 5 (dots/pellets)
map.solids = [1]
var distances = map.dMap(5)
// Query distance from any position
var dist = distances.get(playerX, playerY)
println("Distance to nearest dot: ", dist)
// With multiple target types
var distances2 = map.dMap([5, 6])
// With manual walkable list and wrap
var distances3 = map.dMap(5, [0, 5, 6], true)
| Parameter | Type | Description |
|---|---|---|
targets | integer | list | array | Cell value(s) from which distance is measured |
walkable | integer | list | array | (Optional) Cell values that can be traversed |
allowWrap | boolean | (Optional) Enables toroidal movement |
Returns a new GridMap where each cell contains the minimum distance to the nearest target, in steps of one cell. Target cells are 0 and unreachable ones, 255. Distances of 254 or more stay at 254. The new map is only a distance map: it has no tile size, tileset or solids.
With a single argument, dMap derives walkable cells from map.solids (walkable = NOT solid) and wrap from map.wrapX/map.wrapY.
nearest
The nearest(targets, sx, sy [, walkable [, allowWrap]]) method finds the shortest path from a position to the nearest cell that has one of the target values.
// Player position: which dot is closest?
map.solids = [1]
var route = map.nearest(5, playerX, playerY)
if (route != nil) then
// The last pair of values is the position of the found target
var n = route.size()
var tx = route[n - 2]
var ty = route[n - 1]
println("Nearest dot at (", tx, ", ", ty, ")")
println("Steps: ", n div 2 - 1)
else
println("No reachable dots")
end
| Parameter | Type | Description |
|---|---|---|
targets | integer | list | array | Cell value(s) to search for |
sx | integer | Starting X coordinate |
sy | integer | Starting Y coordinate |
walkable | integer | list | array | (Optional) Walkable cell values |
allowWrap | boolean | (Optional) Enables toroidal movement |
Returns a flat list [sx, sy, ..., tx, ty] with the path from origin to the nearest target cell, or nil if no target is reachable (also if the origin is a wall). If the origin is already a target, it returns [sx, sy].
dir
The dir(x, y [, use8]) method gets the best direction to follow from a position on a distance map (the result of dMap). It lets you move an agent toward the nearest target without recalculating the full path every frame.
map.solids = [1]
var distances = map.dMap(5) // Distance map to the dots
// Ghost AI: move toward the nearest dot
var dir = distances.dir(ghostX, ghostY)
if (dir == 1) then ghostY -= 1 // Up
elsif (dir == 2) then ghostY += 1 // Down
elsif (dir == 3) then ghostX -= 1 // Left
elsif (dir == 4) then ghostX += 1 // Right
end
| Parameter | Type | Description |
|---|---|---|
x | integer | Current X coordinate |
y | integer | Current Y coordinate |
use8 | boolean | (Optional) If true, also considers diagonals. Default: false |
Returns an integer indicating the optimal direction:
| Value | Direction |
|---|---|
0 | No movement (already at target or unreachable cell) |
1 | Up (y-1) |
2 | Down (y+1) |
3 | Left (x-1) |
4 | Right (x+1) |
5 | Up-left (x-1, y-1) — 8 directions only |
6 | Up-right (x+1, y-1) — 8 directions only |
7 | Down-left (x-1, y+1) — 8 directions only |
8 | Down-right (x+1, y+1) — 8 directions only |
It picks the neighbor with the lowest distance; on a tie, the first one in that order. It does not use wrap: at the edge of the map it does not wrap around. With 8 directions it only looks at the distance of the diagonal cell, not at whether the two cells beside it are walls: it can cut a corner.
hits
The hits(sprite_or_list) method resolves the physical collision between one or more sprites and the map. It reads the solids and platforms properties directly from the GridMap to determine which tiles block and which ones are one-way platforms.
map.solids = [1, 2]
map.platforms = [3]
// Collision with one sprite
var hitTiles = map.hits(player)
// Collision with several sprites at once
var all = [player, enemy1, enemy2]
var hitList = map.hits(all)
| Parameter | Type | Description |
|---|---|---|
sprite_or_list | Sprite | list | Single sprite or list of sprites to resolve |
Returns a list with the values of the tiles it collided with, empty if there are none.
Unlike sprite.hitsGrid, which only asks, hits moves the sprites: it pushes out of the walls those that got into them and takes away their velocity against the wall (or makes them bounce, with bouncy). For that:
- It only works with
DYNAMICbodies. Sprites of other types in the list are skipped; that is why you can pass it the whole list. - The map needs its tile size (
tSize). Without it, it does nothing. - It uses the path from
prevX,prevY: a fast sprite does not go through a thin wall. In a scene,scene.update()keeps them up to date. - Call it every frame, after
scene.update(): the scene's physics moves the bodies, but it does not look at the map (the Scene module explains it). - It sets
onGround()totrueon the sprites the map holds up (sprite.onGround).
It accepts a sprite or a list; with an array or anything else, it throws ERR_TYPE_MISMATCH.
Complete example
A chaser (red) goes after the player (green) through a maze: every six frames it computes with dMap the distances to the player's cell and takes a step with dir. The walls are drawn with rectangles, so it needs no images.
use Display, GridMap
const CELL = 20
// The maze: 1 is wall and 0 is corridor
var rows = [
"111111111111",
"100000010001",
"101111010101",
"100001000101",
"111101111101",
"100000000001",
"101111011101",
"100000010001",
"111111111111"
]
var map = GridMap.new(12, 9)
for (var y = 0; y < 9; y++)
for (var x = 0; x < 12; x++)
if (rows[y][x] == "1") then map.set(x, y, 1) end
end
end
map.solids = [1]
var playerX = 10 // the player, standing still
var playerY = 7
var chaserX = 1 // the chaser
var chaserY = 1
var route = map.path(chaserX, chaserY, playerX, playerY)
println("Shortest path: ", len(route) div 2 - 1, " steps") // 15 steps
// dir() returns 0 (still), 1 up, 2 down, 3 left or 4 right
var dx = [0, 0, 0, -1, 1]
var dy = [0, -1, 1, 0, 0]
Display.open(240, 180)
Display.fps = 30
var draw = Display.draw
for (var f = 0; f < 300; f++) // 10 seconds
if (f % 6 == 0) then // one step every 6 frames
map.set(playerX, playerY, 9) // the player's cell, the dMap target
var dist = map.dMap(9)
map.set(playerX, playerY, 0)
var d = dist.dir(chaserX, chaserY)
chaserX = chaserX + dx[d]
chaserY = chaserY + dy[d]
end
draw.color = 0x3050A0
for (var y = 0; y < 9; y++)
for (var x = 0; x < 12; x++)
if (map.get(x, y) == 1) then
draw.rect(x * CELL, y * CELL, CELL, CELL, true)
end
end
end
draw.color = 0x40FF40
draw.rect(playerX * CELL + 4, playerY * CELL + 4, 12, 12, true)
draw.color = 0xFF4040
draw.rect(chaserX * CELL + 4, chaserY * CELL + 4, 12, 12, true)
Display.update()
end
println("The chaser is at (", chaserX, ", ", chaserY, ")") // (10, 7)
Display.close()
To draw the map with images instead of rectangles, use draw.tilemap (one tile per cell, with a tileset) or draw.gridBitmap (one image on the cells with a value).
Pitfalls and known issues
path,dMapandnearestconnect both axes as soon as one has wrap (see Toroidal movement).- On Windows,
draw.tilemapdoes not see whatclearchanges until the nextset(see clear). setdoes not accept decimals, andgetdoes. If you work out the cell by dividing pixels by the tile size, usedivforset(map.set(px div 16, py div 16, 1)).solidsandplatformskeep your list, not a copy: if you change the list afterwards, it changes in the map too.count,hasandfinddo not check the value: one outside 0–255 wraps around in a byte (count(256)counts the 0s).hitsonly movesDYNAMICbodies, and only with the tile size set (see hits).
All the known issues of JARU, together, are in Known issues.