Files
puzzles/aux/doc/hats.html
Simon Tatham 8d6647548f Loopy / grid.c: new grid type, 'Hats'.
The big mathematical news this month is that a polygon has been
discovered that will tile the plane but only aperiodically. Penrose
tiles achieve this with two tile types; it's been an open question for
decades whether you could do it with only one tile. Now someone has
announced the discovery of such a thing, so _obviously_ this
mathematically exciting tiling ought to be one of the Loopy grid
options!

The polygon, named a 'hat' by its discoverers, consists of the union
of eight cells of the 'Kites' periodic tiling that Loopy already
implements. So all the vertex coordinates of the whole tiling are
vertices of the Kites grid, which makes handling the coordinates in an
exact manner a lot easier than Penrose tilings.

What's _harder_ than Penrose tilings is that, although this tiling can
be generated by a vaguely similar system of recursive expansion, the
expansion is geometrically distorting, which means you can't easily
figure out which tiles can be discarded early to save CPU. Instead
I've come up with a completely different system for generating a patch
of tiling, by using a hierarchical coordinate system to track a
location within many levels of the expansion process without ever
simulating the process as a whole. I'm really quite pleased with that
technique, and am tempted to try switching the Penrose generator over
to it too - except that we'd have to keep the old generator around to
stop old game ids being invalidated, and also, I think it would be
slightly trickier without an underlying fixed grid and without
overlaps in the tile expansion system.

However, before coming up with that, I got most of the way through
implementing the more obvious system of actually doing the expansions.
The result worked, but was very slow (because I changed approach
rather than try to implement tree-pruning under distortion). But the
code was reusable for two other useful purposes: it generated the
lookup tables needed for the production code, and it also generated a
lot of useful diagrams. So I've committed it anyway as a supporting
program, in a new 'aux' source subdirectory, and in aux/doc is a
writeup of the coordinate system's concepts, with all those diagrams.
(That's the kind of thing I'd normally put in a huge comment at the
top of the file, but doing all those diagrams in ASCII art would be
beyond miserable.)

From a gameplay perspective: the hat polygon has 13 edges, but one of
them has a vertex of the Kites tiling in the middle, and sometimes two
other tile boundaries meet at that vertex. I've chosen to represent
every hat as having degree 14 for Loopy purposes, because if you only
included that extra vertex when it was needed, then people would be
forever having to check whether this was a 13-hat or a 14-hat and it
would be nightmarish to play.

Even so, there's a lot of clicking involved to turn all those fiddly
individual edges on or off. This grid is noticeably nicer to play in
'autofollow' mode, by setting LOOPY_AUTOFOLLOW in the environment to
either 'fixed' or 'adaptive'. I'm tempted to make 'fixed' the default,
except that I think it would confuse players of ordinary square Loopy!
2023-03-26 20:32:38 +01:00

259 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8" />
<title>Generating hat tilings for Loopy</title>
<style type="text/css">
table, tbody, tr, td, th {
border: 1px solid rgba(0, 0, 0, 0.3);
border-collapse: collapse;
}
table.noborder, table.noborder tbody, table.noborder td,
table.noborder th, table.noborder tr { border: none; }
table {
margin: 1em;
}
th, td {
padding: 0.5em;
}
</style>
</head>
<body>
<h1>Generating hat tilings for Loopy</h1>
<p>The <a href="https://arxiv.org/abs/2303.10798">original paper</a>
describes a method for generating hat tilings from a system of four
'metatiles'. You can start with any one of the four tiles, and then
recursively apply a set of expansion rules that turn each tile into
a collection of smaller tiles from the same set.</p>
<p>This table shows the four tiles, with their one-letter names as
given in the paper, and how each one is expanded.</p>
<p>All the tiles have a significant orientation. The H, T and P
tiles are marked with arrows to indicate the orientation. The F tile
is asymmetric, so no arrow is needed.</p>
<p>I've assigned each tile in each expansion a number, which Loopy
will use for its coordinate system.</p>
<table>
<tr>
<th>Tile name</th>
<th>Single tile</th>
<th>Expansion</th>
<tr>
<td>H</td>
<td><img src="single-H.svg"></td>
<td><img src="expanded-H.svg"></td>
</tr>
<tr>
<td>T</td>
<td><img src="single-T.svg"></td>
<td><img src="expanded-T.svg"></td>
</tr>
<tr>
<td>P</td>
<td><img src="single-P.svg"></td>
<td><img src="expanded-P.svg"></td>
</tr>
<tr>
<td>F</td>
<td><img src="single-F.svg"></td>
<td><img src="expanded-F.svg"></td>
</tr>
</table>
<p><strong>Note that these expansions overlap</strong>. When two
adjacent metatiles are expanded, the outer layer of P and F tiles in
their expansions must be placed so that they overlap each other. The
original paper suggests a set of tiles to remove from these
expansions so that each metatile expands to a <em>disjoint</em> set
of smaller tiles. In our implementation, however, we prefer to keep
the overlap, because our coordinate system will use it.</p>
<p>Once you've generated a large enough patch of metatiles for your
needs, the final step is to convert it into the actual hat tiles.
The expansion of each metatile into hats is shown here. Again, I've
assigned numbers to each hat for coordinate-system purposes:</p>
<table>
<tr>
<th>Tile name</th>
<th>Conversion into hats</th>
<tr>
<td>H</td>
<td><img src="single-H.svg"></td>
<td><img src="hats-single-H.svg"></td>
</tr>
<tr>
<td>T</td>
<td><img src="single-T.svg"></td>
<td><img src="hats-single-T.svg"></td>
</tr>
<tr>
<td>P</td>
<td><img src="single-P.svg"></td>
<td><img src="hats-single-P.svg"></td>
</tr>
<tr>
<td>F</td>
<td><img src="single-F.svg"></td>
<td><img src="hats-single-F.svg"></td>
</tr>
</table>
<p>(The hat in the middle of the H is shaded to indicate that it's
one of the rare reflected ones. All the other hats are rotations of
each other.)</p>
<p>Given all of this, an obvious approach to generating a random
patch of hat tiling would be to start with a single metatile,
iterate the expansion process a few times until you have a tiled
area much larger than you need, and then pick a subrectangle of
it at random.</p>
<p>Loopy's algorithm for generating Penrose tilings (which admit a
similar, though less complicated, expansion system) works in exactly
this way.</p>
<p>One problem with that algorithm is that it spends a lot of effort
on generating huge areas of tiles that aren't actually needed. So
you'd prefer to adjust the algorithm so that at every stage of
expansion it spots tiles completely outside the target rectangle,
and throws them away <em>before</em> spending 5 iterations on
exponentially expanding them into huge amounts of detail that will
only be thrown away anyway later.</p>
<p>That works well for Penrose tilings, because there, the expansion
procedure is geometrically precise: coordinates in the expanded
tiling are scaled up by an exact factor from coordinates in the
original tiling. So at every stage it's easy to know exactly where
your target rectangle <em>is</em>, and discard things that don't
overlap it.</p>
<p>But the metatiles shown here don't have that property. The tiles
distort slightly as they expand. The <em>topological</em> properties
of the expanded tiling match the original (which expanded tiles
connect to each other), but the geometry (precise distances) is
different. So it would be harder to implement the pruning for this
tiling. The target rectangle might not even be rectangular in every
iteration!</p>
<p>Instead, I came up with a completely different mechanism, by
devising a coordinate system to track our position within multiple
layers of tile expansion. This allows us to generate precisely the
area of tiling we need, and waste no effort at all on anything
outside the target region.</p>
<p>We begin by assigning an integer index to each kite making up an
individual hat:</p>
<img src="hat-kites.svg">
<p>(For a reflected hat, these indices work in mirror image, so that
for example 5 is still the kite in the middle.)</p>
<p>Together with the indices we've assigned to hats within each
metatile, and to metatiles in the expansion of another metatile,
this gives us a coordinate system that can identify an individual
kite in an n-times-expanded metatile. For each large metatile
expansion, you can give the index of the smaller metatile selected
from its expansion; when we reach the last layer of metatiles and
expand them into hats, we can give the index of the hat in that
metatile; finally we can index the kite in that hat.</p>
<p><strong>But note that a kite can have multiple
coordinates</strong>, because of the overlap between the expansions
of adjacent metatiles. This will be useful!</p>
<p>Our next step is to unambiguously name the four directions in
which you can move from one kite to an adjacent kite. The directions
should be independent of the orientation of the kite. I've chosen to
name them from the viewpoint of someone standing at the pointy end
of the kite and looking towards the blunt end:</p>
<dl>
<dt><strong>Left</strong></dt>
<dd>Rotate 60° anticlockwise about the pointy end of the kite. For
example, in the above hat, going 'left' from kite 5 takes you to
kite 4.</dd>
<dt><strong>Right</strong></dt>
<dd>Rotate 60° clockwise about the pointy end. From kite 5, this
would take you to kite 6.</dd>
<dt><strong>Forward left</strong></dt>
<dd>Head forwards and slightly left, to the kite sharing the
left-hand one of this kite's short edges (as seen from the
centre). Equivalently, rotate 120° <em>clockwise</em> about the
blunt end. From kite 5, this takes you to kite 2.</dd>
<dt><strong>Forward right</strong></dt>
<dd>Head forwards and slightly right. Or rotate 120° anticlockwise
about the blunt end, if you prefer to think of it that way. From
kite 5, this takes you to kite 1.</dd>
</dl>
<p>The idea is that if we know how to transform the coordinates of a
single kite into the coordinates of each of those four adjacent
kites, then we can iterate that over a whole area and determine the
coordinates of every kite in the whole tiling.</p>
<p>Having done that, it's easy to identify each individual kite, by
several different methods. For example, you could iterate over edges
of the tiling to see whether the kites on each side have coordinates
differing only in the kite index; if so, they're part of the same
hat, and if not, not. Or a completely different approach (in fact
the one Loopy actually uses) would be to trace round the boundary of
each hat by starting from its kite #0 and just knowing what shape a
hat is.</p>
<p>So now we have to come up with an algorithm that lets us
transform a kite coordinate by making one of the four permitted
moves. To do this, we use two multilevel types of map.</p>
<p>The <strong>kitemap</strong> for a given metatile type is made by
expanding the metatile once into more metatiles, and then into hats.
For example, the T tile:</p>
<table class="noborder">
<tr>
<td><img src="single-T.svg"></td>
<td><img src="arrow.svg"></td>
<td><img src="expanded-T.svg" height="200px"></td>
<td><img src="arrow.svg"></td>
<td><img src="kitemap-T.svg" height="500px"></td>
</tr>
</table>
<p>In each kite, we show a three-part coordinate, in little-endian
fashion (because that matches the order the coordinates are stored
in an array in the code that actually generates the tilings). For
example, 7.3.0 means kite 7 in hat 3 of metatile 0 of the
expansion.</p>
<p>This map can be converted into a lookup table, indexed by those
three-part coordinates and also the four move directions, which
allows you to look up that (for example) going Left from kite 7.3.0
goes to 0.0.0, or going Forward Left from 7.3.0 goes to 3.1.3.</p>
<p>But if you're at the very edge of the kitemap, this isn't enough.
For example, kite 0.0.4 right at the top can go Left to 1.0.4, but
if it wants to go in any of the other three directions, this map
doesn't help at all.</p>
<p>This is where the overlap between the metatile expansions comes
in. If you're in kite 0.0.4, then in particular, you're in the F
tile numbered 4 in the expansion of a larger T metatile. And that F
tile is <em>also</em> part of the expansion of at least one other
second-order metatile maybe two of them which means that there
are other equivalent coordinates describing the same kite, which
will place it in a different kitemap where it <em>isn't</em> right
on the edge,</p>
<p>In order to find those equivalent coordinates, we create a second
map for each metatile type, called the <strong>metamap</strong>.
This one arises from expanding the metatile twice into other
metatiles, instead of into hats:</p>
<table class="noborder">
<tr>
<td><img src="single-T.svg"></td>
<td><img src="arrow.svg"></td>
<td><img src="expanded-T.svg" height="200px"></td>
<td><img src="arrow.svg"></td>
<td><img src="metamap-T.svg" height="500px"></td>
</tr>
</table>
<p>Again, the coordinates are little-end first, so that 7.4 means
the 7th smallest-size tile expanded from the 4th medium-sized tile
expanded from the original single large tile.</p>
<p>Unlike the kitemap, the metamap is not used for <em>moving
around</em> the tiling to a different kite. It's used for rewriting
the coordinates of the current kite into equivalent forms. So each
the small tile in the metamap that's part of the expansion
of <em>more than one</em> medium-sized tile has more than one
coordinate pair. For example, tile 5.2 is also tile 5.4, and tile
7.0 is also 8.3 <em>and</em> also 4.5 (because it's where three
medium-tile expansions meet).</p>
<p>Using both of these maps (converted into appropriate lookup
tables in the code), you can always eventually find a valid
coordinate representation of whichever kite you like adjacent to
your current one. If the kitemap corresponding to the current
coordinates doesn't tell you the coordinates of the next kite, then
you can try rewriting the two least-significant metatile indices
(using the metamap corresponding to the type of the next-larger
metatile still) and then see if that gives you a new kitemap that
works. If even that doesn't work, you can move another level up, and
try a metamap rewrite on the 2nd and 3rd smallest metatile levels,
or the 3rd and 4th, etc. And eventually, you find something you can
do.</p>
<p>The full set of kitemaps and metamaps for all the tile
types is in <a href="hatmaps.html">hatmaps.html</a>.</p>
</body>
</html>