← hex_field · API reference · version 0.1.7
9 file(s), 3343 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
HexSet struct | 58 | — | 77 81 85 91 93 95 … (35 total) |
VecMap struct | 68 | — | 429 545 549 570 577 591 … (7 total) |
hexset_chunk fn | 77 | — | 86 859 894 1095 1189 1245 … (32 total) |
hexset_new fn | 85 | — | 318 366 388 12 22 34 … (8 total) |
hexset_q0 fn | 91 | — | 736 749 772 785 795 805 … (21 total) |
hexset_r0 fn | 93 | — | 736 749 773 785 795 805 … (21 total) |
hexset_w fn | 95 | — | 749 759 1046 1047 1058 1068 … (17 total) |
hexset_h fn | 97 | — | 749 760 1046 1047 1058 1068 … (16 total) |
hexset_get fn | 108 | — | 413 416 439 445 785 1074 … (26 total) |
hexset_set fn | 116 | — | 321 333 374 399 912 1114 … (34 total) |
hexset_count fn | 127 | — | 14 25 37 72 39 118 … (50 total) |
lattice_k fn | 133 | — | 370 391 440 446 1006 1021 … (34 total) |
lattice_m fn | 135 | — | 371 392 441 446 1007 1021 … (34 total) |
nb_q fn | 138 | — | 416 443 830 1139 1220 1328 … (21 total) |
nb_r fn | 150 | — | 416 444 830 1140 1221 1329 … (21 total) |
Heights struct | 168 | — | 178 181 184 192 198 717 … (19 total) |
heights_new fn | 178 | — | 860 895 1096 1190 1245 25 … (30 total) |
height_set fn | 192 | — | 921 1115 1203 1272 1409 29 … (12 total) |
height_get fn | 198 | — | 795 1115 1203 1272 45 33 … (12 total) |
Labels struct | 212 | — | 222 225 228 236 242 718 … (19 total) |
labels_new fn | 222 | — | 861 896 1097 1191 1245 26 … (31 total) |
label_set fn | 236 | — | 930 1116 1204 1273 1410 30 … (16 total) |
label_get fn | 242 | — | 805 1116 1204 1273 47 31 … (13 total) |
hex_at fn | 253 | HXL-006 | 223 |
corner_k fn | 282 | — | 449 451 |
corner_m fn | 293 | — | 450 452 |
form_hexdisk fn | 317 | — | 188 189 190 |
hexdisk_into fn | 330 | — | 13 23 35 69 49 |
hex_dist fn | 339 | — | 321 333 |
form_circle fn | 365 | HXL-005 | 192 193 194 197 198 199 |
form_octagon fn | 387 | HXL-005 | 202 203 |
wall_count fn | 409 | — | none in this package |
trace fn | 429 | HXL-001 | 24 36 70 15 65 89 … (11 total) |
shoelace2 fn | 549 | — | 572 579 95 96 99 100 |
outline_count fn | 570 | HXL-003 | 629 130 137 140 163 |
shoelace_total fn | 577 | HXL-002 | 612 25 72 15 101 102 … (12 total) |
validate fn | 591 | HXL-004 | 37 39 129 136 164 174 |
HXF_MAGIC const | 685 | — | 770 872 102 135 147 161 … (14 total) |
HXF_SCHEMA const | 686 | — | 771 874 103 147 161 96 … (12 total) |
HXF_HEADER const | 687 | — | 869 891 |
TAG_OCCU const | 689 | — | 779 907 111 165 100 99 … (9 total) |
TAG_HGHT const | 690 | — | 791 916 |
TAG_LABL const | 691 | — | 801 925 |
TAG_EDGE const | 692 | — | 820 934 102 |
TAG_LAYR const | 699 | — | 840 942 101 |
HXF_OK const | 703 | — | 858 36 65 87 116 127 … (18 total) |
HXF_BAD_MAGIC const | 704 | — | 872 362 |
HXF_BAD_SCHEMA const | 705 | — | 874 139 367 |
HXF_BAD_RESERVED const | 706 | — | 881 152 372 |
HXF_BAD_SECTION const | 707 | — | 908 917 926 936 946 169 … (9 total) |
HXF_TRUNCATED const | 708 | — | 869 902 905 185 192 394 |
HXF_MISSING_OCCU const | 709 | — | 964 406 |
HXF_BAD_EXTENT const | 710 | — | 890 891 446 452 |
HexDoc struct | 714 | — | 856 857 |
doc_write fn | 732 | — | 34 63 66 85 178 45 … (7 total) |
doc_write_edges fn | 744 | HXL-012 | 736 67 82 136 486 499 |
doc_write_all fn | 755 | — | 748 77 |
doc_read fn | 856 | HXL-011 | 35 64 86 115 127 138 … (33 total) |
ROT_OK const | 987 | — | 1048 1059 141 145 |
ROT_NOT_MULTIPLE const | 988 | — | 1248 131 134 |
lattice_to_cell fn | 992 | — | 1015 1021 1270 1296 1332 1333 … (15 total) |
lattice_rot60 fn | 999 | — | 1011 33 |
cell_rot fn | 1005 | — | 1075 1113 1149 1150 49 52 … (10 total) |
cell_mirror fn | 1020 | — | 1173 1201 1227 1228 74 75 |
Stencil struct | 1027 | — | 1043 1044 1055 1056 1066 1165 … (19 total) |
stencil_from fn | 1043 | — | 1244 12 244 40 277 |
stencil_with_edges fn | 1054 | — | 1094 1188 184 15 254 |
stencil_rotate fn | 1066 | HXL-007 | 1192 1224 1251 62 196 342 … (12 total) |
stencil_mirror fn | 1165 | HXL-007 | 208 212 340 266 |
stencil_rotate_deg fn | 1242 | — | 130 133 140 144 |
stencil_stamp fn | 1257 | — | 1285 1348 89 103 104 105 … (14 total) |
stencil_stamp_layers fn | 1281 | — | 1313 67 |
stencil_stamp_all fn | 1309 | — | 300 327 |
stencil_stamp_edges fn | 1344 | — | 54 |
stencil_unstamp fn | 1394 | — | 1425 |
stencil_unstamp_layers fn | 1421 | — | 1456 |
stencil_unstamp_all fn | 1452 | — | 304 328 |
EdgeSet struct | 1521 | — | 719 745 756 1031 1054 1310 … (27 total) |
EDGE_MAT_MAX const | 1538 | — | 1531 1598 328 332 |
edgeset_new fn | 1541 | HXL-008 | 736 862 897 1046 1103 1192 … (17 total) |
edge_key fn | 1554 | HXL-008 | 19 20 297 298 |
edge_set_mat fn | 1590 | HXL-009 | 1334 1369 1475 1652 181 14 … (15 total) |
edgeset_refused fn | 1606 | HXL-009 | 330 336 342 |
edge_mat fn | 1610 | — | 830 1141 1222 1330 1365 1472 … (22 total) |
edge_set_surf fn | 1631 | — | 1651 |
edge_surf fn | 1642 | — | 1142 1223 261 |
edge_set_both fn | 1649 | — | 1151 1230 253 |
edgeset_surf_count fn | 1657 | — | 255 262 |
edgeset_equal fn | 1667 | — | 263 267 271 502 |
edgeset_digest fn | 1682 | — | none in this package |
edgeset_bytes fn | 1697 | — | none in this package |
edgeset_q0 fn | 1700 | — | none in this package |
edgeset_r0 fn | 1702 | — | none in this package |
edgeset_w fn | 1704 | — | 483 |
edgeset_h fn | 1706 | — | none in this package |
edgeset_count_all fn | 1712 | — | 193 198 199 210 211 302 … (15 total) |
edgeset_count fn | 1722 | — | 30 35 38 39 55 71 … (13 total) |
Layers struct | 1753 | — | 720 756 1032 1282 1310 1422 … (22 total) |
layers_new fn | 1764 | — | 749 863 898 1047 1058 1107 … (12 total) |
layer_count fn | 1772 | — | 766 837 1108 1117 1197 1205 … (15 total) |
layer_name fn | 1774 | — | 838 1108 1197 1290 1298 1437 |
layers_q0 fn | 1776 | — | none in this package |
layers_r0 fn | 1778 | — | none in this package |
layers_w fn | 1780 | — | none in this package |
layers_h fn | 1782 | — | none in this package |
layer_index fn | 1786 | — | 1797 1815 1822 82 83 |
layer_add fn | 1796 | — | 948 1108 1197 1290 185 8 … (11 total) |
layer_set fn | 1814 | — | 1299 1438 186 11 12 18 … (9 total) |
layer_get fn | 1821 | — | 1299 202 314 13 14 15 … (15 total) |
layer_get_at fn | 1828 | — | 846 1118 1206 |
layer_set_at fn | 1835 | — | 952 1118 1206 |
src/hex_field.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_field — hex cell sets -> an EXACT integer vector map.
//
// A consumer's Python oracle checks the traced output against golden JSON byte-for-byte,
// which is possible because the whole representation is INTEGER:
//
// every hex centre AND corner lies on the lattice x = k*sqrt(3)/2 , y = m/2
// with centre(q,r) = (2q + (r&1), 3r) and corners at (0,+-2), (+-1,+-1).
//
// So there is no float anywhere in the geometry, no epsilon compare, and the area
// round-trip is exact integer arithmetic:
//
// sum of the integer shoelace over all loops == 12 * (number of hexes)
//
// A single hex shoelaces to exactly 12; holes contribute negative. Cell count and traced
// outline therefore cannot disagree — that is what makes the emitted vector map safe to
// hand to a 2D renderer or a mesh routine.
//
// Storage follows the survival guide: a flat pre-allocated vector indexed by a packed
// key (no tuple-keyed sets, no capture-append-reassign), and loops are held as flat
// coordinate arrays plus a start index, not vector<vector<>>.
//
// ── Using it ─────────────────────────────────────────────────────────────────
//
// Four steps, and every routine below serves one of them: make a chunk, fill cells,
// trace, read the loops.
//
// s = hexset_chunk(-2, -2, 5, 5); // 1. a bounded window of hex coords
// hexdisk_into(s, 0, 0, 1); // 2. fill it — a disc of radius 1 at (0,0)
// hexset_set(s, 0, 0, false); // …and punch the middle out
//
// v = trace(s); // 3. -> VecMap: closed integer loops
// for i in 0..v.vm_loops { // 4. loop i is [vm_start[i], vm_start[i+1])
// for j in (v.vm_start[i] ?? 0)..(v.vm_start[i + 1] ?? 0) {
// // (v.vm_k[j], v.vm_m[j]) is a lattice point: x = k*sqrt(3)/2, y = m/2
// }
// }
//
// The output is what a renderer or a mesher wants — closed outlines, holes included,
// with no float to disagree about. Scale is applied by the CONSUMER at the last
// moment (see the README), so the same map serves a minimap and a mesh.
//
// The rest of the surface is the same four steps at more resolutions: `Heights` and
// `Labels` attach a value per cell, `EdgeSet` and `Layers` attach one per edge, and
// the `hexset_*`/`edgeset_*`/`layers_*` readers are how a consumer asks a chunk what
// window it covers without knowing how it is stored.
//
// Worked examples: `tests/07-worked-examples.loft` (@HXL-001 … @HXL-005) — start at
// @HXL-001, which writes down a single hex's six lattice points in full.
// Occupancy over a BOUNDED CHUNK of hex coords: q in [hs_q0 .. hs_q0+hs_w),
// r in [hs_r0 .. hs_r0+hs_h). Deliberately a chunk, not a global graph — every
// routine here is O(chunk) and never walks an unbounded neighbourhood, so the same
// code serves a 32x32 world chunk (plan #2) as serves a one-off tower window.
// Anything spanning chunks is traced per chunk (with a halo) and stitched; nothing
// here assumes the form is centred, or that the world is reachable.
pub struct HexSet {
hs_q0: integer,
hs_r0: integer,
hs_w: integer,
hs_h: integer,
hs_cells: vector<boolean>,
hs_count: integer,
}
// A vector map: loops concatenated, loop i occupying [vm_start[i], vm_start[i+1]).
pub struct VecMap {
pub vm_k: vector<integer>,
pub vm_m: vector<integer>,
pub vm_start: vector<integer>,
pub vm_loops: integer,
}
// A chunk anchored at (q0,r0), w x h cells. THE primitive — everything else builds
// on it, so no routine can accidentally depend on a global frame.
pub fn hexset_chunk(q0: integer, r0: integer, w: integer, h: integer) -> HexSet {
cells: vector<boolean> = [];
n = w * h;
for _ in 0..n { cells += [false]; }
HexSet { hs_q0: q0, hs_r0: r0, hs_w: w, hs_h: h, hs_cells: cells, hs_count: 0 }
}
// Convenience: a window centred on the origin, radius `w`.
pub fn hexset_new(w: integer) -> HexSet {
hexset_chunk(0 - w, 0 - w, 2 * w + 1, 2 * w + 1)
}
// Step 1's window, read back. A consumer that did not build the chunk asks it where it
// starts and how far it runs, rather than carrying those four numbers alongside it.
pub fn hexset_q0(s: HexSet) -> integer { s.hs_q0 }
// The window's first `r`.
pub fn hexset_r0(s: HexSet) -> integer { s.hs_r0 }
// How many `q` columns the window spans.
pub fn hexset_w(s: HexSet) -> integer { s.hs_w }
// How many `r` rows the window spans.
pub fn hexset_h(s: HexSet) -> integer { s.hs_h }
fn hs_index(s: HexSet, q: integer, r: integer) -> integer {
dq = q - s.hs_q0;
dr = r - s.hs_r0;
if dq < 0 || dq >= s.hs_w || dr < 0 || dr >= s.hs_h { return -1; }
dr * s.hs_w + dq
}
// Step 2 read: is this cell filled? Outside the window is `false`, not an error — a
// caller may probe a neighbour that lies past the edge without guarding every call.
pub fn hexset_get(s: HexSet, q: integer, r: integer) -> boolean {
i = hs_index(s, q, r);
if i < 0 { return false; }
s.hs_cells[i] ?? false
}
// Step 2 write: fill or clear one cell. Clearing an interior cell is how a hole is made,
// and `trace` will emit it as its own negative loop. Outside the window it does nothing.
pub fn hexset_set(s: HexSet, q: integer, r: integer, on: boolean) {
i = hs_index(s, q, r);
if i < 0 { return; }
had = s.hs_cells[i] ?? false;
s.hs_cells[i] = on;
if on && !had { s.hs_count = s.hs_count + 1; }
if !on && had { s.hs_count = s.hs_count - 1; }
}
// How many cells are filled. The cheap half of the area round-trip: `12 * hexset_count`
// is what the traced outline must shoelace to, so this is the number a check compares.
pub fn hexset_count(s: HexSet) -> integer { s.hs_count }
// ── lattice + adjacency (integers only) ──────────────────────────────────────
// Hex (q,r) -> exact integer lattice centre. Matches hex_grid::hex_to_px scaled by
// (sqrt(3)/2, 1/2) — verified against the library before porting.
pub fn lattice_k(q: integer, r: integer) -> integer { 2 * q + (r & 1) }
// The lattice `m` of hex (q,r) — the y half of the pair `lattice_k` gives the x of.
pub fn lattice_m(_q: integer, r: integer) -> integer { 3 * r }
// Neighbour (q,r) in direction d, odd-r offset. Same SET as hex_grid::hex_neighbor.
pub fn nb_q(q: integer, r: integer, d: integer) -> integer {
odd = r & 1;
if d == 0 { return q + 1; }
if d == 1 { return q - 1; }
if d == 2 { if odd == 1 { return q + 1; } return q; }
if d == 3 { if odd == 1 { return q; } return q - 1; }
if d == 4 { if odd == 1 { return q + 1; } return q; }
if odd == 1 { return q; }
q - 1
}
// The neighbour's `r`, the companion of `nb_q`. Walk both to step across the field.
pub fn nb_r(_q: integer, r: integer, d: integer) -> integer {
if d == 0 { return r; }
if d == 1 { return r; }
if d == 2 { return r - 1; }
if d == 3 { return r - 1; }
if d == 4 { return r + 1; }
r + 1
}
// ── HEIGHTS ─────────────────────────────────────────────────────────────────
//
// A per-CELL height, chunk-indexed exactly like a HexSet. Everything before stairs got by
// with height on the material (how tall a wall is); a staircase is the first thing that
// needs the GROUND itself to have a z, because each step is a different floor.
//
// It is deliberately a separate structure rather than a field on HexSet: most of the world
// is flat and should not pay for a height it never reads, and a height field is derived
// (from a way, a contour, a terrace) exactly as the edge field is.
pub struct Heights {
hz_q0: integer,
hz_r0: integer,
hz_w: integer,
hz_h: integer,
hz_z: vector<float>,
}
// A height per cell over the same window shape a HexSet uses, so the two index alike.
// Attach one when the field needs elevation as well as occupancy; every cell starts 0.0.
pub fn heights_new(q0: integer, r0: integer, w: integer, h: integer) -> Heights {
z: vector<float> = [];
for _ in 0..w * h { z += [0.0]; }
Heights { hz_q0: q0, hz_r0: r0, hz_w: w, hz_h: h, hz_z: z }
}
fn hz_index(f: Heights, q: integer, r: integer) -> integer {
dq = q - f.hz_q0;
dr = r - f.hz_r0;
if dq < 0 || dr < 0 || dq >= f.hz_w || dr >= f.hz_h { return -1; }
dr * f.hz_w + dq
}
// Set one cell's height. Outside the window it does nothing, matching `hexset_set`.
pub fn height_set(f: Heights, q: integer, r: integer, z: float) {
i = hz_index(f, q, r);
if i >= 0 { f.hz_z[i] = z; }
}
// One cell's height, or 0.0 outside the window — probe a neighbour without guarding.
pub fn height_get(f: Heights, q: integer, r: integer) -> float {
i = hz_index(f, q, r);
if i < 0 { return 0.0; }
f.hz_z[i] ?? 0.0
}
// ── LABELS ──────────────────────────────────────────────────────────────────
//
// A per-CELL integer, chunk-indexed exactly like Heights. Heights answers "how high is
// this cell"; Labels answers "whose is it". Plan #9 (canopy-first trees) needs the second
// because a canopy is a PARTITION under competition rather than a stamped footprint — the
// assignment is the output, not a tie-break.
//
// -1 means unowned, which is a real state: a cell no crown reaches belongs to nobody.
pub struct Labels {
lb_q0: integer,
lb_r0: integer,
lb_w: integer,
lb_h: integer,
lb_v: vector<integer>,
}
// An integer label per cell over a HexSet-shaped window: region ids, materials, owners.
// Unset cells read -1, which is why -1 is not a usable label.
pub fn labels_new(q0: integer, r0: integer, w: integer, h: integer) -> Labels {
v: vector<integer> = [];
for _ in 0..w * h { v += [-1]; }
Labels { lb_q0: q0, lb_r0: r0, lb_w: w, lb_h: h, lb_v: v }
}
fn lb_index(f: Labels, q: integer, r: integer) -> integer {
dq = q - f.lb_q0;
dr = r - f.lb_r0;
if dq < 0 || dr < 0 || dq >= f.lb_w || dr >= f.lb_h { return -1; }
dr * f.lb_w + dq
}
// Label one cell. Outside the window it does nothing.
pub fn label_set(f: Labels, q: integer, r: integer, v: integer) {
i = lb_index(f, q, r);
if i >= 0 { f.lb_v[i] = v; }
}
// One cell's label, or -1 when unset or outside the window.
pub fn label_get(f: Labels, q: integer, r: integer) -> integer {
i = lb_index(f, q, r);
if i < 0 { return -1; }
f.lb_v[i] ?? -1
}
// World point -> the cell that contains it. The exact inverse of the centre map
// (lattice_k / lattice_m), by the standard axial-fractional + cube-round route. Gated two
// ways in sighttest: it must round-trip every cell of a chunk, and it must agree cell for
// cell with hex_grid::px_to_hex, which is the canonical convention this whole plan follows.
// Example: @HXL-006
pub fn hex_at(px: float, py: float) -> (integer, integer) {
qf = 0.5773502691896258 * px - 0.3333333333333333 * py;
rf = 0.6666666666666666 * py;
cx = qf;
cz = rf;
cy = 0.0 - cx - cz;
rx = round(cx);
ry = round(cy);
rz = round(cz);
dx = abs(rx - cx);
dy = abs(ry - cy);
dz = abs(rz - cz);
if dx > dy && dx > dz {
rx = 0.0 - ry - rz;
} else if dy > dz {
ry = 0.0 - rx - rz;
} else {
rz = 0.0 - rx - ry;
}
aq = rx as integer;
ar = rz as integer;
(aq + ((ar - (ar & 1)) / 2 ?? 0), ar)
}
// Corner i of a cell, as an offset from its lattice centre. Ordered so consecutive
// corners bound one edge; index matches the Python CORNER table exactly.
// CANONICAL corner order — this is hex_grid::hex_corner_offset / worldmesh.loft's
// hex_lat_offset (the watertight instanced floor depends on it), in exact lattice
// units. Do NOT invent another ordering: downstream code reads corner i by index.
pub fn corner_k(i: integer) -> integer {
if i == 0 { return 0; }
if i == 1 { return -1; }
if i == 2 { return -1; }
if i == 3 { return 0; }
if i == 4 { return 1; }
1
}
// Corner i's lattice `m` offset from the centre — the companion of `corner_k`. Together
// they are the six offsets every traced vertex is built from, in the order `trace` emits.
pub fn corner_m(i: integer) -> integer {
if i == 0 { return 2; }
if i == 1 { return 1; }
if i == 2 { return -1; }
if i == 3 { return -2; }
if i == 4 { return -1; }
1
}
// Which corner pair (i, i+1) is the edge shared with the neighbour at lattice delta?
fn edge_of(dk: integer, dm: integer) -> integer {
if dk == -1 && dm == 3 { return 0; }
if dk == -2 && dm == 0 { return 1; }
if dk == -1 && dm == -3 { return 2; }
if dk == 1 && dm == -3 { return 3; }
if dk == 2 && dm == 0 { return 4; }
if dk == 1 && dm == 3 { return 5; }
-1
}
// ── candidate placement routines ─────────────────────────────────────────────
// Steps 1 and 2 in one call: a window of radius `w` holding a disc of radius `n`. The
// quickest way to get something traceable when the shape itself is not the point.
pub fn form_hexdisk(w: integer, n: integer) -> HexSet {
s = hexset_new(w);
for r in s.hs_r0..s.hs_r0 + s.hs_h {
for q in s.hs_q0..s.hs_q0 + s.hs_w {
if hex_dist(0, 0, q, r) <= n { hexset_set(s, q, r, true); }
}
}
s
}
// Fill a hex-distance disk centred anywhere INTO an existing chunk. Cells outside the
// chunk are simply not written — the caller decides the halo. This is the chunk-local
// form of form_hexdisk and the one a world chunk actually uses.
pub fn hexdisk_into(s: HexSet, cq: integer, cr: integer, n: integer) {
for r in s.hs_r0..s.hs_r0 + s.hs_h {
for q in s.hs_q0..s.hs_q0 + s.hs_w {
if hex_dist(cq, cr, q, r) <= n { hexset_set(s, q, r, true); }
}
}
}
// Hex distance on odd-r offset, via cube coords.
pub fn hex_dist(q1: integer, r1: integer, q2: integer, r2: integer) -> integer {
x1 = q1 - ((r1 - (r1 & 1)) / 2 ?? 0);
z1 = r1;
y1 = 0 - x1 - z1;
x2 = q2 - ((r2 - (r2 & 1)) / 2 ?? 0);
z2 = r2;
y2 = 0 - x2 - z2;
dx = x1 - x2; if dx < 0 { dx = 0 - dx; }
dy = y1 - y2; if dy < 0 { dy = 0 - dy; }
dz = z1 - z2; if dz < 0 { dz = 0 - dz; }
m = dx;
if dy > m { m = dy; }
if dz > m { m = dz; }
m
}
// Euclidean disk: the hex CENTRE must lie within `radius` world units of the origin.
// Compared in squared lattice units to keep it integer until the final test.
//
// ⚠ `radius` IS NOT IN HEX STEPS. A world unit here is the one the lattice defines —
// x = k·√3/2, y = m/2 — in which a hex has CIRCUMRADIUS 1 and two neighbouring centres
// are √3 ≈ 1.732 apart. So `form_circle(w, 3.0)` is 13 cells while `form_hexdisk(w, 3)`
// is 37: the same 3, two units, a factor of √3 between them. To ask for n hex steps,
// pass `n * 1.7320508075688772` — `form_circle(w, 2·√3)` and `form_hexdisk(w, 2)` are
// the same 19 cells.
// Example: @HXL-005
pub fn form_circle(w: integer, radius: float) -> HexSet {
s = hexset_new(w);
lim = radius * radius;
for r in s.hs_r0..s.hs_r0 + s.hs_h {
for q in s.hs_q0..s.hs_q0 + s.hs_w {
k = lattice_k(q, r);
m = lattice_m(q, r);
x = (k as float) * 0.8660254037844386;
y = (m as float) * 0.5;
if x * x + y * y <= lim + 0.000000001 { hexset_set(s, q, r, true); }
}
}
s
}
// Regular octagon, apothem `a`, one flat side on the world x-axis (canonical
// orientation — an octagon is 8-fold symmetric and the grid is 6-fold, so the
// orientation is FROZEN, not free).
//
// `apothem` is in the same lattice world units as `form_circle`'s radius — hex
// circumradius 1, so a hex STEP is √3 — not in hex steps.
// Example: @HXL-005
pub fn form_octagon(w: integer, apothem: float) -> HexSet {
s = hexset_new(w);
for r in s.hs_r0..s.hs_r0 + s.hs_h {
for q in s.hs_q0..s.hs_q0 + s.hs_w {
x = (lattice_k(q, r) as float) * 0.8660254037844386;
y = (lattice_m(q, r) as float) * 0.5;
if x < 0.0 { x = 0.0 - x; }
if y < 0.0 { y = 0.0 - y; }
d = x;
if y > d { d = y; }
e = (x + y) * 0.7071067811865476;
if e > d { d = e; }
if d <= apothem + 0.000000001 { hexset_set(s, q, r, true); }
}
}
s
}
// ── the tracer: cells -> exact integer loops ─────────────────────────────────
// How many filled cells touch an empty one or the window edge — the form's perimeter in
// cells. Useful before tracing, when a cost or a bound depends on the boundary size.
pub fn wall_count(s: HexSet) -> integer {
n = 0;
for r in s.hs_r0..s.hs_r0 + s.hs_h {
for q in s.hs_q0..s.hs_q0 + s.hs_w {
if hexset_get(s, q, r) {
edge = false;
for d in 0..6 {
if !hexset_get(s, nb_q(q, r, d), nb_r(q, r, d)) { edge = true; }
}
if edge { n = n + 1; }
}
}
}
n
}
// Trace the boundary into ordered closed loops. Each boundary edge is emitted directed
// corner_i -> corner_i+1, which gives every loop a consistent orientation (outer one
// way, enclosed holes the other) — what a fill rule and a triangulator both need.
// Example: @HXL-001
pub fn trace(s: HexSet) -> VecMap {
// collect directed edges as parallel arrays
ak: vector<integer> = [];
am: vector<integer> = [];
bk: vector<integer> = [];
bm: vector<integer> = [];
used: vector<boolean> = [];
ne = 0;
for r in s.hs_r0..s.hs_r0 + s.hs_h {
for q in s.hs_q0..s.hs_q0 + s.hs_w {
if hexset_get(s, q, r) {
k0 = lattice_k(q, r);
m0 = lattice_m(q, r);
for d in 0..6 {
nq = nb_q(q, r, d);
nr = nb_r(q, r, d);
if !hexset_get(s, nq, nr) {
i = edge_of(lattice_k(nq, nr) - k0, lattice_m(nq, nr) - m0);
if i >= 0 {
j = i + 1; if j > 5 { j = 0; }
ak += [k0 + corner_k(i)];
am += [m0 + corner_m(i)];
bk += [k0 + corner_k(j)];
bm += [m0 + corner_m(j)];
used += [false];
ne = ne + 1;
}
}
}
}
}
}
// TERMINATION. The walk below is unambiguous only because every boundary vertex has
// exactly ONE outgoing edge — a real property of hex grids (each vertex touches 3
// MUTUALLY ADJACENT cells, so a boundary cannot pinch at a vertex the way it can on a
// square grid). Relying on that silently means a bad input HANGS instead of failing,
// so the walk carries an explicit budget: every edge is consumed at most once, so no
// more than `ne` steps can ever be legitimate.
vk: vector<integer> = [];
vm: vector<integer> = [];
st: vector<integer> = [];
nloops = 0;
done = 0;
budget = ne + 1;
while done < ne {
// find an unused edge to start a loop
seed = -1;
for e in 0..ne { if seed < 0 && !(used[e] ?? true) { seed = e; } }
if seed < 0 { done = ne; }
if seed >= 0 {
st += [len(vk)];
nloops = nloops + 1;
sk = ak[seed] ?? 0;
sm = am[seed] ?? 0;
ck = sk;
cm = sm;
going = true;
while going {
budget = budget - 1;
if budget < 0 { going = false; done = ne; }
// find the unused edge leaving (ck,cm)
pick = -1;
for e in 0..ne {
if pick < 0 && !(used[e] ?? true) && (ak[e] ?? 0) == ck && (am[e] ?? 0) == cm {
pick = e;
}
}
if pick < 0 { going = false; }
if pick >= 0 && budget >= 0 {
used[pick] = true;
done = done + 1;
vk += [ck];
vm += [cm];
ck = bk[pick] ?? 0;
cm = bm[pick] ?? 0;
if ck == sk && cm == sm { going = false; }
}
}
}
}
st += [len(vk)];
// CANONICAL START: rotate each loop to begin at its lexicographically smallest
// vertex. A traced cycle is the same shape whichever edge seeded it, so the raw
// start is an artefact of iteration order and two correct implementations disagree
// on it. Canonicalising makes the vector map UNIQUE — which is what lets the golden
// JSON act as a language-neutral contract. (Found by this port disagreeing with the
// python oracle on the start vertex while every point and the area matched.)
for li in 0..nloops {
a = st[li] ?? 0;
b = st[li + 1] ?? 0;
n = b - a;
if n > 1 {
best = 0;
for cj in 1..n {
bk2 = vk[a + best] ?? 0;
bm2 = vm[a + best] ?? 0;
jk = vk[a + cj] ?? 0;
jm = vm[a + cj] ?? 0;
if jk < bk2 || (jk == bk2 && jm < bm2) { best = cj; }
}
if best > 0 {
rk: vector<integer> = [];
rm: vector<integer> = [];
for rj in 0..n {
idx = best + rj; if idx >= n { idx = idx - n; }
rk += [vk[a + idx] ?? 0];
rm += [vm[a + idx] ?? 0];
}
for wj in 0..n {
vk[a + wj] = rk[wj] ?? 0;
vm[a + wj] = rm[wj] ?? 0;
}
}
}
}
VecMap { vm_k: vk, vm_m: vm, vm_start: st, vm_loops: nloops }
}
// Twice the signed area of loop i, in integer lattice units. Exact.
pub fn shoelace2(v: VecMap, i: integer) -> integer {
a = v.vm_start[i] ?? 0;
b = v.vm_start[i + 1] ?? 0;
n = b - a;
s = 0;
for j in 0..n {
j2 = j + 1; if j2 >= n { j2 = 0; }
k1 = v.vm_k[a + j] ?? 0;
m1 = v.vm_m[a + j] ?? 0;
k2 = v.vm_k[a + j2] ?? 0;
m2 = v.vm_m[a + j2] ?? 0;
s = s + k1 * m2 - k2 * m1;
}
s
}
// How many loops enclose area — one per connected component of the cell set. Exposed
// because `validate` deliberately no longer insists on exactly one (see there): a caller
// that knows its form is a single piece can still assert `outline_count(v) == 1`, and a
// caller tracing a world chunk can ask how many pieces landed in it.
// Example: @HXL-003
pub fn outline_count(v: VecMap) -> integer {
n = 0;
for i in 0..v.vm_loops { if shoelace2(v, i) > 0 { n = n + 1; } }
n
}
// Example: @HXL-002
pub fn shoelace_total(v: VecMap) -> integer {
t = 0;
for i in 0..v.vm_loops { t = t + shoelace2(v, i); }
t
}
// ── the validator — what must hold before a renderer/mesh may consume it ─────
// Returns 0 when the vector map is safe to consume; otherwise a failure code:
// 1 no loops 2 degenerate loop 3 segment is not a hex edge
// 4 area round-trip failed 5 no outer loop at all
// 6 a vertex repeats (the outline touches itself)
// Example: @HXL-003
// Example: @HXL-004
pub fn validate(v: VecMap, cells: integer) -> integer {
if v.vm_loops <= 0 { return 1; }
for i in 0..v.vm_loops {
a = v.vm_start[i] ?? 0;
b = v.vm_start[i + 1] ?? 0;
n = b - a;
if n < 3 { return 2; }
for j in 0..n {
j2 = j + 1; if j2 >= n { j2 = 0; }
dk = (v.vm_k[a + j2] ?? 0) - (v.vm_k[a + j] ?? 0);
dm = (v.vm_m[a + j2] ?? 0) - (v.vm_m[a + j] ?? 0);
ok = false;
if dk == 0 && dm == 2 { ok = true; }
if dk == 0 && dm == -2 { ok = true; }
if dk == 1 && dm == 1 { ok = true; }
if dk == 1 && dm == -1 { ok = true; }
if dk == -1 && dm == 1 { ok = true; }
if dk == -1 && dm == -1 { ok = true; }
if !ok { return 3; }
}
}
if shoelace_total(v) != 12 * cells { return 4; }
// AT LEAST one outer loop — not exactly one.
//
// This read `outer != 1`, which is a property of a CONNECTED form, and this package
// traces BOUNDED CHUNKS on purpose: two buildings in one 32x32 window, or one form the
// chunk edge cuts in half, are the ordinary case and not an error. Every such map was
// refused with code 5 while `trace` had produced it correctly and the area round-trip
// agreed exactly — two disjoint hexes give two loops summing to 12 x 2, and were called
// invalid. A map with no positive loop at all is still nonsense, and that is what is
// left here.
//
// What the old rule caught as a side effect, and this does not: a hole wound the SAME
// way as its outer loop, compensated by an extra outer loop, so the areas still sum.
// Deciding that needs each loop's NESTING, not a count of signs — `trace` cannot emit
// it (a boundary edge is directed from the cell side, so the winding is not a free
// choice), so it can only arrive in a map built somewhere else. A caller that KNOWS
// its form is connected keeps the stronger property with `outline_count(v) == 1`.
outer = outline_count(v);
if outer < 1 { return 5; }
// NO REPEATED VERTEX, across the whole map rather than within a loop: a point visited
// twice is a pinch, and a triangulator cannot resolve one.
//
// This has to be CHECKED rather than assumed precisely because `trace` cannot produce
// it — a hex vertex touches three MUTUALLY ADJACENT cells, so a boundary never pinches,
// and every map this package traces is simple. The maps that reach `validate` from a
// file, or from a consumer's own builder, are the ones that can: two hexes emitted as
// two circuits joined at their shared corners give twelve legal hex-edge segments whose
// shoelace is exactly 12 x 2, so every other check here passes it.
nv = v.vm_start[v.vm_loops] ?? 0;
for i in 0..nv {
for j in (i + 1)..nv {
if (v.vm_k[i] ?? 0) == (v.vm_k[j] ?? 0) && (v.vm_m[i] ?? 0) == (v.vm_m[j] ?? 0) {
return 6;
}
}
}
0
}
// ═══════════════════════════════════════════════════════════════════════
// hex_doc — the interchange format for a hex field: a world, or a stencil.
//
// The SHARED artifact. A field written by one consumer must load in another
// bit-for-bit, so `load(save(x)) == x` is not a private test — it is the
// interface, and both sides gate it.
//
// offset size field type notes
// -------------------------------------------------------------------
// 0 4 magic i32 'HXF1'
// 4 4 schema_version i32 1
// 8 4 q0 i32 extent origin
// 12 4 r0 i32
// 16 4 w i32 extent size, cells
// 20 4 h i32
// 24 4 section_count i32
// 28 4 reserved i32 must be 0
//
// then section_count sections, each:
// 0 4 tag i32 'OCCU' | 'HGHT' | 'LABL' | …
// 4 4 byte_len i32 payload size
// 8 byte_len payload
//
// SECTIONS, not a flags word. A reader that meets a tag it does not know
// SKIPS it by byte_len and keeps going, so a field written by a newer consumer
// still loads in an older one. A flags word cannot do that: an unknown bit
// means an array of unknown length, which is unskippable, so the only correct
// response would be to refuse the whole file. That difference is testable, and
// hex_doctest tests it.
//
// Explicit dimensions are load-bearing. A format that infers its extent from
// its payload cannot detect truncation; this one checks every section's length
// against w·h and refuses a mismatch.
pub const HXF_MAGIC = 0x31465848; // 'HXF1' little-endian
pub const HXF_SCHEMA = 1;
pub const HXF_HEADER = 32;
pub const TAG_OCCU = 0x5543434F; // 'OCCU'
pub const TAG_HGHT = 0x54484748; // 'HGHT'
pub const TAG_LABL = 0x4C42414C; // 'LABL'
pub const TAG_EDGE = 0x45474445; // 'EDGE' — the wall layer
// 'LAYR' — one NAMED integer layer. Payload: name_len i32, the name's bytes,
// then w*h i32 values. One section per layer, so a file carries as many as the
// author made and a reader that wants none skips them all by their lengths.
// TAG_LABL predates this and is the unnamed primary layer; it stays because the
// committed fixtures are written with it, and because "the material" needing no
// name is the common case.
pub const TAG_LAYR = 0x5259414C;
// Refusal codes. 0 = loaded. Every one of these is a REFUSAL, never a
// partial parse: a half-read field is worse than no field.
pub const HXF_OK = 0;
pub const HXF_BAD_MAGIC = 1;
pub const HXF_BAD_SCHEMA = 2;
pub const HXF_BAD_RESERVED = 3;
pub const HXF_BAD_SECTION = 4; // section length disagrees with w·h
pub const HXF_TRUNCATED = 5;
pub const HXF_MISSING_OCCU = 6;
pub const HXF_BAD_EXTENT = 7; // w or h impossible for a file this size
// What a load produced. `doc_code` is the refusal code; the field members are
// meaningful only when it is HXF_OK.
pub struct HexDoc {
pub doc_code: integer,
pub doc_cells: HexSet,
pub doc_heights: Heights,
pub doc_labels: Labels,
pub doc_edges: EdgeSet,
pub doc_layers: Layers,
pub doc_has_h: boolean,
pub doc_has_l: boolean,
pub doc_has_e: boolean,
pub doc_skipped: integer, // unknown sections skipped — the forward-compat count
}
// ── writing ──────────────────────────────────────────────────────────────
// Write a field. `heights` / `labels` are optional: pass has_h/has_l false to
// leave the section out entirely rather than writing a zero-filled one, so a
// stencil that only carries occupancy costs only occupancy.
pub fn doc_write(
path: text, s: HexSet, hz: Heights, lb: Labels,
has_h: boolean, has_l: boolean
) {
doc_write_edges(path, s, hz, lb, edgeset_new(hexset_q0(s), hexset_r0(s), 1, 1),
has_h, has_l, false);
}
// As doc_write, plus the wall layer. The EDGE payload is the halo grid — gw*gh*3
// slots — because an edge on the chunk boundary belongs to a cell outside it, and
// dropping those would silently open the walls of every stamped building's rim.
// Example: @HXL-012
pub fn doc_write_edges(
path: text, s: HexSet, hz: Heights, lb: Labels, eg: EdgeSet,
has_h: boolean, has_l: boolean, has_e: boolean
) {
doc_write_all(path, s, hz, lb, eg,
layers_new(hexset_q0(s), hexset_r0(s), hexset_w(s), hexset_h(s)),
has_h, has_l, has_e);
}
// The full form: cells, optional heights/labels/edges, and any number of NAMED
// integer layers.
pub fn doc_write_all(
path: text, s: HexSet, hz: Heights, lb: Labels, eg: EdgeSet, ly: Layers,
has_h: boolean, has_l: boolean, has_e: boolean
) {
w = hexset_w(s);
h = hexset_h(s);
n = w * h;
sections = 1;
if has_h { sections = sections + 1; }
if has_l { sections = sections + 1; }
if has_e { sections = sections + 1; }
sections = sections + layer_count(ly);
f = file(path);
f#format = LittleEndian;
f += (HXF_MAGIC as i32);
f += (HXF_SCHEMA as i32);
f += (hexset_q0(s) as i32);
f += (hexset_r0(s) as i32);
f += (w as i32);
f += (h as i32);
f += (sections as i32);
f += (0 as i32);
f += (TAG_OCCU as i32);
f += (n as i32);
for q in 0..w {
for r in 0..h {
// `on` is 0 or 1 by construction, but the cast is checked anyway —
// a narrowing cast that "cannot fail" is exactly the kind that does.
on = if hexset_get(s, hexset_q0(s) + q, hexset_r0(s) + r) { 1 } else { 0 };
f += ((on as u8?) ?? 0 as u8);
}
}
if has_h {
f += (TAG_HGHT as i32);
f += ((n * 8) as i32);
for q in 0..w {
for r in 0..h {
f += height_get(hz, hexset_q0(s) + q, hexset_r0(s) + r);
}
}
}
if has_l {
f += (TAG_LABL as i32);
f += ((n * 4) as i32);
for q in 0..w {
for r in 0..h {
f += (label_get(lb, hexset_q0(s) + q, hexset_r0(s) + r) as i32);
}
}
}
if has_e {
// Written through `edge_mat`, keyed by the two cells an edge lies between, rather
// than by copying `eg`'s vector. The section's LENGTH comes from `s`, so an EdgeSet
// built at a different extent has a different stride, and a raw copy then lands every
// material on the wrong edge — a file that loads cleanly and describes other
// geometry. Keying by cells makes the bytes independent of how the source layer is
// laid out, and byte-identical to the old ones whenever the extents agree (which the
// committed `walled.hxf` fixture is the gate for).
gw = w + 2;
gh = h + 2;
f += (TAG_EDGE as i32);
f += ((gw * gh * 3) as i32);
for dr in 0..gh {
for dq in 0..gw {
cq = hexset_q0(s) + dq - 1;
cr = hexset_r0(s) + dr - 1;
for sl in 0..3 {
d = 0;
if sl == 1 { d = 2; }
if sl == 2 { d = 3; }
mv = edge_mat(eg, cq, cr, nb_q(cq, cr, d), nb_r(cq, cr, d));
f += ((mv as u8?) ?? 0);
}
}
}
}
for li in 0..layer_count(ly) {
nm = layer_name(ly, li);
nlen = size(nm);
f += (TAG_LAYR as i32);
f += ((4 + nlen + n * 4) as i32);
f += (nlen as i32);
f += nm;
for q in 0..w {
for r in 0..h {
f += (layer_get_at(ly, li, hexset_q0(s) + q, hexset_r0(s) + r) as i32);
}
}
}
}
// ── reading ──────────────────────────────────────────────────────────────
// Example: @HXL-010
// Example: @HXL-011
pub fn doc_read(path: text) -> HexDoc {
d = HexDoc {
doc_code: HXF_OK,
doc_cells: hexset_chunk(0, 0, 1, 1),
doc_heights: heights_new(0, 0, 1, 1),
doc_labels: labels_new(0, 0, 1, 1),
doc_edges: edgeset_new(0, 0, 1, 1),
doc_layers: layers_new(0, 0, 1, 1),
doc_has_h: false, doc_has_l: false, doc_has_e: false, doc_skipped: 0,
};
f = file(path);
f#format = LittleEndian;
total = f#size;
if total < HXF_HEADER { d.doc_code = HXF_TRUNCATED; return d; }
magic = f#read as i32;
if magic != HXF_MAGIC { d.doc_code = HXF_BAD_MAGIC; return d; }
schema = f#read as i32;
if schema != HXF_SCHEMA { d.doc_code = HXF_BAD_SCHEMA; return d; }
q0 = f#read as i32;
r0 = f#read as i32;
w = f#read as i32;
h = f#read as i32;
nsec = f#read as i32;
rsv = f#read as i32;
if rsv != 0 { d.doc_code = HXF_BAD_RESERVED; return d; }
// THE EXTENT IS CHECKED AGAINST THE FILE, not taken on trust. It is the pair of
// fields that decides how much this reader allocates, and it was the only part of the
// header nothing verified: a 32-byte file claiming 4000x4000 built sixteen million
// cells — plus heights, labels, edges and layers over the same extent — and only then
// reported a missing section. OCCU is mandatory and costs one byte per cell, so a
// file can never describe more cells than it has bytes; that is the bound, and it
// needs no second pass to apply.
if w < 0 || h < 0 { d.doc_code = HXF_BAD_EXTENT; return d; }
if w * h > total - HXF_HEADER { d.doc_code = HXF_BAD_EXTENT; return d; }
n = w * h;
d.doc_cells = hexset_chunk(q0, r0, w, h);
d.doc_heights = heights_new(q0, r0, w, h);
d.doc_labels = labels_new(q0, r0, w, h);
d.doc_edges = edgeset_new(q0, r0, w, h);
d.doc_layers = layers_new(q0, r0, w, h);
seen_occu = false;
for _ in 0..nsec {
if f#next + 8 > total { d.doc_code = HXF_TRUNCATED; return d; }
tag = f#read as i32;
blen = f#read as i32;
if f#next + blen > total { d.doc_code = HXF_TRUNCATED; return d; }
if tag == TAG_OCCU {
if blen != n { d.doc_code = HXF_BAD_SECTION; return d; }
for q in 0..w {
for r in 0..h {
v = f#read as u8;
hexset_set(d.doc_cells, q0 + q, r0 + r, v != 0);
}
}
seen_occu = true;
} else if tag == TAG_HGHT {
if blen != n * 8 { d.doc_code = HXF_BAD_SECTION; return d; }
for q in 0..w {
for r in 0..h {
hv = f#read as float;
height_set(d.doc_heights, q0 + q, r0 + r, hv);
}
}
d.doc_has_h = true;
} else if tag == TAG_LABL {
if blen != n * 4 { d.doc_code = HXF_BAD_SECTION; return d; }
for q in 0..w {
for r in 0..h {
lv = f#read as i32;
label_set(d.doc_labels, q0 + q, r0 + r, lv);
}
}
d.doc_has_l = true;
} else if tag == TAG_EDGE {
egn = (w + 2) * (h + 2) * 3;
if blen != egn { d.doc_code = HXF_BAD_SECTION; return d; }
for i in 0..egn {
ev = f#read as u8;
d.doc_edges.eg_mat[i] = ev;
}
d.doc_has_e = true;
} else if tag == TAG_LAYR {
nlen = f#read as i32;
// the declared section length must account for the name AND the values —
// a name length that disagrees with it is a corrupt file, not a short read
if blen != 4 + nlen + n * 4 { d.doc_code = HXF_BAD_SECTION; return d; }
nm = f#read(nlen) as text;
li = layer_add(d.doc_layers, nm);
for q in 0..w {
for r in 0..h {
lv = f#read as i32;
layer_set_at(d.doc_layers, li, q0 + q, r0 + r, lv);
}
}
} else {
// Unknown section: skip it by its declared length. This is the whole
// reason sections carry a length, and why a newer writer does not break
// an older reader.
f#next = f#next + blen;
d.doc_skipped = d.doc_skipped + 1;
}
}
if !seen_occu { d.doc_code = HXF_MISSING_OCCU; return d; }
d
}
// ═══════════════════════════════════════════════════════════════════════
// stencils — a reusable piece of authored field, stamped at an orientation.
//
// A stencil is a SMALL FIELD, not a bitmap: the same structures a world holds,
// at a smaller extent. So stamping is MERGING TWO FIELDS, and needs no new
// conflict rule.
//
// Rotation is why this is cheap. On the exact-integer lattice a 60° turn is an
// integer map — k' = (k − m)/2, m' = (3k + m)/2 — and both halves divide evenly
// for every cell centre, because centres satisfy k ≡ m (mod 2). So a stencil
// rotates with NO resampling and NO drift: a house stamped at 300° is the same
// house as at 0°, cell for cell, not a filtered approximation of it. Reflection
// (k → −k) is exact too, giving 12 orientations.
//
// Translation happens in LATTICE space, never in (q, r). Adding (dq, dr) to an
// odd-r offset coordinate is NOT a translation — it changes the row stagger, so
// a stencil stamped on an odd row would come out sheared. Adding lattice
// vectors is exact, and closed: k₁≡m₁ and k₂≡m₂ give k₁+k₂ ≡ m₁+m₂.
pub const ROT_OK = 0;
pub const ROT_NOT_MULTIPLE = 1;
// Inverse of (lattice_k, lattice_m). m is always 3r and k − (r&1) always even,
// so both divisions are exact rather than truncating.
pub fn lattice_to_cell(k: integer, m: integer) -> (integer, integer) {
lr = (m / 3) ?? 0;
lq = ((k - (lr & 1)) / 2) ?? 0;
(lq, lr)
}
// One 60° step counter-clockwise about the lattice origin.
pub fn lattice_rot60(k: integer, m: integer) -> (integer, integer) {
(((k - m) / 2) ?? 0, ((3 * k + m) / 2) ?? 0)
}
// Rotate a cell n × 60° about cell (0,0). n may be any integer; six steps are
// the identity, so it wraps.
pub fn cell_rot(q: integer, r: integer, n: integer) -> (integer, integer) {
rk = lattice_k(q, r);
rm = lattice_m(q, r);
steps = n % 6;
if steps < 0 { steps = steps + 6; }
for _ in 0..steps {
(nk, nm) = lattice_rot60(rk, rm);
rk = nk;
rm = nm;
}
lattice_to_cell(rk, rm)
}
// Reflect a cell in the vertical axis (k → −k). With the six rotations this
// gives the 12 orientations handed content needs — an L-shaped house, a stair.
pub fn cell_mirror(q: integer, r: integer) -> (integer, integer) {
lattice_to_cell(0 - lattice_k(q, r), lattice_m(q, r))
}
// A stencil: a set of cells with optional heights, labels and edges (`sn_has_*` say which
// are present) and its named layers. `stencil_from` builds one; `stencil_rotate` and
// `stencil_mirror` transform it.
pub struct Stencil {
pub sn_cells: HexSet,
pub sn_heights: Heights,
pub sn_labels: Labels,
pub sn_edges: EdgeSet,
pub sn_layers: Layers,
sn_has_h: boolean,
sn_has_l: boolean,
sn_has_e: boolean,
pub sn_code: integer,
}
// Build the field FIRST, then construct the stencil: these take their arguments
// by value, so cells set on the HexSet afterwards do not reach the stencil. The
// failure is quiet — the stencil reports the right layer count and extent, and
// only the contents are missing.
pub fn stencil_from(s: HexSet, hz: Heights, lb: Labels, has_h: boolean, has_l: boolean) -> Stencil {
Stencil {
sn_cells: s, sn_heights: hz, sn_labels: lb,
sn_edges: edgeset_new(hexset_q0(s), hexset_r0(s), hexset_w(s), hexset_h(s)),
sn_layers: layers_new(hexset_q0(s), hexset_r0(s), hexset_w(s), hexset_h(s)),
sn_has_h: has_h, sn_has_l: has_l, sn_has_e: false, sn_code: ROT_OK,
}
}
// A stencil that carries walls. Separate constructor so the common case — a
// footprint with no walls — does not allocate an edge layer it never reads.
pub fn stencil_with_edges(s: HexSet, hz: Heights, lb: Labels, eg: EdgeSet,
has_h: boolean, has_l: boolean) -> Stencil {
Stencil {
sn_cells: s, sn_heights: hz, sn_labels: lb, sn_edges: eg,
sn_layers: layers_new(hexset_q0(s), hexset_r0(s), hexset_w(s), hexset_h(s)),
sn_has_h: has_h, sn_has_l: has_l, sn_has_e: true, sn_code: ROT_OK,
}
}
// Rotate a whole stencil n × 60° about its own (0,0), re-fitting the extent to
// whatever the rotated cells need.
// Example: @HXL-007
pub fn stencil_rotate(st: Stencil, n: integer) -> Stencil {
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
// pass 1 — where do the occupied cells land?
seen = false;
mnq = 0; mxq = 0; mnr = 0; mxr = 0;
for iq in 0..w {
for ir in 0..h {
if hexset_get(st.sn_cells, q0 + iq, r0 + ir) {
(tq, tr) = cell_rot(q0 + iq, r0 + ir, n);
if !seen { mnq = tq; mxq = tq; mnr = tr; mxr = tr; seen = true; }
if tq < mnq { mnq = tq; }
if tq > mxq { mxq = tq; }
if tr < mnr { mnr = tr; }
if tr > mxr { mxr = tr; }
}
}
}
// NO EARLY `return st`. A stencil with no occupied cells has no bounding box to fit, so
// it comes back unturned at its own extent — expressed as a zero-turn copy rather than by
// handing the PARAMETER back, because a function whose return paths disagree about
// ownership (parameter on one, freshly built on the other) leaks the fresh one, one store
// per call, on both backends: loft#982. The leak fires on the path that IS taken, so the
// guard had to go even though rotating an empty stencil is the rare case.
turns = n;
if !seen { mnq = q0; mnr = r0; mxq = q0 + w - 1; mxr = r0 + h - 1; turns = 0; }
nw = mxq - mnq + 1;
nh = mxr - mnr + 1;
out = stencil_with_edges(
hexset_chunk(mnq, mnr, nw, nh),
heights_new(mnq, mnr, nw, nh),
labels_new(mnq, mnr, nw, nh),
// The CELL extent, not a pre-haloed one: `edgeset_new` adds the one-cell halo
// itself (`gw = w + 2`), which is exactly the reach a rim edge needs — its owner
// is at most one cell outside. Passing `(mnq - 1, nw + 2)` here halos it twice,
// and `stencil_from` does not, so two stencils holding the same walls would
// disagree on `eg_gw` and `edgeset_equal` could never compare them.
edgeset_new(mnq, mnr, nw, nh),
st.sn_has_h, st.sn_has_l
);
out.sn_has_e = st.sn_has_e;
out.sn_layers = layers_new(mnq, mnr, nw, nh);
for li in 0..layer_count(st.sn_layers) { layer_add(out.sn_layers, layer_name(st.sn_layers, li)); }
// pass 2 — carry the payload across
for iq in 0..w {
for ir in 0..h {
if hexset_get(st.sn_cells, q0 + iq, r0 + ir) {
(tq, tr) = cell_rot(q0 + iq, r0 + ir, turns);
hexset_set(out.sn_cells, tq, tr, true);
if st.sn_has_h { height_set(out.sn_heights, tq, tr, height_get(st.sn_heights, q0 + iq, r0 + ir)); }
if st.sn_has_l { label_set(out.sn_labels, tq, tr, label_get(st.sn_labels, q0 + iq, r0 + ir)); }
for li in 0..layer_count(st.sn_layers) {
layer_set_at(out.sn_layers, li, tq, tr, layer_get_at(st.sn_layers, li, q0 + iq, r0 + ir));
}
}
}
}
// Edges rotate because their key is the doubled midpoint of two centres, and
// the midpoint of two rotated centres is the rotation of the midpoint. So
// rotating both endpoints and re-keying is exact — no resampling, no drift.
if st.sn_has_e {
// Walk the HALO grid, not the cell extent. An edge is stored against one of
// its two cells, and for a rim edge that owner is OUTSIDE the extent — so a
// loop over the extent silently drops it. A stencil must never lose data,
// so this iterates every stored slot rather than every occupied cell.
for iq in -1..(w + 1) {
for ir in -1..(h + 1) {
cq = q0 + iq;
cr = r0 + ir;
for sl in 0..3 {
d = 0;
if sl == 1 { d = 2; }
if sl == 2 { d = 3; }
nq = nb_q(cq, cr, d);
nr = nb_r(cq, cr, d);
mv = edge_mat(st.sn_edges, cq, cr, nq, nr);
sv = edge_surf(st.sn_edges, cq, cr, nq, nr);
// BOTH slots, because a rotation is a transform of this stencil in memory —
// the surface ids still point into the same table they did before the turn.
// Carrying only the material made six turns lose the geometry half while the
// cells came back exactly, so `edgeset_equal` (which compares both, on purpose)
// could never confirm the identity the exact lattice exists to give.
if mv != 0 || sv != 0 {
(aq, ar) = cell_rot(cq, cr, turns);
(bq, br) = cell_rot(nq, nr, turns);
edge_set_both(out.sn_edges, aq, ar, bq, br, sv, mv);
}
}
}
}
}
out
}
// Reflect a whole stencil (k -> -k). With the six rotations this is the other
// half of the TWELVE orientations: six placements, and the remaining six reached
// by flipping. Handed content — an L-shaped house, a staircase — needs it, and
// it is exact for the same reason the rotation is.
// Example: @HXL-007
pub fn stencil_mirror(st: Stencil) -> Stencil {
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
seen = false;
mnq = 0; mxq = 0; mnr = 0; mxr = 0;
for iq in 0..w {
for ir in 0..h {
if hexset_get(st.sn_cells, q0 + iq, r0 + ir) {
(tq, tr) = cell_mirror(q0 + iq, r0 + ir);
if !seen { mnq = tq; mxq = tq; mnr = tr; mxr = tr; seen = true; }
if tq < mnq { mnq = tq; }
if tq > mxq { mxq = tq; }
if tr < mnr { mnr = tr; }
if tr > mxr { mxr = tr; }
}
}
}
// No early `return st` — see stencil_rotate, and loft#982. With no cells to place there
// is nothing to reflect, so the extent stays and `flip` turns the reflection off.
flip = seen;
if !seen { mnq = q0; mnr = r0; mxq = q0 + w - 1; mxr = r0 + h - 1; }
nw = mxq - mnq + 1;
nh = mxr - mnr + 1;
out = stencil_with_edges(
hexset_chunk(mnq, mnr, nw, nh),
heights_new(mnq, mnr, nw, nh),
labels_new(mnq, mnr, nw, nh),
edgeset_new(mnq, mnr, nw, nh), // the cell extent — see stencil_rotate
st.sn_has_h, st.sn_has_l
);
out.sn_has_e = st.sn_has_e;
out.sn_layers = layers_new(mnq, mnr, nw, nh);
for li in 0..layer_count(st.sn_layers) { layer_add(out.sn_layers, layer_name(st.sn_layers, li)); }
for iq in 0..w {
for ir in 0..h {
if hexset_get(st.sn_cells, q0 + iq, r0 + ir) {
(tq, tr) = cell_mirror(q0 + iq, r0 + ir);
hexset_set(out.sn_cells, tq, tr, true);
if st.sn_has_h { height_set(out.sn_heights, tq, tr, height_get(st.sn_heights, q0 + iq, r0 + ir)); }
if st.sn_has_l { label_set(out.sn_labels, tq, tr, label_get(st.sn_labels, q0 + iq, r0 + ir)); }
for li in 0..layer_count(st.sn_layers) {
layer_set_at(out.sn_layers, li, tq, tr, layer_get_at(st.sn_layers, li, q0 + iq, r0 + ir));
}
}
}
}
if st.sn_has_e {
for iq in -1..(w + 1) {
for ir in -1..(h + 1) {
cq = q0 + iq;
cr = r0 + ir;
for sl in 0..3 {
d = 0;
if sl == 1 { d = 2; }
if sl == 2 { d = 3; }
nq = nb_q(cq, cr, d);
nr = nb_r(cq, cr, d);
mv = edge_mat(st.sn_edges, cq, cr, nq, nr);
sv = edge_surf(st.sn_edges, cq, cr, nq, nr);
if mv != 0 || sv != 0 { // both slots — see stencil_rotate
aq = cq; ar = cr; bq = nq; br = nr;
if flip {
(aq, ar) = cell_mirror(cq, cr);
(bq, br) = cell_mirror(nq, nr);
}
edge_set_both(out.sn_edges, aq, ar, bq, br, sv, mv);
}
}
}
}
}
out
}
// Degrees, refused rather than rounded when they are not a multiple of 60.
// A rotation that silently rounds is how an exact lattice quietly becomes an
// approximate one, so this returns an EMPTY stencil carrying ROT_NOT_MULTIPLE.
pub fn stencil_rotate_deg(st: Stencil, deg: integer) -> Stencil {
if deg % 60 != 0 {
bad = stencil_from(
hexset_chunk(0, 0, 1, 1), heights_new(0, 0, 1, 1), labels_new(0, 0, 1, 1),
false, false
);
bad.sn_code = ROT_NOT_MULTIPLE;
return bad;
}
stencil_rotate(st, (deg / 60) ?? 0)
}
// Stamp `st` so its (0,0) lands on target cell (aq, ar). Occupied stencil
// cells win; everything else in the target is untouched, which is what makes
// two stamps at different places order-free.
pub fn stencil_stamp(
dst: HexSet, dst_h: Heights, dst_l: Labels,
st: Stencil, aq: integer, ar: integer
) {
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
for iq in 0..w {
for ir in 0..h {
sq = q0 + iq;
sr = r0 + ir;
if hexset_get(st.sn_cells, sq, sr) {
(tq, tr) = lattice_to_cell(lattice_k(sq, sr) + dk, lattice_m(sq, sr) + dm);
hexset_set(dst, tq, tr, true);
if st.sn_has_h { height_set(dst_h, tq, tr, height_get(st.sn_heights, sq, sr)); }
if st.sn_has_l { label_set(dst_l, tq, tr, label_get(st.sn_labels, sq, sr)); }
}
}
}
}
// Stamp the named layers too. Separate from stencil_stamp so a consumer that
// uses no layers pays nothing for them.
pub fn stencil_stamp_layers(
dst: HexSet, dst_h: Heights, dst_l: Labels, dst_y: Layers,
st: Stencil, aq: integer, ar: integer
) {
stencil_stamp(dst, dst_h, dst_l, st, aq, ar);
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
for li in 0..layer_count(st.sn_layers) { layer_add(dst_y, layer_name(st.sn_layers, li)); }
for iq in 0..w {
for ir in 0..h {
sq = q0 + iq;
sr = r0 + ir;
if hexset_get(st.sn_cells, sq, sr) {
(tq, tr) = lattice_to_cell(lattice_k(sq, sr) + dk, lattice_m(sq, sr) + dm);
for li in 0..layer_count(st.sn_layers) {
nm = layer_name(st.sn_layers, li);
layer_set(dst_y, nm, tq, tr, layer_get(st.sn_layers, nm, sq, sr));
}
}
}
}
}
// Everything at once: cells, heights, labels, named layers and walls. Exists so
// a consumer does not stamp cells three times by calling the three narrower
// forms, and so there is ONE place that knows the full payload.
pub fn stencil_stamp_all(
dst: HexSet, dst_h: Heights, dst_l: Labels, dst_y: Layers, dst_e: EdgeSet,
st: Stencil, aq: integer, ar: integer
) {
stencil_stamp_layers(dst, dst_h, dst_l, dst_y, st, aq, ar);
if !st.sn_has_e { return; }
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
// the halo, for the same reason as stencil_rotate
for iq in -1..(w + 1) {
for ir in -1..(h + 1) {
cq = q0 + iq;
cr = r0 + ir;
for sl in 0..3 {
d = 0;
if sl == 1 { d = 2; }
if sl == 2 { d = 3; }
nq = nb_q(cq, cr, d);
nr = nb_r(cq, cr, d);
mv = edge_mat(st.sn_edges, cq, cr, nq, nr);
if mv != 0 {
(tq, tr) = lattice_to_cell(lattice_k(cq, cr) + dk, lattice_m(cq, cr) + dm);
(uq, ur) = lattice_to_cell(lattice_k(nq, nr) + dk, lattice_m(nq, nr) + dm);
edge_set_mat(dst_e, tq, tr, uq, ur, mv);
}
}
}
}
}
// Stamp including the wall layer. Edges translate the same way cells do — in
// lattice space — so a wall stamped on an odd row stays on the edge it was
// authored on.
pub fn stencil_stamp_edges(
dst: HexSet, dst_h: Heights, dst_l: Labels, dst_e: EdgeSet,
st: Stencil, aq: integer, ar: integer
) {
stencil_stamp(dst, dst_h, dst_l, st, aq, ar);
if !st.sn_has_e { return; }
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
// the halo, for the same reason as stencil_rotate
for iq in -1..(w + 1) {
for ir in -1..(h + 1) {
cq = q0 + iq;
cr = r0 + ir;
for sl in 0..3 {
d = 0;
if sl == 1 { d = 2; }
if sl == 2 { d = 3; }
nq = nb_q(cq, cr, d);
nr = nb_r(cq, cr, d);
mv = edge_mat(st.sn_edges, cq, cr, nq, nr);
if mv != 0 {
(tq, tr) = lattice_to_cell(lattice_k(cq, cr) + dk, lattice_m(cq, cr) + dm);
(uq, ur) = lattice_to_cell(lattice_k(nq, nr) + dk, lattice_m(nq, nr) + dm);
edge_set_mat(dst_e, tq, tr, uq, ur, mv);
}
}
}
}
}
// ── un-stamping ──────────────────────────────────────────────────────────
//
// The inverse of a stamp: it clears exactly the slots the stamp WROTE, back to
// the values a freshly-built field holds — cell false, height 0.0, label -1,
// layer 0, edge material 0.
//
// ⚠ THIS RESTORES THE TARGET ONLY WHERE THE TARGET WAS PRISTINE. A stamp is
// last-writer-wins (see `stencil_stamp`), so it DESTROYS whatever payload sat
// under its footprint; nothing here can bring that back, because the previous
// value was never kept. "Stamp then un-stamp is the identity" is therefore a
// claim about placing on FREE ground — which is the case the editor needs, a
// placement made and then taken back. Un-stamping onto ground another stencil
// already occupied leaves that other stencil's cells cleared too, and that is
// the honest behaviour rather than a bug: the two stencils are indistinguishable
// once merged, which is what merging into one field MEANS. A consumer that
// needs true undo over occupied ground must snapshot, not un-stamp.
// Clear the cells, heights and labels a `stencil_stamp` wrote.
pub fn stencil_unstamp(
dst: HexSet, dst_h: Heights, dst_l: Labels,
st: Stencil, aq: integer, ar: integer
) {
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
for iq in 0..w {
for ir in 0..h {
sq = q0 + iq;
sr = r0 + ir;
if hexset_get(st.sn_cells, sq, sr) {
(tq, tr) = lattice_to_cell(lattice_k(sq, sr) + dk, lattice_m(sq, sr) + dm);
hexset_set(dst, tq, tr, false);
if st.sn_has_h { height_set(dst_h, tq, tr, 0.0); }
if st.sn_has_l { label_set(dst_l, tq, tr, -1); }
}
}
}
}
// Clear the named layers too — the inverse of `stencil_stamp_layers`.
//
// The layer NAMES the stamp added to the target stay: a name is a property of
// the field, not of one placement, and a second placement of the same part
// would only add it back. Only the VALUES under this footprint are cleared.
pub fn stencil_unstamp_layers(
dst: HexSet, dst_h: Heights, dst_l: Labels, dst_y: Layers,
st: Stencil, aq: integer, ar: integer
) {
stencil_unstamp(dst, dst_h, dst_l, st, aq, ar);
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
for iq in 0..w {
for ir in 0..h {
sq = q0 + iq;
sr = r0 + ir;
if hexset_get(st.sn_cells, sq, sr) {
(tq, tr) = lattice_to_cell(lattice_k(sq, sr) + dk, lattice_m(sq, sr) + dm);
for li in 0..layer_count(st.sn_layers) {
nm = layer_name(st.sn_layers, li);
layer_set(dst_y, nm, tq, tr, 0);
}
}
}
}
}
// Everything at once — the inverse of `stencil_stamp_all`, and the form a
// consumer should use, so one place knows the full payload in BOTH directions.
//
// Edges walk the same halo the stamp did, and clear only the slots the stencil
// itself carried a material for. Iterating the occupied cells instead would
// leave the rim walls behind — the same blindness that once turned 18 edge
// slots into 10 with every cell count still agreeing.
pub fn stencil_unstamp_all(
dst: HexSet, dst_h: Heights, dst_l: Labels, dst_y: Layers, dst_e: EdgeSet,
st: Stencil, aq: integer, ar: integer
) {
stencil_unstamp_layers(dst, dst_h, dst_l, dst_y, st, aq, ar);
if !st.sn_has_e { return; }
dk = lattice_k(aq, ar);
dm = lattice_m(aq, ar);
q0 = hexset_q0(st.sn_cells); r0 = hexset_r0(st.sn_cells);
w = hexset_w(st.sn_cells); h = hexset_h(st.sn_cells);
for iq in -1..(w + 1) {
for ir in -1..(h + 1) {
cq = q0 + iq;
cr = r0 + ir;
for sl in 0..3 {
d = 0;
if sl == 1 { d = 2; }
if sl == 2 { d = 3; }
nq = nb_q(cq, cr, d);
nr = nb_r(cq, cr, d);
if edge_mat(st.sn_edges, cq, cr, nq, nr) != 0 {
(tq, tr) = lattice_to_cell(lattice_k(cq, cr) + dk, lattice_m(cq, cr) + dm);
(uq, ur) = lattice_to_cell(lattice_k(nq, nr) + dk, lattice_m(nq, nr) + dm);
edge_set_mat(dst_e, tq, tr, uq, ur, 0);
}
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// edges — the wall layer.
//
// The canonical key, direction canon, halo and type widths are crawler's edge
// layer's. That layer also carries a SURFACE id (physics geometry — straights
// and arcs); this holds only the material, which is what authored content and a
// stencil need, and `hex_edge` adds the surface on top.
//
// The canonical key is the DOUBLED MIDPOINT of the two cell centres:
// edge_k = lattice_k(a) + lattice_k(b), edge_m = lattice_m(a) + lattice_m(b)
// Symmetric in a and b, so an edge has one identity regardless of which side
// names it. It is also why rotation is free: the midpoint of two rotated
// centres IS the rotation of the midpoint, so an edge rotates exactly when its
// endpoints do.
//
// Opposite direction pairs are {0,1} {2,5} {3,4}, so the canonical set is
// {0,2,3} — exactly one of an edge's two cells sees it that way, which is what
// makes storage one slot per edge rather than two.
//
// TYPE WIDTHS are crawler's finding, adopted whole (their LOFT-HANDOFF H3):
// u16/i16 cannot be index-assigned at all, and u32 loses the write SILENTLY
// through a struct parameter. u8 and i32 are safe. The material is u8 because
// that is the widest safe SMALL type, not because 255 materials is a design.
// TWO SLOTS, TWO OWNERS. `eg_mat` is the PHYSICS of an edge — what it is made of — and
// is authored: a stencil carries materials, a document round-trips them. `eg_surf` is the
// GEOMETRY — WHICH exact surface blocks here — and is derived at build time.
//
// The surface slot exists because a hex edge has only 6 possible normals, and measured
// against the true wall normal across 24 headings, using the edge's own normal is wrong by
// up to 90 degrees: a bounce that sends you sideways. So the edge stores an id and the
// surface carries the exact geometry. That makes the grid a SPATIAL INDEX — it says THAT
// you hit something and WHICH thing — while the normal stays exact and mesh-free.
//
// `eg_surf` is DELIBERATELY NOT SERIALIZED by the document format, and that is not an
// omission to fix later: a surface id points into a `Surfaces` table this library does not
// own, so writing ids to a file would persist dangling references. Materials are ids into
// a closed enumeration of mechanisms and travel fine; surfaces are not. The TAG_EDGE
// section stays exactly `(w+2)*(h+2)*3` material bytes, so existing documents keep loading.
pub struct EdgeSet {
eg_q0: integer,
eg_r0: integer,
eg_w: integer,
eg_h: integer,
eg_gw: integer, // grid width = w + 2, a one-cell halo
eg_gh: integer,
eg_mat: vector<u8>, // material (PHYSICS); 0 = open — authored, serialized
eg_surf: vector<i32>, // surface id (GEOMETRY); 0 = passable — derived, not serialized
eg_count: integer, // blocked edges, maintained O(1) rather than counted by walking
eg_refused: integer, // writes rejected for a material outside 0..EDGE_MAT_MAX
}
// The whole domain of a material id, because the slot is a u8. Named rather than
// left implicit: a caller allocating material ids needs to know where they stop, and
// the failure at the far end is not a wrong material but a MISSING WALL — see
// `edge_set_mat`.
pub const EDGE_MAT_MAX = 255;
// Example: @HXL-008
pub fn edgeset_new(q0: integer, r0: integer, w: integer, h: integer) -> EdgeSet {
gw = w + 2;
gh = h + 2;
m: vector<u8> = [];
sf: vector<i32> = [];
n = gw * gh * 3;
for _ in 0..n { m += [(0 as u8?) ?? 0]; sf += [(0 as i32?) ?? 0]; }
EdgeSet { eg_q0: q0, eg_r0: r0, eg_w: w, eg_h: h, eg_gw: gw, eg_gh: gh,
eg_mat: m, eg_surf: sf, eg_count: 0, eg_refused: 0 }
}
// The doubled midpoint — an edge's identity, independent of which cell names it.
// Example: @HXL-008
pub fn edge_key(qa: integer, ra: integer, qb: integer, rb: integer) -> (integer, integer) {
(lattice_k(qa, ra) + lattice_k(qb, rb), lattice_m(qa, ra) + lattice_m(qb, rb))
}
fn eg_dir_from(qa: integer, ra: integer, qb: integer, rb: integer) -> integer {
d = -1;
for i in 0..6 {
if nb_q(qa, ra, i) == qb && nb_r(qa, ra, i) == rb { d = i; }
}
d
}
// Storage slot for the edge between two adjacent cells, or -1.
fn eg_index(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer) -> integer {
d = eg_dir_from(qa, ra, qb, rb);
if d < 0 { return -1; }
cq = qa; cr = ra; slot = 0;
if d == 2 { slot = 1; }
if d == 3 { slot = 2; }
if d == 1 { cq = qb; cr = rb; slot = 0; }
if d == 5 { cq = qb; cr = rb; slot = 1; }
if d == 4 { cq = qb; cr = rb; slot = 2; }
dq = cq - e.eg_q0 + 1;
dr = cr - e.eg_r0 + 1;
if dq < 0 || dq >= e.eg_gw || dr < 0 || dr >= e.eg_gh { return -1; }
(dr * e.eg_gw + dq) * 3 + slot
}
// The storage slot for an edge, or -1. PUBLIC because a consumer building its
// own accessors on this layer cannot address storage it cannot index — crawler's
// apply_features and its junction arbitration write by index. Exposing the slot
// is cheaper than exporting a get/set pair for every access pattern they have.
// Unconditional write — the last writer wins. The write POLICY is deliberately the
// caller's: first-writer-wins is `if edge_mat(...) == 0 { edge_set_mat(...) }` at the call
// site, where the reader can see which rule is in force.
// Example: @HXL-009
pub fn edge_set_mat(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer, mat: integer) {
i = eg_index(e, qa, ra, qb, rb);
if i < 0 { return; }
// OUT OF DOMAIN IS REFUSED, NOT NARROWED. The slot is a u8, and narrowing past it
// with a `?? 0` default would write the one value that means NO WALL —
// `edge_set_mat(e, …, 300)` would delete the wall it was setting. The slot keeps
// what it held and the refusal is counted, because a wall that is not there is not
// something a caller can see.
if mat < 0 || mat > EDGE_MAT_MAX { e.eg_refused = e.eg_refused + 1; return; }
e.eg_mat[i] = (mat as u8?) ?? 0;
}
// Writes this layer refused because the material was outside 0..EDGE_MAT_MAX. Non-zero
// means a wall a caller asked for is absent — check it wherever material ids are derived
// rather than written as literals.
// Example: @HXL-009
pub fn edgeset_refused(e: EdgeSet) -> integer { e.eg_refused }
// The material on the edge between two neighbouring cells, or 0 when there is none.
// Edges carry what cells cannot: a wall belongs BETWEEN two hexes, not inside one.
pub fn edge_mat(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer) -> integer {
i = eg_index(e, qa, ra, qb, rb);
if i < 0 { return 0; }
(e.eg_mat[i] ?? 0) as integer
}
// ── THE SURFACE SLOT ────────────────────────────────────────────────────────
//
// WHY THESE ARE ACCESSORS AND `eg_index` STAYS PRIVATE. The obvious alternative is to
// publish the index so a consumer can write `e.eg_surf[i]` in its own loops, which is what
// crawler's collision layer does today over its own copy of this storage. Publishing it
// would freeze `(cell, canonical direction, slot)` into the contract, and with two
// consumers on this library that layout could then never change. The cost of accessors is
// one extra index computation per write in a build-time loop; the cost of the alternative
// is permanent. Correctness of the seam outranks a small constant here.
// Write the surface blocking this edge, maintaining the blocked count exactly.
// Unconditional — the WRITE POLICY belongs to the caller, because there is more than one:
// crawler's collision layer wants first-writer-wins for plain construction and
// nearest-surface arbitration where two parts meet. A policy baked in here would silently
// decide which surface owns a junction, which is a physics question, not a storage one.
pub fn edge_set_surf(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer,
surf: integer) {
i = eg_index(e, qa, ra, qb, rb);
if i < 0 { return; }
was = (e.eg_surf[i] ?? 0) as integer;
if was == 0 && surf != 0 { e.eg_count = e.eg_count + 1; }
if was != 0 && surf == 0 { e.eg_count = e.eg_count - 1; }
e.eg_surf[i] = (surf as i32?) ?? 0;
}
// The surface on that same edge — the finish, where `edge_mat` is the substance.
pub fn edge_surf(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer) -> integer {
i = eg_index(e, qa, ra, qb, rb);
if i < 0 { return 0; }
(e.eg_surf[i] ?? 0) as integer
}
// Both halves of an edge in one call, so a caller cannot set geometry and forget physics.
pub fn edge_set_both(e: EdgeSet, qa: integer, ra: integer, qb: integer, rb: integer,
surf: integer, mat: integer) {
edge_set_surf(e, qa, ra, qb, rb, surf);
edge_set_mat(e, qa, ra, qb, rb, mat);
}
// Blocked edges, O(1). `edgeset_count` walks the grid and counts MATERIALS; this is the
// derived-geometry count and is maintained on write, so it is free to call every frame.
pub fn edgeset_surf_count(e: EdgeSet) -> integer { e.eg_count }
// Exact element-wise equality over BOTH slots and the whole grid, halo included.
//
// Exists because the obvious consumer test — "is the cached layer identical to a freshly
// derived one?" — otherwise has to read the vectors directly, which would make the field
// layout contract. Compares the halo too: `edgeset_count` walks only in-chunk cells, so a
// corruption in the 74-of-75 halo slots of a small layer round-trips faithfully and no
// assertion sees it. That is not hypothetical here — it is how a negative control in this
// package failed to fail.
pub fn edgeset_equal(a: EdgeSet, b: EdgeSet) -> boolean {
if a.eg_gw != b.eg_gw || a.eg_gh != b.eg_gh { return false; }
n = a.eg_gw * a.eg_gh * 3;
same = true;
for i in 0..n {
if ((a.eg_mat[i] ?? 0) as integer) != ((b.eg_mat[i] ?? 0) as integer) { same = false; }
if ((a.eg_surf[i] ?? 0) as integer) != ((b.eg_surf[i] ?? 0) as integer) { same = false; }
}
same
}
// An order-sensitive checksum of the whole layer — the cheap "did anything change at all?"
// for a test that operates on a layer and asserts it was left alone. Use `edgeset_equal`
// when the answer must be exact; a digest can collide, and for a correctness gate that
// matters.
pub fn edgeset_digest(e: EdgeSet) -> integer {
h = 1469598103;
n = e.eg_gw * e.eg_gh * 3;
for i in 0..n {
h = (h * 31 + ((e.eg_mat[i] ?? 0) as integer)) % 1000000007 ?? 0;
h = (h * 31 + ((e.eg_surf[i] ?? 0) as integer)) % 1000000007 ?? 0;
}
h
}
// Actual bytes held, so a consumer can GATE the footprint instead of reasoning about it.
// An edge layer is a per-region cache in at least one consumer, which makes its size a
// real cost; `size` reports what is truly allocated, so an assertion on it is what stops
// the structure quietly growing back. Exposed as a function rather than leaving consumers
// to read the vectors, which would make the field layout contract.
pub fn edgeset_bytes(e: EdgeSet) -> integer { size(e.eg_mat) + size(e.eg_surf) }
// The edge layer's window, read back, exactly as `hexset_q0` reads a cell layer's.
pub fn edgeset_q0(e: EdgeSet) -> integer { e.eg_q0 }
// The edge layer's first `r`.
pub fn edgeset_r0(e: EdgeSet) -> integer { e.eg_r0 }
// How many `q` columns the edge layer spans.
pub fn edgeset_w(e: EdgeSet) -> integer { e.eg_w }
// How many `r` rows the edge layer spans.
pub fn edgeset_h(e: EdgeSet) -> integer { e.eg_h }
// Every non-zero slot in the layer, INCLUDING the halo. edgeset_count walks
// only in-chunk cells, which leaves the halo — 74 of a 3x3 layer's 75 slots —
// unverified: a corruption there round-trips faithfully and no assertion sees
// it. Found by a negative control that failed to fail.
pub fn edgeset_count_all(e: EdgeSet) -> integer {
n = 0;
for i in 0..(e.eg_gw * e.eg_gh * 3) {
if (e.eg_mat[i] ?? 0) != 0 { n = n + 1; }
}
n
}
// How many edges carry a material. Walks the canonical slots only, so each
// edge is counted once however many cells touch it.
pub fn edgeset_count(e: EdgeSet) -> integer {
n = 0;
for iq in 0..e.eg_w {
for ir in 0..e.eg_h {
cq = e.eg_q0 + iq;
cr = e.eg_r0 + ir;
for s in 0..3 {
d = 0;
if s == 1 { d = 2; }
if s == 2 { d = 3; }
if edge_mat(e, cq, cr, nb_q(cq, cr, d), nb_r(cq, cr, d)) != 0 { n = n + 1; }
}
}
}
n
}
// ═══════════════════════════════════════════════════════════════════════
// named integer layers.
//
// `Labels` is one integer per cell. A consumer generally needs several — moros
// carries a material, an item and that item's rotation — and which ones exist is
// CONTENT, not mechanism. So the library holds "a named integer field over the
// chunk" and the consumer names them.
//
// One flat vector rather than a vector of Labels: layer i, cell (q,r) lives at
// i*w*h + r*w + q. Adding a layer appends w*h slots, so a stencil with one layer
// costs one layer's worth of storage.
// Named integer layers over a `ly_w × ly_h` window anchored at `(ly_q0, ly_r0)`: the layer
// names, and every value in one flat vector, layer i and cell (q,r) at i*w*h + r*w + q.
pub struct Layers {
ly_q0: integer,
ly_r0: integer,
ly_w: integer,
ly_h: integer,
ly_names: vector<text>,
ly_vals: vector<integer>,
}
// Several named integer layers over one window, added as they are needed. Reach for it
// instead of many `Labels` when the set of layers is the consumer's to decide.
pub fn layers_new(q0: integer, r0: integer, w: integer, h: integer) -> Layers {
ns: vector<text> = [];
vs: vector<integer> = [];
Layers { ly_q0: q0, ly_r0: r0, ly_w: w, ly_h: h, ly_names: ns, ly_vals: vs }
}
// How many layers exist, and the name of layer i — enough to walk them without knowing
// which names a consumer chose.
pub fn layer_count(ls: Layers) -> integer { len(ls.ly_names) }
// Layer i's name, or "" — walk `0..layer_count` to list them.
pub fn layer_name(ls: Layers, i: integer) -> text { ls.ly_names[i] ?? "" }
// The layer stack's window, read back, as `hexset_q0` reads a cell layer's.
pub fn layers_q0(ls: Layers) -> integer { ls.ly_q0 }
// The layer stack's first `r`.
pub fn layers_r0(ls: Layers) -> integer { ls.ly_r0 }
// How many `q` columns the layer stack spans.
pub fn layers_w(ls: Layers) -> integer { ls.ly_w }
// How many `r` rows the layer stack spans.
pub fn layers_h(ls: Layers) -> integer { ls.ly_h }
// Index of a named layer, or -1. Names are the consumer's vocabulary; the
// library never interprets them.
pub fn layer_index(ls: Layers, name: text) -> integer {
found = -1;
for i in 0..len(ls.ly_names) {
if (ls.ly_names[i] ?? "") == name { found = i; }
}
found
}
// Add a layer, zero-filled. Adding one that exists is a no-op, so a consumer
// can call it defensively without checking first.
pub fn layer_add(ls: Layers, name: text) -> integer {
had = layer_index(ls, name);
if had >= 0 { return had; }
ls.ly_names += [name];
n = ls.ly_w * ls.ly_h;
for _ in 0..n { ls.ly_vals += [0]; }
len(ls.ly_names) - 1
}
fn ly_slot(ls: Layers, li: integer, q: integer, r: integer) -> integer {
dq = q - ls.ly_q0;
dr = r - ls.ly_r0;
if li < 0 || dq < 0 || dq >= ls.ly_w || dr < 0 || dr >= ls.ly_h { return -1; }
li * ls.ly_w * ls.ly_h + dr * ls.ly_w + dq
}
// Write one cell of a named layer. An unknown name does nothing rather than creating a
// layer, so a typo loses the write instead of silently growing the stack.
pub fn layer_set(ls: Layers, name: text, q: integer, r: integer, v: integer) {
i = ly_slot(ls, layer_index(ls, name), q, r);
if i < 0 { return; }
ls.ly_vals[i] = v;
}
// Read one cell of a named layer; 0 when the name or the cell is not there.
pub fn layer_get(ls: Layers, name: text, q: integer, r: integer) -> integer {
i = ly_slot(ls, layer_index(ls, name), q, r);
if i < 0 { return 0; }
ls.ly_vals[i] ?? 0
}
// By index, for the writer and for iterating without knowing the names.
pub fn layer_get_at(ls: Layers, li: integer, q: integer, r: integer) -> integer {
i = ly_slot(ls, li, q, r);
if i < 0 { return 0; }
ls.ly_vals[i] ?? 0
}
// Write by layer INDEX, for a loop that resolved the name once with `layer_index`.
pub fn layer_set_at(ls: Layers, li: integer, q: integer, r: integer, v: integer) {
i = ly_slot(ls, li, q, r);
if i < 0 { return; }
ls.ly_vals[i] = v;
}
tests/01-hex-field.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// hex_field: the exact-integer contract, asserted standalone.
//
// These are the CONTRACT tests — the guarantees a consumer (the game, the in-world
// editor) is entitled to rely on. They are deliberately self-contained: crawler keeps
// its own golden-JSON diff against the Python oracle, because that is a CONSUMER's
// check of this package, not this package's check of itself.
use hex_field::*;
fn main() {
s = hexset_new(16);
hexdisk_into(s, 8, 8, 2);
assert(hexset_count(s) == 19, "hexdisk N=2 is 19 cells");
}
// I-ROUNDTRIP: outline and cell set can never disagree.
// sum of the integer shoelace over all loops == 12 * (number of hexes)
fn test_area_roundtrip_is_exact() {
ok = true;
for n in 0..5 {
s = hexset_new(32);
hexdisk_into(s, 16, 16, n);
v = trace(s);
if shoelace_total(v) != 12 * hexset_count(s) { ok = false; }
}
assert(ok, "shoelace total == 12 * cell count, for every hexdisk radius");
}
// The validator must pass every traced form it produced itself.
fn test_validator_accepts_traced_forms() {
ok = true;
for n in 1..5 {
s = hexset_new(32);
hexdisk_into(s, 16, 16, n);
v = trace(s);
if validate(v, hexset_count(s)) != 0 { ok = false; }
}
assert(ok, "validate() == 0 on forms this package traced");
}
// Vertices are INTEGER lattice points — the property the whole design rests on.
// Cell centres satisfy k == m (mod 2); no float, no epsilon, no drift.
fn test_lattice_is_integral() {
ok = true;
for r in 0..12 {
for q in 0..12 {
k = lattice_k(q, r);
m = lattice_m(q, r);
if ((k - m) % 2) != 0 { ok = false; }
}
}
assert(ok, "every cell centre satisfies k == m (mod 2)");
}
// Occupancy round-trips, and out-of-chunk reads are false rather than a crash.
fn test_hexset_setget_and_bounds() {
s = hexset_new(8);
hexset_set(s, 3, 4, true);
assert(hexset_get(s, 3, 4), "set then get");
assert(!hexset_get(s, 4, 4), "an untouched cell is empty");
assert(!hexset_get(s, 99, 99), "out of chunk reads false, not a crash");
}
// NEGATIVE CONTROL: the area invariant must FAIL when the set is corrupted by one
// cell without re-tracing. A gate that cannot go red is not a gate.
fn test_negative_control_detects_one_cell() {
s = hexset_new(32);
hexdisk_into(s, 16, 16, 3);
v = trace(s);
hexset_set(s, 16, 16 + 9, true); // a detached cell, outline NOT re-traced
assert(shoelace_total(v) != 12 * hexset_count(s),
"one added cell must break the area round-trip");
}
tests/02-hex-doc.loft
// The document format's contract. `load(save(x)) == x` is the interface
// between consumers, so it is gated here, in the package, not in either game.
use hex_field::*;
// Clears any leftover before naming the path: `file()` APPENDS to an existing file,
// so a run that fails before its `delete` would poison the next one's header.
fn tmp(name: text) -> text {
p = "hexdoc_{name}.bin";
delete(p);
p
}
fn build(w: integer, h: integer) -> HexSet {
s = hexset_chunk(2, 3, w, h);
for q in 0..w {
for r in 0..h {
if (q + r) % 3 != 0 { hexset_set(s, 2 + q, 3 + r, true); }
}
}
s
}
fn test_round_trip_is_identity() {
s = build(5, 4);
hz = heights_new(2, 3, 5, 4);
lb = labels_new(2, 3, 5, 4);
for q in 0..5 {
for r in 0..4 {
height_set(hz, 2 + q, 3 + r, (q * 10 + r) as float + 0.5);
label_set(lb, 2 + q, 3 + r, q * 7 + r);
}
}
p = tmp("rt");
doc_write(p, s, hz, lb, true, true);
d = doc_read(p);
assert(d.doc_code == HXF_OK, "load refused with {d.doc_code}");
assert(d.doc_has_h, "heights section missing");
assert(d.doc_has_l, "labels section missing");
assert(hexset_count(d.doc_cells) == hexset_count(s), "cell count changed");
for q in 0..5 {
for r in 0..4 {
assert(hexset_get(d.doc_cells, 2 + q, 3 + r) == hexset_get(s, 2 + q, 3 + r),
"occupancy differs at {q},{r}");
// fractional heights survive: the format carries f64, so 12.5 is NOT 12
assert(height_get(d.doc_heights, 2 + q, 3 + r) == height_get(hz, 2 + q, 3 + r),
"height differs at {q},{r}");
assert(label_get(d.doc_labels, 2 + q, 3 + r) == label_get(lb, 2 + q, 3 + r),
"label differs at {q},{r}");
}
}
delete(p);
}
// Writing what was read must produce the SAME BYTES. Structural equality
// hides field-ordering and default-value drift, which is how two consumers
// stop agreeing without either noticing.
fn test_rewrite_is_byte_identical() {
s = build(4, 4);
hz = heights_new(2, 3, 4, 4);
lb = labels_new(2, 3, 4, 4);
for q in 0..4 { for r in 0..4 { height_set(hz, 2 + q, 3 + r, 1.25); label_set(lb, 2 + q, 3 + r, 9); } }
a = tmp("b1"); b = tmp("b2");
doc_write(a, s, hz, lb, true, true);
d = doc_read(a);
assert(d.doc_code == HXF_OK, "first load refused {d.doc_code}");
doc_write(b, d.doc_cells, d.doc_heights, d.doc_labels, d.doc_has_h, d.doc_has_l);
fa = file(a); fa#format = LittleEndian;
fb = file(b); fb#format = LittleEndian;
na = fa#size; nb = fb#size;
assert(na == nb, "sizes differ: {na} vs {nb}");
for i in 0..na {
x = fa#read as u8;
y = fb#read as u8;
assert(x == y, "byte {i} differs: {x} vs {y}");
}
delete(a); delete(b);
}
// An occupancy-only field must not carry empty height/label sections.
fn test_absent_sections_are_absent() {
s = build(3, 3);
hz = heights_new(2, 3, 3, 3);
lb = labels_new(2, 3, 3, 3);
p = tmp("occ");
doc_write(p, s, hz, lb, false, false);
d = doc_read(p);
assert(d.doc_code == HXF_OK, "refused {d.doc_code}");
assert(!d.doc_has_h, "heights present when not written");
assert(!d.doc_has_l, "labels present when not written");
f = file(p); f#format = LittleEndian;
assert(f#size == 32 + 8 + 9, "occupancy-only file is {f#size} bytes, want 49");
delete(p);
}
// THE reason sections carry a length: a field written by a newer consumer,
// with a section this reader has never heard of, still loads.
fn test_unknown_section_is_skipped() {
p = tmp("fwd");
{
f = file(p);
f#format = LittleEndian;
f += (HXF_MAGIC as i32);
f += (HXF_SCHEMA as i32);
f += (0 as i32); f += (0 as i32); // q0, r0
f += (2 as i32); f += (2 as i32); // w, h
f += (2 as i32); // two sections
f += (0 as i32); // reserved
f += (0x5A5A5A5A as i32); // a tag from the future
f += (6 as i32);
for _ in 0..6 { f += (0xEE as u8); }
f += (TAG_OCCU as i32);
f += (4 as i32);
for _ in 0..4 { f += (1 as u8); }
}
d = doc_read(p);
assert(d.doc_code == HXF_OK, "a future section made the load fail: {d.doc_code}");
assert(d.doc_skipped == 1, "expected 1 skipped section, got {d.doc_skipped}");
assert(hexset_count(d.doc_cells) == 4, "occupancy after the unknown section was lost");
delete(p);
}
// ── refusals: each must fail LOUDLY, never parse half a field ────────────
fn test_refuses_bad_magic() {
p = tmp("magic");
{ f = file(p); f#format = LittleEndian; f += ((0xDEADBEEF as i32?) ?? 0 as i32); }
assert(doc_read(p).doc_code != HXF_OK, "bad magic accepted");
delete(p);
}
fn test_refuses_future_schema() {
p = tmp("schema");
{
f = file(p); f#format = LittleEndian;
f += (HXF_MAGIC as i32); f += (99 as i32);
for _ in 0..6 { f += (0 as i32); }
}
d = doc_read(p);
assert(d.doc_code == HXF_BAD_SCHEMA, "future schema gave {d.doc_code}");
delete(p);
}
fn test_refuses_nonzero_reserved() {
p = tmp("rsv");
{
f = file(p); f#format = LittleEndian;
f += (HXF_MAGIC as i32); f += (HXF_SCHEMA as i32);
f += (0 as i32); f += (0 as i32); f += (2 as i32); f += (2 as i32);
f += (1 as i32); f += (7 as i32);
}
d = doc_read(p);
assert(d.doc_code == HXF_BAD_RESERVED, "non-zero reserved gave {d.doc_code}");
delete(p);
}
// Explicit dimensions exist to catch exactly this.
fn test_refuses_section_length_disagreeing_with_dimensions() {
p = tmp("dims");
{
f = file(p); f#format = LittleEndian;
f += (HXF_MAGIC as i32); f += (HXF_SCHEMA as i32);
f += (0 as i32); f += (0 as i32);
f += (4 as i32); f += (4 as i32); // claims 16 cells
f += (1 as i32); f += (0 as i32);
f += (TAG_OCCU as i32); f += (9 as i32); // but ships 9 bytes
for _ in 0..9 { f += (1 as u8); }
}
d = doc_read(p);
assert(d.doc_code == HXF_BAD_SECTION, "dimension mismatch gave {d.doc_code}");
delete(p);
}
fn test_refuses_truncated_file() {
s = build(4, 4);
hz = heights_new(2, 3, 4, 4);
lb = labels_new(2, 3, 4, 4);
full = tmp("full");
doc_write(full, s, hz, lb, true, true);
ff = file(full); ff#format = LittleEndian;
keep = ff#size - 20;
cut = tmp("cut");
{ g = file(cut); g#format = LittleEndian;
for _ in 0..keep { b = ff#read as u8; g += (b as u8); } }
d = doc_read(cut);
assert(d.doc_code == HXF_TRUNCATED, "truncated file gave {d.doc_code}");
delete(full); delete(cut);
}
fn test_refuses_header_only_stub() {
p = tmp("stub");
{ f = file(p); f#format = LittleEndian; f += (HXF_MAGIC as i32); }
assert(doc_read(p).doc_code == HXF_TRUNCATED, "8-byte stub accepted");
delete(p);
}
tests/03-fixture.loft
// The interface gate. `tests/fixtures/canonical.hxf` is ONE committed file that
// both consumers read. If either side's writer or reader drifts, one of them
// stops agreeing with a file neither of them can quietly regenerate.
//
// Its values are chosen to exercise the format's decisions, not to look tidy:
// a NEGATIVE extent origin, labels past u8 (301), and heights that are both
// negative and fractional (-3.5) — the three things moros#1 measured the dense
// model could not express.
use hex_field::*;
// Relative paths resolve from the TEST FILEs directory under `loft test`,
// not the package root — a doubled `tests/` is the classic way to get this wrong.
const FIXTURE = "fixtures/canonical.hxf";
fn test_canonical_fixture_loads() {
d = doc_read(FIXTURE);
assert(d.doc_code == HXF_OK, "the committed fixture no longer loads: code {d.doc_code}");
assert(hexset_q0(d.doc_cells) == -2, "q0 is {hexset_q0(d.doc_cells)}, want -2");
assert(hexset_r0(d.doc_cells) == -1, "r0 is {hexset_r0(d.doc_cells)}, want -1");
assert(hexset_w(d.doc_cells) == 4, "w is {hexset_w(d.doc_cells)}");
assert(hexset_h(d.doc_cells) == 3, "h is {hexset_h(d.doc_cells)}");
assert(hexset_count(d.doc_cells) == 8, "cells is {hexset_count(d.doc_cells)}, want 8");
assert(d.doc_has_h, "heights section lost");
assert(d.doc_has_l, "labels section lost");
}
fn test_canonical_fixture_values_survive() {
d = doc_read(FIXTURE);
assert(d.doc_code == HXF_OK, "fixture refused {d.doc_code}");
// a label past u8 — the format is i32 on purpose
assert(label_get(d.doc_labels, 1, -1) == 301, "label at (1,-1) is {label_get(d.doc_labels, 1, -1)}, want 301");
// a height that is negative AND fractional — the format is f64 on purpose
assert(height_get(d.doc_heights, -2, -1) == 0.0 - 3.5,
"height at (-2,-1) is {height_get(d.doc_heights, -2, -1)}, want -3.5");
assert(height_get(d.doc_heights, 1, 0) == 9.5,
"height at (1,0) is {height_get(d.doc_heights, 1, 0)}, want 9.5");
}
// Read it, write it back, and the bytes must match the committed file. This is
// what stops a writer from drifting while its own reader drifts with it.
fn test_rewriting_the_fixture_reproduces_it() {
d = doc_read(FIXTURE);
assert(d.doc_code == HXF_OK, "fixture refused {d.doc_code}");
tmp = "fixture_rewrite.bin";
doc_write(tmp, d.doc_cells, d.doc_heights, d.doc_labels, d.doc_has_h, d.doc_has_l);
fa = file(FIXTURE); fa#format = LittleEndian;
fb = file(tmp); fb#format = LittleEndian;
na = fa#size; nb = fb#size;
assert(na == nb, "rewrite is {nb} bytes, committed fixture is {na}");
for i in 0..na {
x = fa#read as u8;
y = fb#read as u8;
assert(x == y, "byte {i} differs: {x} vs {y} — the writer has drifted from the fixture");
}
delete(tmp);
}
tests/04-stencil.loft
// The stencil contract. Every assertion here is exact — no tolerance appears in
// this file, because the whole claim is that the lattice makes rotation exact.
use hex_field::*;
// An L-shape: handed, so a mirror is distinguishable from a rotation.
fn ell() -> Stencil {
s = hexset_chunk(0, 0, 3, 3);
hz = heights_new(0, 0, 3, 3);
lb = labels_new(0, 0, 3, 3);
for r in 0..3 { hexset_set(s, 0, r, true); label_set(lb, 0, r, 10 + r); height_set(hz, 0, r, r as float + 0.5); }
for q in 1..3 { hexset_set(s, q, 0, true); label_set(lb, q, 0, 20 + q); height_set(hz, q, 0, q as float + 0.5); }
stencil_from(s, hz, lb, true, true)
}
fn shoelace_of(s: HexSet) -> integer { shoelace_total(trace(s)) }
// ── the load-bearing invariant ───────────────────────────────────────────
// SIX 60° STEPS ARE THE IDENTITY — asserted at the lattice level, because
// cell_rot reduces n modulo 6, so "rotate by 6" short-circuits to doing nothing
// and would pass no matter how wrong the rotation was. A control that perturbed
// lattice_rot60 by (+1,+1) left an earlier version of this test green; composing
// the primitive six times is what actually pins it.
fn test_six_lattice_steps_are_the_identity() {
for qi in 0..9 {
for ri in 0..9 {
q = qi - 4;
r = ri - 4;
k0 = lattice_k(q, r);
m0 = lattice_m(q, r);
k = k0; m = m0;
for s in 0..6 {
(nk, nm) = lattice_rot60(k, m);
k = nk; m = nm;
// every intermediate image must still be a legal cell centre
assert(m % 3 == 0, "step {s} from {q},{r}: m={m} is not a multiple of 3");
assert((k - m) % 2 == 0, "step {s} from {q},{r}: k={k} m={m} breaks k = m (mod 2)");
}
assert(k == k0 && m == m0,
"six steps from cell {q},{r}: ({k},{m}) != ({k0},{m0})");
}
}
}
// And each single step must actually MOVE a non-origin cell — otherwise the
// identity above is satisfied by a rotation that does nothing at all.
fn test_a_single_step_is_not_a_no_op() {
for n in 1..6 {
(rq, rr) = cell_rot(2, 1, n);
assert(rq != 2 || rr != 1, "rotating (2,1) by {n} steps left it in place");
}
(bq, br) = cell_rot(2, 1, 6);
assert(bq == 2 && br == 1, "rotating (2,1) by 6 steps did not return it");
}
fn test_rotation_preserves_count_and_area() {
st = ell();
n0 = hexset_count(st.sn_cells);
a0 = shoelace_of(st.sn_cells);
assert(a0 == 12 * n0, "the source itself breaks the area identity");
for step in 1..6 {
rot = stencil_rotate(st, step);
n = hexset_count(rot.sn_cells);
a = shoelace_of(rot.sn_cells);
assert(n == n0, "step {step}: cell count {n}, want {n0}");
assert(a == a0, "step {step}: shoelace {a}, want {a0}");
assert(a == 12 * n, "step {step}: area identity broken");
}
}
fn test_mirror_is_exact_and_self_inverse() {
for q in 0..5 {
for r in 0..5 {
(mq, mr) = cell_mirror(q - 2, r - 2);
(bq, br) = cell_mirror(mq, mr);
assert(bq == q - 2 && br == r - 2, "mirror is not self-inverse at {q-2},{r-2}");
}
}
}
// ── stamping ─────────────────────────────────────────────────────────────
fn test_stamp_lands_where_asked_on_an_odd_row() {
// An ODD anchor row is the case that a naive (q,r) translation shears.
st = ell();
dst = hexset_chunk(-5, -5, 20, 20);
dh = heights_new(-5, -5, 20, 20);
dl = labels_new(-5, -5, 20, 20);
stencil_stamp(dst, dh, dl, st, 4, 3);
assert(hexset_count(dst) == hexset_count(st.sn_cells),
"stamped {hexset_count(dst)} cells, stencil has {hexset_count(st.sn_cells)}");
// the stamped shape must still trace to the same area
assert(shoelace_of(dst) == 12 * hexset_count(dst), "stamped shape breaks the area identity");
}
// ⚠ AT DIFFERENT PLACES — and the last line is what says so. The OVERLAPPING
// case is a different claim with a different answer, and it is
// `test_overlapping_stamps_are_order_free_only_in_occupancy` below.
fn test_two_stamps_at_different_places_are_order_free() {
st = ell();
a1 = hexset_chunk(-8, -8, 30, 30); h1 = heights_new(-8, -8, 30, 30); l1 = labels_new(-8, -8, 30, 30);
a2 = hexset_chunk(-8, -8, 30, 30); h2 = heights_new(-8, -8, 30, 30); l2 = labels_new(-8, -8, 30, 30);
stencil_stamp(a1, h1, l1, st, 1, 1);
stencil_stamp(a1, h1, l1, st, 6, 5);
stencil_stamp(a2, h2, l2, st, 6, 5);
stencil_stamp(a2, h2, l2, st, 1, 1);
for q in 0..30 {
for r in 0..30 {
assert(hexset_get(a1, -8 + q, -8 + r) == hexset_get(a2, -8 + q, -8 + r),
"stamp order changed cell {q},{r}");
}
}
assert(hexset_count(a1) == 2 * hexset_count(st.sn_cells), "the two stamps overlapped");
}
fn test_stamp_leaves_the_rest_of_the_target_alone() {
st = ell();
dst = hexset_chunk(0, 0, 20, 20); dh = heights_new(0, 0, 20, 20); dl = labels_new(0, 0, 20, 20);
hexset_set(dst, 15, 15, true); // a pre-existing cell far away
label_set(dl, 15, 15, 999);
stencil_stamp(dst, dh, dl, st, 2, 2);
assert(hexset_get(dst, 15, 15), "an unrelated cell was cleared by a stamp");
assert(label_get(dl, 15, 15) == 999, "an unrelated label was overwritten");
}
// ── the negative control the issue asks for ──────────────────────────────
fn test_rotation_by_a_non_multiple_of_60_is_refused() {
st = ell();
bad = stencil_rotate_deg(st, 45);
assert(bad.sn_code == ROT_NOT_MULTIPLE, "45° was accepted, code {bad.sn_code}");
assert(hexset_count(bad.sn_cells) == 0, "a refused rotation still produced cells");
bad2 = stencil_rotate_deg(st, 1);
assert(bad2.sn_code == ROT_NOT_MULTIPLE, "1° was accepted");
}
fn test_rotation_by_multiples_of_60_is_accepted() {
st = ell();
for i in 0..7 {
ok = stencil_rotate_deg(st, i * 60);
assert(ok.sn_code == ROT_OK, "{i * 60}° was refused");
assert(hexset_count(ok.sn_cells) == hexset_count(st.sn_cells), "{i * 60}° lost cells");
}
neg = stencil_rotate_deg(st, 0 - 120);
assert(neg.sn_code == ROT_OK, "-120° was refused");
assert(hexset_count(neg.sn_cells) == hexset_count(st.sn_cells), "-120° lost cells");
}
// ── a stencil must never lose data ───────────────────────────────────────
//
// Not "the cells survive" — EVERYTHING survives. A stencil is cells plus
// labels, heights, edge materials and named layers, and an orientation change
// is a relabelling of the lattice, so nothing has anywhere to go.
//
// This is stated over edgeset_count_all, which walks the halo too: rim edges are
// stored against a cell OUTSIDE the extent, so an implementation that iterates
// occupied cells drops them and every in-chunk count still looks plausible.
// That is exactly what happened — 18 slots became 10 and the cell count never
// moved.
fn walled_ring() -> Stencil {
c = hexset_chunk(-1, -1, 3, 3);
hz = heights_new(-1, -1, 3, 3);
lb = labels_new(-1, -1, 3, 3);
eg = edgeset_new(-1, -1, 3, 3);
hexset_set(c, 0, 0, true);
label_set(lb, 0, 0, 2);
for d in 0..6 {
rq = nb_q(0, 0, d);
rr = nb_r(0, 0, d);
hexset_set(c, rq, rr, true);
label_set(lb, rq, rr, 1);
height_set(hz, rq, rr, 3.5);
}
for d in 0..6 {
rq = nb_q(0, 0, d);
rr = nb_r(0, 0, d);
for e2 in 0..6 {
oq = nb_q(rq, rr, e2);
orr = nb_r(rq, rr, e2);
if !hexset_get(c, oq, orr) { edge_set_mat(eg, rq, rr, oq, orr, 1); }
}
}
st = stencil_with_edges(c, hz, lb, eg, true, true);
layer_add(st.sn_layers, "item");
layer_set(st.sn_layers, "item", 0, 0, 44);
st
}
fn test_rotation_loses_nothing_at_all() {
st = walled_ring();
n0 = hexset_count(st.sn_cells);
e0 = edgeset_count_all(st.sn_edges);
assert(e0 > 0, "the fixture has no walls");
for n in 1..6 {
rot = stencil_rotate(st, n);
assert(hexset_count(rot.sn_cells) == n0, "turn {n}: cells {hexset_count(rot.sn_cells)}, want {n0}");
assert(edgeset_count_all(rot.sn_edges) == e0,
"turn {n}: {edgeset_count_all(rot.sn_edges)} edge slots, want {e0} — a rotation lost walls");
assert(layer_count(rot.sn_layers) == 1, "turn {n}: lost the named layer");
(tq, tr) = cell_rot(0, 0, n);
assert(layer_get(rot.sn_layers, "item", tq, tr) == 44, "turn {n}: lost the item value");
}
}
fn test_mirror_loses_nothing_and_is_self_inverse() {
st = walled_ring();
m1 = stencil_mirror(st);
assert(hexset_count(m1.sn_cells) == hexset_count(st.sn_cells), "mirror lost cells");
assert(edgeset_count_all(m1.sn_edges) == edgeset_count_all(st.sn_edges),
"mirror lost walls: {edgeset_count_all(m1.sn_edges)} vs {edgeset_count_all(st.sn_edges)}");
m2 = stencil_mirror(m1);
assert(hexset_count(m2.sn_cells) == hexset_count(st.sn_cells), "mirroring twice lost cells");
for q in 0..3 {
for r in 0..3 {
assert(hexset_get(m2.sn_cells, q - 1, r - 1) == hexset_get(st.sn_cells, q - 1, r - 1),
"mirroring twice is not the identity at {q-1},{r-1}");
}
}
}
// ── what OVERLAP actually does ───────────────────────────────────────────
//
// ⚠ THE DESIGN SENTENCE WAS WRONG, AND MEASURING IT IS HOW WE KNOW. moros#5
// promised "two stencils overlapping at the same level arbitrate
// deterministically AND ORDER-FREELY — stamping A then B equals B then A".
// Half of that is true. Stamped both ways with different payloads on the
// shared cells, the occupancy agreed everywhere and SIX labels and SIX heights
// did not: a stamp is last-writer-wins, so the payload carries the order.
//
// Order-freedom for the payload was never the right rule anyway — it would make
// "place this on top of that" impossible, which is the one thing an editor's
// stamp is for. What survives is the pair below, and both halves are gated,
// because a mechanism that quietly became order-DEPENDENT in its occupancy
// would break stamping into a shared field and no count would notice.
fn overlap_pair(lab: integer, hgt: float) -> Stencil {
s = hexset_chunk(0, 0, 3, 3);
hz = heights_new(0, 0, 3, 3);
lb = labels_new(0, 0, 3, 3);
for q in 0..3 { for r in 0..3 {
hexset_set(s, q, r, true); label_set(lb, q, r, lab); height_set(hz, q, r, hgt);
} }
stencil_from(s, hz, lb, true, true)
}
fn test_overlapping_stamps_are_order_free_only_in_occupancy() {
a = overlap_pair(11, 1.5);
b = overlap_pair(22, 9.5);
c1 = hexset_chunk(-8,-8,30,30); h1 = heights_new(-8,-8,30,30); l1 = labels_new(-8,-8,30,30);
c2 = hexset_chunk(-8,-8,30,30); h2 = heights_new(-8,-8,30,30); l2 = labels_new(-8,-8,30,30);
stencil_stamp(c1, h1, l1, a, 0, 0);
stencil_stamp(c1, h1, l1, b, 1, 0);
stencil_stamp(c2, h2, l2, b, 1, 0);
stencil_stamp(c2, h2, l2, a, 0, 0);
// the two anchors are one apart, so the 3x3 blobs genuinely share cells —
// without this the test degrades into the disjoint case above and proves
// nothing about overlap at all
assert(hexset_count(c1) < 2 * 9, "the fixtures did not overlap: {hexset_count(c1)} cells");
shared = 0;
for qi in 0..30 { for ri in 0..30 {
q = qi - 8; r = ri - 8;
assert(hexset_get(c1,q,r) == hexset_get(c2,q,r), "occupancy depended on stamp order at {q},{r}");
if hexset_get(c1,q,r) && label_get(l1,q,r) != label_get(l2,q,r) { shared += 1; }
} }
assert(hexset_count(c1) == hexset_count(c2), "overlap changed the cell count with the order");
// and the payload DOES carry the order — the refuted half, pinned so that a
// future arbitration rule cannot land silently
assert(shared > 0, "labels no longer depend on stamp order — the last-writer-wins rule changed");
}
fn test_the_last_writer_owns_the_shared_payload() {
a = overlap_pair(11, 1.5);
b = overlap_pair(22, 9.5);
c = hexset_chunk(-8,-8,30,30); h = heights_new(-8,-8,30,30); l = labels_new(-8,-8,30,30);
stencil_stamp(c, h, l, a, 0, 0);
stencil_stamp(c, h, l, b, 1, 0);
// (1,0) is inside both footprints; B went second, so B owns it
assert(label_get(l, 1, 0) == 22, "the second stamp did not win the shared cell: {label_get(l, 1, 0)}");
assert(height_get(h, 1, 0) == 9.5, "the second stamp did not win the shared height");
// (0,0) is A's alone — B's footprint starts at its anchor
assert(label_get(l, 0, 0) == 11, "the second stamp reached a cell it does not cover");
}
// ── stamp then un-stamp, on free ground ──────────────────────────────────
//
// Invariant 3 of moros#5. Stated over the WHOLE payload — cells, heights,
// labels, named layers and the walls including the rim — because a version that
// restored the cells and left the walls standing passes every cell count.
// `walled_ring` is the fixture with something in every one of those, which is
// why it is reused here.
fn test_stamp_then_unstamp_restores_the_target_bit_for_bit() {
st = walled_ring();
c = hexset_chunk(-8,-8,20,20); hz = heights_new(-8,-8,20,20); lb = labels_new(-8,-8,20,20);
ly = layers_new(-8,-8,20,20); eg = edgeset_new(-8,-8,20,20);
// an ODD anchor row, the case a naive translation shears
stencil_stamp_all(c, hz, lb, ly, eg, st, 3, 3);
assert(hexset_count(c) == hexset_count(st.sn_cells), "the stamp did not land");
assert(edgeset_count_all(eg) > 0, "the stamp laid no walls, so un-stamping them proves nothing");
stencil_unstamp_all(c, hz, lb, ly, eg, st, 3, 3);
assert(hexset_count(c) == 0, "un-stamp left {hexset_count(c)} cells behind");
assert(edgeset_count_all(eg) == 0,
"un-stamp left {edgeset_count_all(eg)} edge slots behind — the rim walls survived");
for qi in 0..20 { for ri in 0..20 {
q = qi - 8; r = ri - 8;
assert(!hexset_get(c, q, r), "cell {q},{r} survived the un-stamp");
assert(height_get(hz, q, r) == 0.0, "height at {q},{r} is {height_get(hz, q, r)}, want 0.0");
assert(label_get(lb, q, r) == -1, "label at {q},{r} is {label_get(lb, q, r)}, want -1");
lv = layer_get(ly, "item", q, r);
assert(lv == 0, "layer at {q},{r} is {lv}, want 0");
} }
}
// The other half: an un-stamp must not reach past its own footprint.
fn test_unstamp_leaves_the_rest_of_the_target_alone() {
st = walled_ring();
c = hexset_chunk(0,0,20,20); hz = heights_new(0,0,20,20); lb = labels_new(0,0,20,20);
ly = layers_new(0,0,20,20); eg = edgeset_new(0,0,20,20);
hexset_set(c, 15, 15, true);
label_set(lb, 15, 15, 999);
height_set(hz, 15, 15, 7.25);
stencil_stamp_all(c, hz, lb, ly, eg, st, 4, 4);
stencil_unstamp_all(c, hz, lb, ly, eg, st, 4, 4);
assert(hexset_get(c, 15, 15), "an unrelated cell was cleared by an un-stamp");
assert(label_get(lb, 15, 15) == 999, "an unrelated label was cleared: {label_get(lb, 15, 15)}");
assert(height_get(hz, 15, 15) == 7.25, "an unrelated height was cleared");
assert(hexset_count(c) == 1, "un-stamp left {hexset_count(c)} cells, want just the unrelated one");
}
// Six rotations, and the other six by flipping — the twelve the design promises.
fn test_twelve_orientations_all_keep_the_whole_stencil() {
st = walled_ring();
n0 = hexset_count(st.sn_cells);
e0 = edgeset_count_all(st.sn_edges);
flipped = stencil_mirror(st);
for n in 0..6 {
a = stencil_rotate(st, n);
b = stencil_rotate(flipped, n);
assert(hexset_count(a.sn_cells) == n0, "orientation {n}: cells lost");
assert(edgeset_count_all(a.sn_edges) == e0, "orientation {n}: walls lost");
assert(hexset_count(b.sn_cells) == n0, "flipped orientation {n}: cells lost");
assert(edgeset_count_all(b.sn_edges) == e0, "flipped orientation {n}: walls lost");
}
}
tests/05-edges.loft
// Walls in stencils. An edge's identity is the doubled midpoint of its two cell
// centres, so it survives rotation and translation for the same reason a cell
// does — exactly, on the integer lattice.
use hex_field::*;
// A room: a 3x3 block with a wall on one specific edge.
fn walled() -> Stencil {
s = hexset_chunk(0, 0, 3, 3);
hz = heights_new(0, 0, 3, 3);
lb = labels_new(0, 0, 3, 3);
eg = edgeset_new(0, 0, 3, 3);
for q in 0..3 { for r in 0..3 { hexset_set(s, q, r, true); label_set(lb, q, r, 5); } }
// wall between (1,1) and its E neighbour
edge_set_mat(eg, 1, 1, nb_q(1, 1, 0), nb_r(1, 1, 0), 7);
stencil_with_edges(s, hz, lb, eg, false, true)
}
fn test_edge_key_is_symmetric() {
(ka, ma) = edge_key(1, 1, 2, 1);
(kb, mb) = edge_key(2, 1, 1, 1);
assert(ka == kb && ma == mb, "an edge has two identities depending on who names it");
}
fn test_a_wall_is_stored_once_and_read_from_either_side() {
eg = edgeset_new(0, 0, 4, 4);
nq = nb_q(1, 1, 0); nr = nb_r(1, 1, 0);
edge_set_mat(eg, 1, 1, nq, nr, 9);
assert(edge_mat(eg, 1, 1, nq, nr) == 9, "wall not readable from the near side");
assert(edge_mat(eg, nq, nr, 1, 1) == 9, "wall not readable from the far side");
assert(edgeset_count(eg) == 1, "one wall counted {edgeset_count(eg)} times");
}
fn test_rotation_carries_the_wall() {
st = walled();
assert(edgeset_count(st.sn_edges) == 1, "source lost its wall");
for n in 1..6 {
rot = stencil_rotate(st, n);
assert(edgeset_count(rot.sn_edges) == 1,
"rotating by {n} left {edgeset_count(rot.sn_edges)} walls, want 1");
// the wall must still sit between the ROTATED images of the same two cells
(aq, ar) = cell_rot(1, 1, n);
(bq, br) = cell_rot(nb_q(1, 1, 0), nb_r(1, 1, 0), n);
assert(edge_mat(rot.sn_edges, aq, ar, bq, br) == 7,
"at {n} turns the wall is not between the rotated cells");
}
}
fn test_stamping_carries_the_wall_onto_an_odd_row() {
st = walled();
dc = hexset_chunk(-6, -6, 24, 24);
dh = heights_new(-6, -6, 24, 24);
dl = labels_new(-6, -6, 24, 24);
de = edgeset_new(-6, -6, 24, 24);
stencil_stamp_edges(dc, dh, dl, de, st, 5, 3); // ODD anchor row
assert(edgeset_count(de) == 1, "stamped {edgeset_count(de)} walls, want 1");
// between the translated images of the same two cells
dk = lattice_k(5, 3); dm = lattice_m(5, 3);
(tq, tr) = lattice_to_cell(lattice_k(1, 1) + dk, lattice_m(1, 1) + dm);
(uq, ur) = lattice_to_cell(lattice_k(nb_q(1, 1, 0), nb_r(1, 1, 0)) + dk,
lattice_m(nb_q(1, 1, 0), nb_r(1, 1, 0)) + dm);
assert(edge_mat(de, tq, tr, uq, ur) == 7, "the wall did not land on the stamped edge");
}
fn test_walls_survive_the_document_format() {
st = walled();
p = "edge_rt.bin";
doc_write_edges(p, st.sn_cells, st.sn_heights, st.sn_labels, st.sn_edges, false, true, true);
d = doc_read(p);
assert(d.doc_code == HXF_OK, "refused {d.doc_code}");
assert(d.doc_has_e, "the EDGE section was lost");
assert(edgeset_count(d.doc_edges) == 1, "read back {edgeset_count(d.doc_edges)} walls, want 1");
assert(edge_mat(d.doc_edges, 1, 1, nb_q(1, 1, 0), nb_r(1, 1, 0)) == 7,
"the wall came back on the wrong edge or with the wrong material");
delete(p);
}
// A reader with no EDGE support must still load the rest — the whole point of
// sections carrying lengths.
fn test_a_file_with_walls_still_loads_its_cells() {
st = walled();
p = "edge_fwd.bin";
doc_write_edges(p, st.sn_cells, st.sn_heights, st.sn_labels, st.sn_edges, false, true, true);
d = doc_read(p);
assert(d.doc_code == HXF_OK, "refused {d.doc_code}");
assert(hexset_count(d.doc_cells) == 9, "cells lost when walls were present");
delete(p);
}
// The other sections have a length gate; this one did not, and removing the
// check broke no test until this existed. An EDGE payload is sized by the HALO
// grid ((w+2)(h+2)3), not w*h*3, so getting that wrong is easy and silent.
fn test_refuses_an_edge_section_of_the_wrong_length() {
p = "edge_badlen.bin";
{
f = file(p); f#format = LittleEndian;
f += (HXF_MAGIC as i32); f += (HXF_SCHEMA as i32);
f += (0 as i32); f += (0 as i32);
f += (3 as i32); f += (3 as i32); // 3x3 -> halo grid is 5x5x3 = 75
f += (2 as i32); f += (0 as i32);
f += (TAG_OCCU as i32); f += (9 as i32);
for _ in 0..9 { f += ((1 as u8?) ?? 0); }
f += (TAG_EDGE as i32); f += (27 as i32); // w*h*3 — the plausible wrong answer
for _ in 0..27 { f += ((0 as u8?) ?? 0); }
}
d = doc_read(p);
assert(d.doc_code == HXF_BAD_SECTION,
"an EDGE section sized w*h*3 instead of the halo grid gave {d.doc_code}");
delete(p);
}
// The second shared fixture: one committed file with a WALL in it, read by both
// consumers. `canonical.hxf` proved the cell layers were an interface rather
// than two implementations that happened to agree; this does the same for edges,
// which is the layer most likely to drift because its indexing is subtle.
const WALL_FIXTURE = "fixtures/walled.hxf";
fn test_committed_wall_fixture_loads() {
d = doc_read(WALL_FIXTURE);
assert(d.doc_code == HXF_OK, "the committed wall fixture no longer loads: {d.doc_code}");
assert(hexset_count(d.doc_cells) == 9, "cells: {hexset_count(d.doc_cells)}, want 9");
assert(d.doc_has_e, "the EDGE section is gone");
assert(edgeset_count(d.doc_edges) == 1, "walls: {edgeset_count(d.doc_edges)}, want 1");
// the wall sits on the E edge of (0,1) — an ODD row, at a negative-origin extent
assert(edge_mat(d.doc_edges, 0, 1, nb_q(0, 1, 0), nb_r(0, 1, 0)) == 12,
"the wall is not on the edge it was authored on");
// and NOTHING else is set anywhere in the layer, halo included — otherwise a
// corrupted halo slot rides along unnoticed
assert(edgeset_count_all(d.doc_edges) == 1,
"the layer carries {edgeset_count_all(d.doc_edges)} non-zero slots, want exactly 1");
}
fn test_rewriting_the_wall_fixture_reproduces_it() {
d = doc_read(WALL_FIXTURE);
assert(d.doc_code == HXF_OK, "refused {d.doc_code}");
tmp = "wall_rewrite.bin";
doc_write_edges(tmp, d.doc_cells, d.doc_heights, d.doc_labels, d.doc_edges,
d.doc_has_h, d.doc_has_l, d.doc_has_e);
fa = file(WALL_FIXTURE); fa#format = LittleEndian;
fb = file(tmp); fb#format = LittleEndian;
na = fa#size; nb = fb#size;
assert(na == nb, "rewrite is {nb} bytes, fixture is {na}");
for i in 0..na {
x = fa#read as u8;
y = fb#read as u8;
assert(x == y, "byte {i} differs — the edge writer has drifted from the fixture");
}
delete(tmp);
}
tests/06-layers.loft
// Named integer layers. The library holds "a named integer field over the
// chunk"; WHICH layers exist is the consumer's vocabulary — moros carries an
// item and its rotation, another game would carry something else.
use hex_field::*;
fn test_layers_are_independent_and_named() {
ls = layers_new(0, 0, 4, 4);
layer_add(ls, "item");
layer_add(ls, "rotation");
assert(layer_count(ls) == 2, "expected 2 layers, got {layer_count(ls)}");
layer_set(ls, "item", 1, 1, 7);
layer_set(ls, "rotation", 1, 1, 9);
assert(layer_get(ls, "item", 1, 1) == 7, "item layer lost its value");
assert(layer_get(ls, "rotation", 1, 1) == 9, "rotation layer lost its value");
assert(layer_get(ls, "item", 2, 2) == 0, "an unset cell is not zero");
// an unknown name reads 0 and writes nowhere, rather than aliasing layer 0
assert(layer_get(ls, "nonesuch", 1, 1) == 0, "an unknown layer aliased a real one");
layer_set(ls, "nonesuch", 1, 1, 42);
assert(layer_get(ls, "item", 1, 1) == 7, "writing an unknown layer corrupted a real one");
}
fn test_adding_an_existing_layer_is_a_no_op() {
ls = layers_new(0, 0, 3, 3);
a = layer_add(ls, "item");
layer_set(ls, "item", 0, 0, 5);
b = layer_add(ls, "item");
assert(a == b, "re-adding returned a different index");
assert(layer_count(ls) == 1, "re-adding created a second layer");
assert(layer_get(ls, "item", 0, 0) == 5, "re-adding cleared the layer");
}
fn withlayers() -> Stencil {
s = hexset_chunk(0, 0, 3, 3);
// Populate the field BEFORE constructing the stencil — stencil_from takes the
// field by value, so cells added afterwards do not reach it. An earlier
// version of this helper did it the other way round and produced an empty
// stencil that still reported the right layer COUNT, so only the values failed.
for q in 0..3 { hexset_set(s, q, 0, true); }
hexset_set(s, 0, 1, true);
st = stencil_from(s, heights_new(0, 0, 3, 3), labels_new(0, 0, 3, 3), false, false);
layer_add(st.sn_layers, "item");
layer_add(st.sn_layers, "rotation");
layer_set(st.sn_layers, "item", 2, 0, 31);
layer_set(st.sn_layers, "rotation", 2, 0, 9);
st
}
fn test_rotation_carries_named_layers() {
st = withlayers();
for n in 1..6 {
rot = stencil_rotate(st, n);
assert(layer_count(rot.sn_layers) == 2, "turn {n} lost a layer");
(tq, tr) = cell_rot(2, 0, n);
assert(layer_get(rot.sn_layers, "item", tq, tr) == 31,
"turn {n}: item did not follow its cell to {tq},{tr}");
assert(layer_get(rot.sn_layers, "rotation", tq, tr) == 9,
"turn {n}: rotation value did not follow its cell");
}
}
fn test_stamping_carries_named_layers_onto_an_odd_row() {
st = withlayers();
dc = hexset_chunk(-6, -6, 24, 24);
dh = heights_new(-6, -6, 24, 24);
dl = labels_new(-6, -6, 24, 24);
dy = layers_new(-6, -6, 24, 24);
stencil_stamp_layers(dc, dh, dl, dy, st, 5, 3);
dk = lattice_k(5, 3); dm = lattice_m(5, 3);
(tq, tr) = lattice_to_cell(lattice_k(2, 0) + dk, lattice_m(2, 0) + dm);
assert(layer_get(dy, "item", tq, tr) == 31, "item did not land at {tq},{tr}");
assert(layer_get(dy, "rotation", tq, tr) == 9, "rotation did not land");
}
fn test_named_layers_survive_the_document_format() {
st = withlayers();
p = "layers_rt.bin";
doc_write_all(p, st.sn_cells, st.sn_heights, st.sn_labels, st.sn_edges, st.sn_layers,
false, false, false);
d = doc_read(p);
assert(d.doc_code == HXF_OK, "refused {d.doc_code}");
assert(layer_count(d.doc_layers) == 2, "read back {layer_count(d.doc_layers)} layers, want 2");
assert(layer_index(d.doc_layers, "item") >= 0, "the layer NAME was lost");
assert(layer_index(d.doc_layers, "rotation") >= 0, "the second layer name was lost");
assert(layer_get(d.doc_layers, "item", 2, 0) == 31, "item value lost in the round-trip");
assert(layer_get(d.doc_layers, "rotation", 2, 0) == 9, "rotation value lost");
delete(p);
}
// A LAYR whose declared length disagrees with its name length is corrupt, and
// must be refused rather than read short.
fn test_refuses_a_layer_section_of_the_wrong_length() {
p = "layers_badlen.bin";
{
f = file(p); f#format = LittleEndian;
f += (HXF_MAGIC as i32); f += (HXF_SCHEMA as i32);
f += (0 as i32); f += (0 as i32);
f += (2 as i32); f += (2 as i32);
f += (2 as i32); f += (0 as i32);
f += (TAG_OCCU as i32); f += (4 as i32);
for _ in 0..4 { f += ((1 as u8?) ?? 0); }
f += (TAG_LAYR as i32);
f += (20 as i32); // claims 20; a 4-byte name + 4 cells needs 4+4+16 = 24
f += (4 as i32); f += "item";
for _ in 0..4 { f += (0 as i32); }
}
d = doc_read(p);
assert(d.doc_code == HXF_BAD_SECTION, "a short LAYR section gave {d.doc_code}");
delete(p);
}
// The old fixtures use TAG_LABL and no layers; they must still load untouched.
fn test_the_committed_fixtures_still_load_without_layers() {
d = doc_read("fixtures/canonical.hxf");
assert(d.doc_code == HXF_OK, "canonical.hxf broke: {d.doc_code}");
assert(layer_count(d.doc_layers) == 0, "it grew {layer_count(d.doc_layers)} layers from nowhere");
assert(d.doc_has_l, "its unnamed label layer was lost");
}
tests/07-worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for `hex_field` (@PLN141). Each `test_*` below is a REAL,
// run-every-CI demonstration of a contract the signature cannot state; the
// function documents itself by CITING the tag with `// Example: @HXL-0NN`, and
// `check_doc_drift.sh examples` fails if a citation goes dangling. These are
// teaching examples, distinct from the conformance suite in `01-hex-field.loft`.
//
// WHAT THIS PACKAGE ASKS OF A CALLER. Everything here is EXACT: a cell is
// (q,r), a corner is a lattice point (k,m), an edge is a pair of cells, and an
// area is an integer. There is no epsilon anywhere and none is wanted. Every
// trap below is one of three things:
//
// a UNIT the lattice's world unit is the hex CIRCUMRADIUS, so a hex STEP
// is sqrt(3) of them — and one of the two disc routines takes each
// a PREMISE a rule that is true of the shape the author had in mind and not
// of the shapes this package is built to hold (a bounded chunk
// holds several forms; a document arrives from somewhere else)
// a DOMAIN a slot narrower than the value offered to it, where the value
// out of range means something rather than failing
//
// The fixtures are written BY HAND and are as small as the claim allows: one
// hex, two hexes, a ring of six. A shrunken real thing would not be readable.
use hex_field::*;
// Names a scratch file AND clears any leftover from a run that died before its own
// cleanup. `file(path)` opens an existing file to APPEND, so without this a crashed
// run hands the next one a header with a header already in front of it — and the
// failure then lands in a different test from the one that caused it.
fn tmp(name: text) -> text {
p = "_tmp_hxl_{name}.bin";
delete(p);
p
}
const SQRT3 = 1.7320508075688772;
// One cell, on its own, in the smallest chunk that can hold it.
fn one_hex() -> HexSet {
s = hexset_chunk(0, 0, 1, 1);
hexset_set(s, 0, 0, true);
s
}
// A ring of six around an empty centre — the smallest form with a HOLE.
fn donut() -> HexSet {
s = hexset_chunk(-2, -2, 5, 5);
hexdisk_into(s, 0, 0, 1);
hexset_set(s, 0, 0, false);
s
}
// @HXL-001 — ONE HEX IS SIX NAMED LATTICE POINTS, and you can write them down.
//
// This is the whole representation in one value, which is why it is the first
// example rather than a footnote. A cell's centre is (2q + (r&1), 3r) and its
// six corners are (0,+-2) and (+-1,+-1) away from it, so the cell at the origin
// traces to exactly the points asserted below — no scaling, no rounding, nothing
// derived at run time. The order is the one `corner_k`/`corner_m` fixes, rotated
// to start at the loop's lexicographically smallest vertex, which is what makes
// two independent implementations produce the SAME array rather than the same
// shape.
fn test_one_hex_is_six_lattice_points() {
v = trace(one_hex());
assert(v.vm_loops == 1, "one cell is one loop, got {v.vm_loops}");
assert(len(v.vm_k) == 6, "and six vertices, got {len(v.vm_k)}");
ks: vector<integer> = [-1, 0, 1, 1, 0, -1];
ms: vector<integer> = [-1, -2, -1, 1, 2, 1];
for i in 0..6 {
assert((v.vm_k[i] ?? 99) == (ks[i] ?? 0) && (v.vm_m[i] ?? 99) == (ms[i] ?? 0),
"vertex {i} is ({v.vm_k[i] ?? 99},{v.vm_m[i] ?? 99}), want ({ks[i] ?? 0},{ms[i] ?? 0})");
}
// The canonical start is the SMALLEST vertex, not the one iteration happened to reach.
assert((v.vm_k[0] ?? 0) == -1 && (v.vm_m[0] ?? 0) == -1, "the loop starts at (-1,-1)");
}
// @HXL-002 — THE AREA ROUND-TRIP IS AN INTEGER IDENTITY, AND A HOLE IS A
// NEGATIVE ONE. A single hex shoelaces to exactly 12, so a form of n hexes
// shoelaces to exactly 12n whatever its shape — and a hole subtracts. The ring
// of six below is 6 cells: its outer loop encloses the 7-cell disc (84) and its
// hole gives back one cell (-12), which is 72 and nothing else. Because both
// sides are integers, "cell count and traced outline can never disagree" is a
// statement about equality, not about tolerance.
fn test_the_hole_is_a_negative_hex_and_the_areas_are_exact() {
s = donut();
assert(hexset_count(s) == 6, "a ring of six, got {hexset_count(s)}");
v = trace(s);
assert(v.vm_loops == 2, "an outer loop and a hole, got {v.vm_loops}");
outer = -1;
hole = -1;
for i in 0..2 {
if shoelace2(v, i) > 0 { outer = i; }
if shoelace2(v, i) < 0 { hole = i; }
}
assert(outer >= 0 && hole >= 0, "one of each");
assert(shoelace2(v, outer) == 84, "the outer loop encloses seven hexes: {shoelace2(v, outer)}");
assert(shoelace2(v, hole) == 0 - 12, "the hole gives one back: {shoelace2(v, hole)}");
assert(shoelace_total(v) == 72, "84 - 12 = 72");
assert(shoelace_total(v) == 12 * hexset_count(s), "and 72 is 12 x 6");
// A single hex is the unit of that identity.
assert(shoelace_total(trace(one_hex())) == 12, "one hex is exactly 12");
}
// @HXL-003 — A CHUNK MAY HOLD MORE THAN ONE FORM, AND THE VALIDATOR MUST SAY YES.
//
// This package traces BOUNDED CHUNKS on purpose — "the same code serves a 32x32
// world chunk and a one-off tower window" — so two buildings in one window, or one
// building the chunk edge cuts in half, is the ordinary case rather than an error.
// Every such map used to be refused with code 5, "wrong number of outer loops",
// while `trace` had produced it correctly and the areas summed exactly: two
// disjoint hexes are two loops of 12 each, and 24 is 24.
//
// The rule that refused them is a property of a CONNECTED form, and connectedness
// is not something a chunk gets to assume. So code 5 now means *no* outer loop,
// and a caller who knows its form is one piece keeps the stronger property by
// asking for it: `outline_count(v) == 1`.
fn test_a_chunk_may_hold_two_forms() {
s = hexset_chunk(0, 0, 9, 9);
hexset_set(s, 1, 1, true);
hexset_set(s, 7, 7, true); // far enough apart to share no edge
v = trace(s);
assert(v.vm_loops == 2, "two components, two loops");
assert(shoelace_total(v) == 24, "and 12 per hex: {shoelace_total(v)}");
assert(shoelace_total(v) == 12 * hexset_count(s), "the round-trip holds across components");
assert(validate(v, hexset_count(s)) == 0, "a two-form chunk is VALID, got {validate(v, hexset_count(s))}");
assert(outline_count(v) == 2, "and it reports two outlines, got {outline_count(v)}");
// One form cut in half by the chunk edge — the same shape, arrived at the other way.
c = hexset_chunk(0, 0, 7, 7);
for r in 0..7 { hexset_set(c, 0, r, true); hexset_set(c, 6, r, true); }
vc = trace(c);
assert(validate(vc, hexset_count(c)) == 0, "a clipped form is VALID, got {validate(vc, hexset_count(c))}");
assert(outline_count(vc) == 2, "in two pieces");
// And the single-piece caller still gets the strong answer.
assert(outline_count(trace(donut())) == 1, "a donut is ONE outline plus a hole");
}
// @HXL-004 — THE OUTLINE MUST NOT TOUCH ITSELF, AND THE CHEAP CHECKS CANNOT SEE IT.
//
// `trace` can never emit a pinch: a hex vertex touches three MUTUALLY ADJACENT
// cells, so a boundary cannot pass through the same point twice. That is exactly
// why the check belongs in `validate` rather than in the tracer — the maps that
// arrive from a file, or from a consumer's own builder, are the ones that can.
//
// The map below is two hexes emitted as two circuits joined at the corners they
// share, which is what a naive per-cell tracer produces. Every other check in
// `validate` passes it: twelve segments, each a legal hex edge; a shoelace of
// exactly 12 x 2; one positive loop. It is still unusable — the shared edge is
// traversed in both directions, a zero-width slit no triangulator can resolve.
fn test_a_figure_eight_passes_every_other_check() {
fk: vector<integer> = [1, 0, -1, -1, 0, 1, 1, 1, 2, 3, 3, 2];
fm: vector<integer> = [1, 2, 1, -1, -2, -1, 1, -1, -2, -1, 1, 2];
st: vector<integer> = [0, 12];
f8 = VecMap { vm_k: fk, vm_m: fm, vm_start: st, vm_loops: 1 };
assert(shoelace_total(f8) == 24, "the area is right: {shoelace_total(f8)}");
assert(shoelace_total(f8) == 12 * 2, "which is 12 per hex, for two hexes");
assert(outline_count(f8) == 1, "and there is one positive loop");
assert(validate(f8, 2) == 6, "yet it is refused as self-touching, got {validate(f8, 2)}");
// The honest trace of the same two cells: ONE loop of ten vertices, no repeats.
two = hexset_chunk(0, 0, 2, 1);
hexset_set(two, 0, 0, true);
hexset_set(two, 1, 0, true);
vt = trace(two);
assert(vt.vm_loops == 1, "two adjacent hexes are one loop");
assert(len(vt.vm_k) == 10, "of ten vertices — the two shared corners are visited once each");
assert(shoelace_total(vt) == 24, "same area");
assert(validate(vt, 2) == 0, "and it validates");
}
// @HXL-005 — `radius` IS NOT IN HEX STEPS, AND THE FACTOR IS sqrt(3).
//
// `form_hexdisk(w, n)` counts hex STEPS; `form_circle(w, radius)` measures LATTICE
// WORLD UNITS, the ones `x = k*sqrt(3)/2, y = m/2` defines — in which a hex has
// circumradius 1 and two neighbouring centres are sqrt(3) apart. Both are
// dimensionless, and they are not the same dimension.
//
// So the same `3` asks for two different discs, and neither call says so at the
// call site. Passing hex steps to `form_circle` gives you a disc 1.73x too small;
// the conversion is one multiplication and it is exact.
fn test_a_disc_radius_and_a_hex_distance_are_different_units() {
assert(hexset_count(form_hexdisk(8, 1)) == 7, "1 hex step out is 7 cells");
assert(hexset_count(form_hexdisk(8, 2)) == 19, "2 out is 19");
assert(hexset_count(form_hexdisk(8, 3)) == 37, "3 out is 37");
assert(hexset_count(form_circle(8, 1.0)) == 1, "radius 1.0 does not even reach a neighbour");
assert(hexset_count(form_circle(8, 2.0)) == 7, "radius 2.0 reaches the first ring and no further");
assert(hexset_count(form_circle(8, 3.0)) == 13, "radius 3.0 is 13 cells, NOT 37");
// The conversion, and the proof that it is the whole difference.
assert(hexset_count(form_circle(8, 1.0 * SQRT3)) == 7, "sqrt(3) world units is one hex step");
assert(hexset_count(form_circle(8, 2.0 * SQRT3)) == 19, "and two steps is 19");
assert(hexset_count(form_circle(8, 3.0 * SQRT3)) == 37, "and three is 37 — the same disc");
// `form_octagon`'s apothem is in the same world units.
assert(hexset_count(form_octagon(8, 3.0)) == 13, "an apothem of 3.0 is 13 cells");
assert(hexset_count(form_octagon(8, 3.0 * SQRT3)) == 37, "3 hex steps of apothem is 37");
}
// @HXL-006 — `hex_at` IS THE EXACT INVERSE OF THE CENTRE MAP, INCLUDING ON
// NEGATIVE ODD ROWS. It is the one routine here that leaves the integers — a
// world point is a float — so it is the one place a rounding convention can drift
// without anything else noticing. The dangerous half is the odd-row shift: `r & 1`
// and `r % 2` agree for every non-negative row and differ for every negative odd
// one, where loft's `%` answers -1, and a copy of this map that used `%` put every
// such row a full hex step west for a year with a green suite.
//
// So the sweep below covers both parities AND both signs, and asserts the identity
// rather than a sample of it.
fn test_hex_at_inverts_the_centre_map_on_both_signs() {
bad = 0;
neg_odd = 0;
for r in 0 - 6..7 {
for q in 0 - 6..7 {
px = (lattice_k(q, r) as float) * 0.8660254037844386;
py = (lattice_m(q, r) as float) * 0.5;
(aq, ar) = hex_at(px, py);
if aq != q || ar != r { bad = bad + 1; }
if r < 0 && (r & 1) == 1 { neg_odd = neg_odd + 1; }
}
}
assert(neg_odd == 39, "the sweep must actually contain negative odd rows, got {neg_odd}");
assert(bad == 0, "{bad} of 169 cells did not survive the round trip");
}
// @HXL-007 — SIX TURNS ARE THE IDENTITY, ON EVERY SLOT A STENCIL CARRIES.
//
// A 60-degree turn is the integer map k' = (k-m)/2, m' = (3k+m)/2, and both halves
// divide evenly because centres satisfy k == m (mod 2). So a rotation resamples
// nothing and six of them come back exactly — that is the property the exact
// lattice exists to give, and `edgeset_equal` (which compares BOTH edge slots, and
// the halo) is the comparator strong enough to state it.
//
// It has to be stated on both slots because it was true on only one: the walk
// carried each edge's MATERIAL and dropped its SURFACE, so a stencil holding
// derived geometry came back with the cells identical, the materials identical and
// the geometry gone — under a comment promising a stencil never loses data.
//
// The extent is the other half of the answer. A rotation re-fits the extent to
// what the rotated cells need, so it is the CONTENT that returns, not the box: a
// stencil authored in a roomy window comes back trimmed to its own occupied cells.
fn test_six_turns_return_every_slot() {
s = hexset_chunk(0, 0, 2, 1);
hexset_set(s, 0, 0, true);
hexset_set(s, 1, 0, true);
eg = edgeset_new(0, 0, 2, 1);
edge_set_both(eg, 0, 0, 1, 0, 7, 3); // surface 7, material 3, one wall
st = stencil_with_edges(s, heights_new(0, 0, 2, 1), labels_new(0, 0, 2, 1), eg, false, false);
assert(edgeset_surf_count(st.sn_edges) == 1, "one surface to lose");
assert(edgeset_count(st.sn_edges) == 1, "and one material");
r6 = stencil_rotate(st, 6);
assert(hexset_count(r6.sn_cells) == 2, "the cells come back");
assert(edge_mat(r6.sn_edges, 0, 0, 1, 0) == 3, "the material comes back");
assert(edge_surf(r6.sn_edges, 0, 0, 1, 0) == 7, "and so does the surface");
assert(edgeset_surf_count(r6.sn_edges) == 1, "counted, not just stored");
assert(edgeset_equal(st.sn_edges, r6.sn_edges), "six turns are the identity on the whole layer");
// Two reflections likewise.
m2 = stencil_mirror(stencil_mirror(st));
assert(edgeset_equal(st.sn_edges, m2.sn_edges), "and so are two reflections");
// One turn is NOT the identity — the gate above would pass on a rotation that did nothing.
r1 = stencil_rotate(st, 1);
assert(!edgeset_equal(st.sn_edges, r1.sn_edges), "one turn must move the wall");
assert(edgeset_count(r1.sn_edges) == 1, "without losing it");
// The extent follows the CELLS, so it is content that returns and not the box.
roomy = hexset_chunk(0 - 3, 0 - 3, 7, 7);
hexset_set(roomy, 0, 0, true);
wide = stencil_from(roomy, heights_new(0 - 3, 0 - 3, 7, 7), labels_new(0 - 3, 0 - 3, 7, 7),
false, false);
back = stencil_rotate(wide, 6);
assert(hexset_count(back.sn_cells) == 1, "the one cell survives");
assert(hexset_w(back.sn_cells) == 1 && hexset_h(back.sn_cells) == 1,
"and the 7x7 window is trimmed to 1x1, not restored");
}
// @HXL-008 — ONE WALL, TWO NAMES, ONE SLOT — AND THE HALO IS WHY A RIM WALL EXISTS.
//
// An edge's identity is the DOUBLED MIDPOINT of the two cell centres, which is
// symmetric in the two cells: whichever side names it, it is the same edge and the
// same stored slot. That is what makes a wall a property of the seam rather than
// of a cell, so two neighbours cannot disagree about it.
//
// The halo is the other half. Exactly one of an edge's two cells owns the slot, so
// for a wall on the chunk RIM the owner is a cell OUTSIDE the extent — which is why
// the layer is (w+2) x (h+2) and why a loop over the extent alone silently drops
// every rim wall.
fn test_an_edge_has_one_identity_and_the_rim_is_inside_the_halo() {
(ka, ma) = edge_key(0, 0, 1, 0);
(kb, mb) = edge_key(1, 0, 0, 0);
assert(ka == 2 && ma == 0, "the doubled midpoint of (0,0) and (1,0) is (2,0), got ({ka},{ma})");
assert(ka == kb && ma == mb, "and naming it the other way round gives the same key");
e = edgeset_new(0, 0, 3, 3);
edge_set_mat(e, 0, 0, 1, 0, 4);
assert(edge_mat(e, 1, 0, 0, 0) == 4, "written from one side, read from the other");
assert(edgeset_count(e) == 1, "and stored ONCE, not once per side");
// A wall on the rim: its owner is outside the extent and it is still stored.
edge_set_mat(e, 0 - 1, 0, 0, 0, 5);
assert(edge_mat(e, 0, 0, 0 - 1, 0) == 5, "a rim wall lives in the halo");
assert(edgeset_count_all(e) == 2, "and `count_all` sees it, got {edgeset_count_all(e)}");
assert(edgeset_count(e) == 1, "while `count` walks in-chunk cells only, got {edgeset_count(e)}");
}
// @HXL-009 — A MATERIAL PAST THE SLOT IS REFUSED, BECAUSE NARROWING IT WOULD MEAN
// "NO WALL".
//
// The material slot is one byte and the narrowing cast is CHECKED, so a value it
// cannot hold answers null rather than its low byte — and the `?? 0` behind such a
// cast writes 0, which in this layer is not a fallback value but the ABSENCE of a
// wall. `edge_set_mat(e, ..., 300)` therefore used to delete the wall it was
// setting, and read back as open ground.
//
// A label is the deliberate contrast: same package, same per-cell shape, i32 slot,
// so a label of 301 is a label of 301. The two domains differ and only one of them
// has a value that means "nothing here".
fn test_a_material_past_the_slot_is_refused_not_narrowed() {
e = edgeset_new(0, 0, 2, 1);
edge_set_mat(e, 0, 0, 1, 0, EDGE_MAT_MAX);
assert(edge_mat(e, 0, 0, 1, 0) == 255, "255 is a material");
assert(edgeset_refused(e) == 0, "and nothing was refused");
edge_set_mat(e, 0, 0, 1, 0, EDGE_MAT_MAX + 1);
assert(edge_mat(e, 0, 0, 1, 0) == 255, "256 leaves the wall standing, got {edge_mat(e, 0, 0, 1, 0)}");
edge_set_mat(e, 0, 0, 1, 0, 0 - 1);
assert(edge_mat(e, 0, 0, 1, 0) == 255, "and so does a negative id");
assert(edgeset_refused(e) == 2, "both refusals are counted, got {edgeset_refused(e)}");
assert(edgeset_count(e) == 1, "and the wall is still there");
// 0 is in the domain: it is how a wall is REMOVED, not an out-of-range value.
edge_set_mat(e, 0, 0, 1, 0, 0);
assert(edgeset_count(e) == 0, "0 opens the edge");
assert(edgeset_refused(e) == 2, "and is not a refusal");
// The contrast: a label is i32, so a value past a byte is just a value.
lb = labels_new(0, 0, 2, 1);
label_set(lb, 0, 0, 301);
assert(label_get(lb, 0, 0) == 301, "a label of 301 is a label of 301");
}
// @HXL-010 — THE READER REFUSES; IT NEVER PARTLY PARSES. A half-read field is
// worse than no field, so every fault below returns a CODE and an untouched
// document rather than whatever it managed to read.
//
// Two of these are the format's design rather than its defence. An unknown section
// is SKIPPED by its declared length, which is why a field written by a newer
// consumer still loads in an older one — a flags word could not do that, because an
// unknown bit means an array of unknown length. And the section lengths are checked
// against w*h, which is how truncation is detected at all.
fn test_the_reader_refuses_rather_than_partly_parsing() {
bad_magic = tmp("magic");
raw_header(bad_magic, 0x99999999, HXF_SCHEMA, 0, 0, 2, 2, 0, 0);
assert(doc_read(bad_magic).doc_code == HXF_BAD_MAGIC, "a foreign file is refused");
delete(bad_magic);
bad_schema = tmp("schema");
raw_header(bad_schema, HXF_MAGIC, 99, 0, 0, 2, 2, 0, 0);
assert(doc_read(bad_schema).doc_code == HXF_BAD_SCHEMA, "a future schema is refused, not guessed");
delete(bad_schema);
bad_rsv = tmp("rsv");
raw_header(bad_rsv, HXF_MAGIC, HXF_SCHEMA, 0, 0, 2, 2, 0, 7);
assert(doc_read(bad_rsv).doc_code == HXF_BAD_RESERVED, "the reserved word must be 0");
delete(bad_rsv);
// A section whose declared length disagrees with the extent.
wrong_len = tmp("seclen");
f = file(wrong_len);
f#format = LittleEndian;
put_header(f, 0, 0, 2, 2, 1);
f += (TAG_OCCU as i32);
f += (3 as i32); // 3 bytes for a 4-cell extent
for _ in 0..3 { f += ((0 as u8?) ?? 0); }
assert(doc_read(wrong_len).doc_code == HXF_BAD_SECTION, "a section length must match w*h");
delete(wrong_len);
// A section that runs off the end of the file.
cut = tmp("cut");
g = file(cut);
g#format = LittleEndian;
put_header(g, 0, 0, 2, 2, 1);
g += (TAG_OCCU as i32);
g += (4 as i32); // declares 4, supplies 2
for _ in 0..2 { g += ((0 as u8?) ?? 0); }
assert(doc_read(cut).doc_code == HXF_TRUNCATED, "a short read is a refusal");
delete(cut);
// Occupancy is mandatory: a document without it is not a field.
no_occu = tmp("noocc");
h = file(no_occu);
h#format = LittleEndian;
put_header(h, 0, 0, 2, 2, 1);
h += (0x5A5A5A5A as i32); // a tag this reader does not know
h += (4 as i32);
for _ in 0..4 { h += ((1 as u8?) ?? 0); }
d = doc_read(no_occu);
assert(d.doc_code == HXF_MISSING_OCCU, "no OCCU section is a refusal");
delete(no_occu);
// FORWARD COMPATIBILITY: the same unknown section, skipped, beside a real one.
fwd = tmp("fwd");
k = file(fwd);
k#format = LittleEndian;
put_header(k, 0, 0, 2, 2, 2);
k += (0x5A5A5A5A as i32);
k += (4 as i32);
for _ in 0..4 { k += ((1 as u8?) ?? 0); }
k += (TAG_OCCU as i32);
k += (4 as i32);
cells: vector<integer> = [1, 0, 0, 1];
for i in 0..4 { k += (((cells[i] ?? 0) as u8?) ?? 0); }
fd = doc_read(fwd);
assert(fd.doc_code == HXF_OK, "a newer writer's file still loads, got {fd.doc_code}");
assert(fd.doc_skipped == 1, "and says what it skipped, got {fd.doc_skipped}");
assert(hexset_count(fd.doc_cells) == 2, "while the section it DID know was read");
assert(hexset_get(fd.doc_cells, 0, 0) && hexset_get(fd.doc_cells, 1, 1),
"at the two cells the bytes name");
delete(fwd);
}
// @HXL-011 — THE EXTENT IS CHECKED AGAINST THE FILE, NOT TAKEN ON TRUST.
//
// `w` and `h` are the two header fields that decide how much this reader ALLOCATES,
// and they were the only part of the header nothing verified: a 32-byte file
// claiming 4000 x 4000 built sixteen million cells — plus heights, labels, edges and
// layers over the same extent — and only then reported a missing section. The
// format's own doctrine already says why that is wrong: explicit dimensions exist so
// truncation can be detected, and a dimension nobody checks detects nothing.
//
// The bound needs no second pass. Occupancy is mandatory and costs one byte per
// cell, so a file can never describe more cells than it has bytes.
fn test_the_header_cannot_ask_for_more_than_the_file_holds() {
huge = tmp("huge");
raw_header(huge, HXF_MAGIC, HXF_SCHEMA, 0, 0, 4000, 4000, 0, 0);
hf = file(huge);
assert(hf#size == 32, "the whole file is a header: {hf#size} bytes");
assert(doc_read(huge).doc_code == HXF_BAD_EXTENT,
"a 32-byte file cannot hold 16 million cells");
delete(huge);
neg = tmp("neg");
raw_header(neg, HXF_MAGIC, HXF_SCHEMA, 0, 0, 0 - 3, 4, 0, 0);
assert(doc_read(neg).doc_code == HXF_BAD_EXTENT, "and an extent cannot be negative");
delete(neg);
// The bound is not so tight that it refuses real files: a written document loads.
ok = tmp("ok");
s = hexset_chunk(2, 3, 5, 4);
for q in 0..5 { for r in 0..4 { if (q + r) % 2 == 0 { hexset_set(s, 2 + q, 3 + r, true); } } }
doc_write(ok, s, heights_new(2, 3, 5, 4), labels_new(2, 3, 5, 4), false, false);
d = doc_read(ok);
assert(d.doc_code == HXF_OK, "a real document still loads, got {d.doc_code}");
assert(hexset_count(d.doc_cells) == hexset_count(s), "with its cells");
delete(ok);
}
// @HXL-012 — THE WALL LAYER IS WRITTEN BY WHICH EDGE IT IS, NOT BY WHERE IT SITS.
//
// The EDGE section's LENGTH comes from the field's extent, `(w+2)*(h+2)*3`. Copying
// the source layer's vector into it therefore assumes the two agree — and when they
// do not, every material lands at a slot that means a DIFFERENT edge. The file
// loads cleanly, the wall count is right, and the walls are somewhere else.
//
// Keying each byte by the two cells its edge lies between removes the assumption:
// the bytes are identical whenever the extents agree (the committed `walled.hxf`
// fixture is that gate), and correct when they do not.
fn test_a_wall_keeps_its_identity_across_a_layer_of_another_extent() {
s = hexset_chunk(0, 0, 3, 2);
for q in 0..3 { for r in 0..2 { hexset_set(s, q, r, true); } }
// A layer built at ANOTHER extent — 2x1, so its row stride is 4 and the field's is 5.
narrow = edgeset_new(0, 0, 2, 1);
edge_set_mat(narrow, 0, 0, 1, 0, 9);
assert(edgeset_w(narrow) != hexset_w(s), "the extents really do differ");
p = tmp("stride");
doc_write_edges(p, s, heights_new(0, 0, 3, 2), labels_new(0, 0, 3, 2), narrow, false, false, true);
d = doc_read(p);
assert(d.doc_code == HXF_OK, "the file loads: {d.doc_code}");
assert(d.doc_has_e, "with its wall layer");
assert(edge_mat(d.doc_edges, 0, 0, 1, 0) == 9,
"and the wall is on the edge it was set on, got {edge_mat(d.doc_edges, 0, 0, 1, 0)}");
assert(edgeset_count_all(d.doc_edges) == 1, "exactly one wall, not one somewhere else");
delete(p);
// The same wall through a layer that DOES match — the two must agree.
same = edgeset_new(0, 0, 3, 2);
edge_set_mat(same, 0, 0, 1, 0, 9);
pmatch = tmp("match");
doc_write_edges(pmatch, s, heights_new(0, 0, 3, 2), labels_new(0, 0, 3, 2), same, false, false, true);
e = doc_read(pmatch);
assert(edge_mat(e.doc_edges, 0, 0, 1, 0) == 9, "the matching layer round-trips too");
assert(edgeset_equal(d.doc_edges, e.doc_edges), "and both files describe the SAME layer");
delete(pmatch);
}
// ── raw-file helpers, so the reader can meet a header nobody wrote ──────────
fn put_header(f: File, q0: integer, r0: integer, w: integer, h: integer, nsec: integer) {
f += (HXF_MAGIC as i32);
f += (HXF_SCHEMA as i32);
f += (q0 as i32);
f += (r0 as i32);
f += (w as i32);
f += (h as i32);
f += (nsec as i32);
f += (0 as i32);
}
fn raw_header(path: text, magic: integer, schema: integer, q0: integer, r0: integer,
w: integer, h: integer, nsec: integer, rsv: integer) {
f = file(path);
f#format = LittleEndian;
f += (magic as i32);
f += (schema as i32);
f += (q0 as i32);
f += (r0 as i32);
f += (w as i32);
f += (h as i32);
f += (nsec as i32);
f += (rsv as i32);
}
tests/08-hex-grid-parity.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// `hex_field` is a leaf: it does not depend on `hex_grid`, so `lattice_k` / `lattice_m` and
// `nb_q` / `nb_r` restate `hex_grid`'s convention (pointy-top, odd-r offset). Two copies of a
// convention drift silently — an odd-row shift written `r % 2` instead of `r & 1` agrees for
// every non-negative row and is a full hex step off on every negative odd one. This test is
// what keeps the copies equal: both parities and both signs, every cell and every neighbour.
use hex_field::*;
use hex_grid::*;
fn test_the_lattice_map_is_hex_grids_world_position() {
for r in -5..5 { for q in -5..5 {
(x, y) = hex_grid::hex_to_px(q, r);
assert(abs((lattice_k(q, r) as float) * 0.8660254037844386 - x) < 0.000000001,
"x of ({q}, {r}): lattice {lattice_k(q, r)} against hex_grid {x}");
assert(abs((lattice_m(q, r) as float) * 0.5 - y) < 0.000000001,
"y of ({q}, {r}): lattice {lattice_m(q, r)} against hex_grid {y}");
} }
}
fn test_every_neighbour_is_one_of_hex_grids() {
for r in -5..5 { for q in -5..5 {
for d in 0..6 {
nq = nb_q(q, r, d);
nr = nb_r(q, r, d);
found = false;
for g in 0..6 {
(gq, gr) = hex_grid::hex_neighbor(q, r, g);
if gq == nq && gr == nr { found = true; }
}
assert(found, "neighbour {d} of ({q}, {r}) is ({nq}, {nr}), not one of hex_grid's six");
}
} }
}