origami_hexmap 1.8.0
Hexagonal tile maps library for Dojo based games.
The Origami Hexmap crate provides hexagonal tile maps for Dojo-based games: generation,
pathfinding and range queries on bitmaps packed in a single felt252. It is the hexagonal sibling
of origami_map and mirrors its Map API name for name.
Every operation is bit-parallel: a whole board is processed by a few field products and bitwise
operations per step, so a 17x14 cave, its entrance, 10 objects and a path cost about 1.14M gas,
more than 100 times less than the same scenario on origami_map (see Gas).
scarb add origami_hexmap@1.8.0
Or from git, in your [dependencies]:
[dependencies]
origami_hexmap = { git = "https://github.com/dojoengine/origami", tag = "v1.8.0" }
Pointy-top tiles, odd-r offset, row-major: tile (x, y) is bit i = y * W + x of the grid, 1
is walkable. Bit 0 is drawn bottom-right, +1 is West and +W is North, like origami_map. Odd
rows are shifted half a tile toward increasing x (to the left in the drawings). A 5x5 board:
24 23 22 21 20 y = 4
19 18 17 16 15 y = 3
14 13 12 11 10 y = 2
09 08 07 06 05 y = 1
04 03 02 01 00 y = 0
This is HexPrinter's layout: top row first, x = 0 on the right, even rows indented.
NorthWest NorthEast
West tile East
SouthWest SouthEast
Direction
even row
odd row
East
i - 1
i - 1
NorthEast
i + W - 1
i + W
NorthWest
i + W
i + W + 1
West
i + 1
i + 1
SouthWest
i - W
i - W + 1
SouthEast
i - W - 1
i - W
open_with_corridor and open_with_maze open one edge tile
(not a corner) and dig inward. A path may start or end on an open edge tile but never crosses
one; reachable, range, ring and field_of_movement include the open edge tiles on which a
path can end. compute_distribution treats them as any walkable tile.HexMapTrait::new is the raw constructor and checks nothing, like origami_map's: the caller
is responsible for valid dimensions and for a grid without bits at or above W * H, with the
border ring wall except for such entrances. The dimensions and positions are validated by
open_with_corridor, open_with_maze, compute_distribution, search_path,
search_path_weighted, field_of_movement, distance_to, reachable, range, ring and
keep_component (they panic); hex_distance, neighbor and is_walkable check their
positions against W * H only.W, H >= 3 and W * H <= 251, for example 17x14, 16x15, 19x13, 25x10 or 7x7. Boards of at most
128 tiles run on a single u128 limb, about a third cheaper per step.u8.R <= 6 fit in a (2R + 3) x (2R + 3) rectangle (new_hexagon).C columns and R rows is the
pointy-top map with W = R, H = C, tile (col, row) at index col * W + row (transpose).q = x - floor(y / 2), r = y, d = max(|dq|, |dr|, |dq + dr|).use origami_hexmap::{Direction, HexMap, HexMapTrait};
HexMap { width, height, grid, seed } is Copy, Drop and Serde; store grid as a felt252.
The names follow the Rust crate hexx where they apply (distance_to,
range, ring, field_of_movement, neighbor) to ease a later migration.
// From an existing grid, unchecked (see Border ring and entrances)
let map = HexMapTrait::new(grid, 17, 14, seed);
// Every interior tile walkable
let map = HexMapTrait::new_empty(17, 14, seed);
// A maze, order 0 (dense) or 1 (sparse, walls at least 2 thick)
let map = HexMapTrait::new_maze(17, 14, 0, seed);
// A cave: cellular automaton B4/S2, `order` generations, 3 is a good default
let map = HexMapTrait::new_cave(17, 14, 3, seed);
// A random walk of `steps` steps from a random interior tile
let map = HexMapTrait::new_random_walk(17, 14, 500, seed);
// A hexagon of radius 4 in a 11x11 board
let map = HexMapTrait::new_hexagon(4, seed);
let mut map = HexMapTrait::new_cave(17, 14, 3, seed);
// Keep the cave connected to tile 113 (flood fill)
map.keep_component(113);
// Dig a corridor from the edge tile 8 until it touches an open tile
map.open_with_corridor(8, 0);
// Or grow a maze from an edge tile, merged with the open tiles it touches
map.open_with_maze(8, 0);
The digger stops (corridor) or merges (maze) as soon as a dug tile touches an open tile: on hexes, touching means connected.
// 10 distinct walkable tiles, uniform, as a bitmap
let objects: felt252 = map.compute_distribution(10, seed);
// Shortest path, from the target (included) to the start (excluded), empty if unreachable
let path: Span<u8> = map.search_path(8, 202);
// Number of steps of the shortest path (walls block), `None` if unreachable
let steps: Option<u8> = map.distance_to(8, 202);
// Distance on an empty board, walls ignored
let d: u8 = map.hex_distance(8, 202);
// Cheapest path with entry costs
let costs = array![swamps, mountains].span();
let path: Span<u8> = map.search_path_weighted(8, 202, costs);
Cost classes. costs[k] is the bitmap of the tiles whose entry costs k + 2, with at most 3
classes; every other walkable tile costs 1. A tile in several bitmaps takes the highest cost. The
cost is paid on entering a tile, so the start is free. With costs empty, the weighted search is a
plain BFS.
// Every tile reachable from 113
let component: felt252 = map.reachable(113);
// Tiles within 4 steps (walls block), 113 included
let area: felt252 = map.range(113, 4);
// Tiles at exactly 4 steps
let ring: felt252 = map.ring(113, 4);
// Tiles reachable with a movement budget of 6 under the cost classes
let moves: felt252 = map.field_of_movement(113, 6, costs);
// Neighbour, `None` outside the board
let next: Option<u8> = map.neighbor(113, Direction::NorthEast);
let open: bool = map.is_walkable(113);
Asserter: invalid dimension
W < 3, H < 3 or W * H > 251 (constructors, digger, finders, spreader); new_hexagon with a radius above 6
Asserter: position not inside
a finder endpoint, a ring / range centre or a hex_distance position outside the board (position >= W * H)
Asserter: position not an edge
open_with_* from a tile that is not on the edge
Asserter: position is a corner
open_with_* from a corner
Mazer: order > 1 not supported
new_maze or open_with_* with an order above 1
Bfs: position not walkable
search_path, distance_to, reachable, range, ring or keep_component from or to a wall
Dial: position not walkable
search_path_weighted or field_of_movement from or to a wall
Dial: too many costs
more than 3 cost classes
Spreader: not enough place
compute_distribution with more objects than walkable tiles
Spreader: invalid grid
compute_distribution on a grid with a walkable bit outside the board
search_path returns an empty path when the target is unreachable and when from == to;
distance_to returns None and Some(0) respectively. Outside the board
(position >= W * H), neighbor returns None and is_walkable returns false; hex_distance
panics, since a distance has no neutral value.
Endpoints on walls panic. This differs from origami_map, whose search_path returns an
empty path when the start or the target is a wall. In origami_hexmap an empty path only means
"unreachable" (or from == to); test is_walkable first if an endpoint may be a wall.
new_maze, new_cave,
new_random_walk, open_with_corridor, open_with_maze, compute_distribution) has a test
pinning the exact grid of one seed; a change of those outputs is a breaking change.Rng draws bounded integers from a 128-bit pool (one Poseidon
permutation) refilled below 2^64: the draws of one pool are within 2^-56 of independent
uniform draws for bounds up to 251 (2^-54.5 for a shuffle of 6). compute_distribution is
uniform over the count-subsets of the walkable tiles up to these sources (its Poseidon key
words add < 2^-51.3 per call, see GAS.md, L7).origami_security).Sierra gas measured by snforge 0.61.0 (scarb 2.19.4) in bench_map.cairo,
each test including a ~14k test baseline. The facade adds no gas over the library call it forwards
to (identical or lower figures on every function). Details and the per-lot measurements:
GAS.md (in the repository,
not in the package).
new_empty
17x14
18k
new_maze
17x14, order 0 (90 tiles carved)
2.90M
new_cave
17x14, order 3
142k
new_cave
7x7, order 3
81k
new_random_walk
17x14, 200 steps
969k (~4.8k per step)
new_hexagon
radius 6
126k
open_with_corridor
17x14 cave, entrance 8
63k
keep_component / reachable
17x14 cave / maze
536k / 1.11M
keep_component / reachable
7x7 cave
98k
compute_distribution
17x14 cave, 10 objects
193k
search_path
17x14 cave, 24 steps
706k
search_path
7x7, 6 steps
132k
distance_to
17x14 cave, 24 steps
502k
search_path_weighted
17x14 cave, 2 cost classes
1.41M
field_of_movement
17x14 cave, budget 6, 2 classes
254k
range
17x14 cave, radius 4
104k
ring
17x14 cave, radius 4
100k
ring
7x7 cave, radius 2
46k
hex_distance
per call, both positions checked
10.4k
neighbor
per call, position checked
7.0k
is_walkable
per call, position checked
7.1k
Rules of thumb: a BFS layer costs ~19k on a two-limb board and ~10k on a single-limb board, a backtracking step ~8k; a weighted time step ~37k with 2 cost classes.
new_cave(order 3) + keep_component + open_with_corridor + compute_distribution(10) +
search_path from the entrance, against the same scenario on origami_map 18x14 (which has no
keep_component; cairo-test estimate).
new_cave
142k
81k
126.0M
keep_component
182k
55k
-
open_with_corridor
161k
48k
60k
compute_distribution(10)
177k
111k
8.96M
search_path
476k (15 steps)
177k (6 steps)
10.77M (25 steps)
Total
1.14M
471k
145.8M
u252Grids and every bitmap in the API are felt252: free to store, to serialize and to pass around.
The crate also exports u252, an unsigned integer held in one felt252 (every felt is a valid
u252, value set [0, P - 1]):
use origami_hexmap::{U252Trait, u252};
let objects: u252 = map.compute_distribution(10, seed).into(); // free
let raw: felt252 = objects.into(); // free
Use it for counters and stored values that are felts on the wire: conversions, Serde,
StorePacking, ==, wrapping arithmetic and exact shifts cost nothing or less than u256.
Keep u256 (or the felt bitmaps as they are) for set operations and comparisons: &, |, ^,
< and checked + / - split the felt into two limbs first and cost 2x to 3x the u256
operation (checked add 4.2k vs 1.9k, & 6.1k vs 2.4k).
origami_map::hexorigami_map::hex::Hex { col, row } is a coordinate pair without a board. In origami_hexmap, a
tile is an index on a board with a wall ring:
Hex { col, row } becomes LayoutTrait::index(W, col + 1, row + 2): one column and two rows of
margin keep the wall ring and the row parity. Both crates shift odd rows toward increasing
col / x, so the neighbour sets are the same.origami_map::hex labels the vertical neighbours differently
on even and odd rows (NorthEast is row - 1 on odd rows and row + 1 on even rows). Convert
positions, then use the direction table above.hex.neighbors() becomes six neighbor calls, or the bitmap layout.neighbour_mask(index).hex.tiles_within_range(range) (an Array<Hex>, walls ignored) becomes map.range(index, range)
(a bitmap, walls respected); on an empty board they hold the same tiles, as long as they stay
inside the board.hex.is_neighbor(other) becomes map.hex_distance(a, b) == 1.scarb test -p origami_hexmap # runs snforge
Version 1.8.0
Uploaded 12 hours ago
License MIT
Size 65.6 KB
Run the following command in your project dir
scarb add origami_hexmap@1.8.0
Or add the following line to your Scarb.toml
origami_hexmap = "1.8.0"