A pointy-top hexagon with three printed paths
Each tile is a regular hexagon with its six sides numbered 0–5 clockwise, starting at the upper-right. Three paths are printed inside, each connecting a pair of sides:
- 0↔1 — adjacent sides, tight 120° corner-cut arc.
- 2↔4 — skip-one sides, wide 60° arc that sweeps through the far interior.
- 3↔5 — skip-one sides, same geometry as 2↔4 — it passes under the green arc at their crossing.
Together the three paths form a perfect matching of the
six sides: every side belongs to exactly one path. Rotation by one step
(60° clockwise) relabels every physical side
i → (i+1) mod 6,
shifting which sides each path connects — but the arc geometry stays fixed
inside the tile frame. There are therefore 6 distinct rotations
(0–5), each producing a different matching of the physical sides.
The over/under rule at the crossing is fixed by the printing: the green 2↔4 arc is always drawn over the blue 3↔5 arc. This becomes important for topology (knots) later; for connectivity it makes no difference, because the crossing does not merge the two strands.
Arc ribbons crossing edges perpendicularly
The rendered ribbon for each path is a circular arc strip with a fixed width (radius ± k/6 around the arc centreline, where k is the circumradius). The arc centre is chosen so that the ribbon meets each hex side at a right angle. This is the key geometric property that makes tiles visually seamless across shared edges.
The construction is the same for both arc types:
- Extend each of the two connected sides as a line into the plane.
- Find their intersection — that is the arc centre O.
- The arc centreline radius is r = |O – midpoint of side|.
Because O lies on the extension of each side-line, the vector from O to any side midpoint is perpendicular to that side — so the arc naturally arrives perpendicular to the edge. For the adjacent pair 0↔1, the lines intersect at a shared vertex (giving a tight arc, r ≈ k/3); for skip-one pairs the intersection is far outside the tile, giving the wide sweeping arcs (r ≈ 3k/2).
Placing tiles on a lattice
Tiles sit on the axial hex lattice. Each cell is identified by an integer pair (col, row), and its pixel centre is
Side e (0–5) of cell (q, r) faces the neighbour at (q+Δq, r+Δr):
| Side e | Screen direction | (Δcol, Δrow) |
|---|---|---|
| 0 | 30° upper-right | (+1, −1) |
| 1 | 90° right | (+1, 0) |
| 2 | 150° lower-right | (0, +1) |
| 3 | 210° lower-left | (−1, +1) |
| 4 | 270° left | (−1, 0) |
| 5 | 330° upper-left | (0, −1) |
When two tiles share an edge, the arc-ends from both sides meet at the same edge midpoint, and their perpendicular arrival guarantees a smooth joint. Gluing is purely by matching: tile A's side e connects to tile B's side (e+3) mod 6 (the opposite side).
Reduction to a union-find over lattice edges
Because crossings are over/under only — they never merge strands — the entire connectivity of any tiling reduces to a graph on the edges of the hex lattice itself. Each lattice edge is a potential arc endpoint; tiles connect them.
This is both the computational model and the conceptual one. Two quantities are tracked as tiles are placed:
paths— number of open (non-cyclic) components. Both ends of every open path sit on boundary lattice edges.loops— number of closed loops found so far.
They satisfy a simple accounting rule. Each new tile registers up to 6
new lattice-edge nodes and merges up to 3 pairs. If a merge joins two
nodes in the same component, loops increments; otherwise
the two components merge and comps decrements. The
invariant comps = paths + loops is maintained throughout.
Analytic checks
- Tile 1 (isolated): 6 new edges, 3 merges, no same-component pair.
Result:
comps=3, loops=0, paths=3. - Tile 2 (shares 1 edge with tile 1): 5 new edges, one
pre-existing edge. After 3 merges:
paths=5, loops=0. - Tile 3 (completing a triangle — shares 2 edges,
one from each of the first two tiles): a loop closes if and only if
all three tiles send the same-corner arc through the shared central
vertex. Probability
(1/6)³ = 1/216. Either way,paths=6after.
paths = B/2
There is a clean topological identity that holds for any filled convex patch, regardless of the individual rotations chosen:
where B is the number of boundary lattice edges (sides that face an unoccupied cell). The proof is a two-line counting argument: every open path component has exactly two ends, and ends can only sit on boundary edges; conversely every boundary edge belongs to exactly one open path end. So paths × 2 = B.
For an N-tile patch laid out as a filled hex spiral, the boundary length is B ≈ 2√3·√N (the perimeter of a hex disc). Therefore:
- Open paths grow like √N — a boundary effect, ultimately negligible as the patch grows.
- Interior edges (loop opportunities) grow like 3N
— so
loopsis expected to grow linearly. - The Euler characteristic pins
pathsbut leavesloopsgenuinely random — it must be determined by simulation.
paths = B/2 is verified immediately in the
3-tile triangle: B = 12 boundary sides (each tile
has 6 sides, 3 pairs are shared, so 18−6=12), giving
paths = 6. The widget below confirms this.
The first possible loop: 1 in 216 triples
The earliest a loop can form is when the third tile is placed, completing the smallest possible enclosed region — a triangle of three tiles sharing one central vertex. There are 6³ = 216 equally likely rotation triples for three randomly-placed tiles. Exactly 1 closes a loop.
The argument: for the loop to close at tile 3, each of the three tiles must route the red 0↔1 adjacent arc through the shared vertex. The probability of a single tile doing this is 1/6 (only one of the 6 rotations places the corner-cut arc at that vertex). Independent tiles give (1/6)³ = 1/216.
The unique looping triple for the canonical triangle (0,0)-(1,0)-(0,1) is rotations (1, 3, 5) — each tile's adjacent arc meets at the shared internal vertex.
The widget above runs an exhaustive search over all 216 triples on page load and reports the count in the footer. Try rotating the tiles by hand to find the loop, or press "Find the loop" to jump directly to (1, 3, 5).
The shared simulation engine
All interactive widgets on this site use the same two JavaScript modules, ported directly from the Python ground-truth:
hexsim.js(window.Hex) — the connectivity engine:OFFSETS,matching(rot),edgeKey(cell, e),spiral(n),UnionFind,Sim,classify(sim),mulberry32(seed).render.js(window.HexRender) — geometry and colour:tileCenter,hexVertices,sideMidpoint,ribbonGeometry,palette, plus canvas helpersdrawTile/drawRibbon.
A node self-test (u_engine_selftest.mjs) verifies the
JS port reproduces the Python results exactly: tile 1/2/3 path counts,
the 1/216 triangle, and loop counts matching on a seeded 2000-tile spiral.