Files
puzzles/penrose.h
Simon Tatham 61e9c78248 grid.c: new and improved Penrose tiling generator.
The new generator works on the same 'combinatorial coordinates' system
as the more recently written Hats and Spectre generators.

When I came up with that technique for the Hats tiling, I was already
tempted to rewrite the Penrose generator on the same principle,
because it has a lot of advantages over the previous technique of
picking a randomly selected patch out of the subdivision of a huge
starting tile. It generates the exact limiting distribution of
possible tiling patches rather than an approximation based on how big
a tile you subdivided; it doesn't use any overly large integers or
overly precise floating point to suffer overflow or significance loss,
because it never has to even _think_ about the coordinates of any
point not in the actual output area.

But I resisted the temptation to throw out our existing Penrose
generator and move to the shiny new algorithm, for just one reason:
backwards compatibility. There's no sensible way to translate existing
Loopy game IDs into the new notation, so they would stop working,
unless we kept the old generator around as well to interpret the
previous system. And although _random seeds_ aren't guaranteed to
generate the same result from one build of Puzzles to the next, I do
try to keep existing descriptive game IDs working.

So if we had to keep the old generator around anyway, it didn't seem
worth writing a new one...

... at least, until we discovered that the old generator has a latent
bug. The function that decides whether each tile is within the target
area, and hence whether to make it part of the output grid, is based
on floating-point calculation of the tile's vertices. So a change in
FP rounding behaviour between two platforms could cause them to
interpret the same grid description differently, invalidating a Loopy
game on that grid. This is _unlikely_, as long as everyone uses IEEE
754 double, but the C standard doesn't actually guarantee that.

We found this out when I started investigating a slight distortion in
large instances of Penrose Loopy. For example, the Loopy random seed
"40x40t11#12345", as of just before this commit, generates a game
description beginning with the Penrose grid string "G-4944,5110,108",
in which you can see a star shape of five darts a few tiles down the
left edge, where two of the radii of the star don't properly line up
with edges in the surrounding shell of kites that they should be
collinear with. This turns out to be due to FP error: not because
_double precision_ ran out, but because the definitions of COS54,
SIN54, COS18 and SIN18 in penrose.c were specified to only 7 digits of
precision. And when you expand a patch of tiling that large, you end
up with integer combinations of those numbers with coefficients about
7 digits long, mostly cancelling out to leave a much smaller output
value, and the inaccuracies in those constants start to be noticeable.

But having noticed that, it then became clear that these badly
computed values were also critical to _correctness_ of the grid. So
they can't be fixed without invalidating old game IDs. _And_ then we
realised that this also means existing platforms might disagree on a
game ID's validity.

So if we have to break compatibility anyway, we should go all the way,
and instead of fixing the old system, introduce the shiny new system
that gets all of this right. Therefore, the new penrose.c uses the
more reliable combinatorial-coordinates system which doesn't deal in
integers that large in the first place. Also, there's no longer any
floating point at all in the calculation of tile vertex coordinates:
the combinations of 1 and sqrt(5) are computed exactly by the
n_times_root_k function. So now a large Penrose grid should never have
obvious failures of alignment like that.

The old system is kept for backwards compatibility. It's not fully
reliable, because that bug hasn't been fixed - but any Penrose Loopy
game ID that was working before on _some_ platform should still work
on that one. However, new Penrose Loopy game IDs are based on
combinatorial coordinates, computed in exact arithmetic, and should be
properly stable.

The new code looks suspiciously like the Spectre code (though
considerably simpler - the Penrose coordinate maps are easy enough
that I just hand-typed them rather than having an elaborate auxiliary
data-generation tool). That's because they were similar enough in
concept to make it possible to clone and hack. But there are enough
divergences that it didn't seem like a good idea to try to merge them
again afterwards - in particular, the fact that output Penrose tiles
are generated by merging two triangular metatiles while Spectres are
subdivisions of their metatiles.
2023-07-07 18:17:02 +01:00

87 lines
3.1 KiB
C

#ifndef PUZZLES_PENROSE_H
#define PUZZLES_PENROSE_H
struct PenrosePatchParams {
/*
* A patch of Penrose tiling is identified by giving
*
* - the coordinates of the starting triangle, using a
* combinatorial coordinate system
*
* - which vertex of that triangle is at the centre point of the
* tiling
*
* - the orientation of the triangle's base edge, as a number
* from 0 to 9, measured in tenths of a turn
*
* Coordinates are a sequence of letters. For a P2 tiling all
* letters are from the set {A,B,U,V}; for P3, {C,D,X,Y}.
*/
unsigned start_vertex;
int orientation;
size_t ncoords;
char *coords;
};
#ifndef PENROSE_ENUM_DEFINED
#define PENROSE_ENUM_DEFINED
enum { PENROSE_P2, PENROSE_P3 };
#endif
bool penrose_valid_letter(char c, int which);
/*
* Fill in PenrosePatchParams with a randomly selected set of
* coordinates, in enough detail to generate a patch of tiling filling
* a w x h area.
*
* Units of measurement: the tiling will be oriented such that
* horizontal tile edges are possible (and therefore vertical ones are
* not). Therefore, all x-coordinates will be rational linear
* combinations of 1 and sqrt(5), and all y-coordinates will be
* sin(pi/5) times such a rational linear combination. By scaling up
* appropriately we can turn those rational combinations into
* _integer_ combinations, so we do. Therefore, w is measured in units
* of 1/4, and h is measured in units of sin(pi/5)/2, on a scale where
* a length of 1 corresponds to the legs of the acute isosceles
* triangles in the tiling (hence, the long edges of P2 kites and
* darts, or all edges of P3 rhombs).
*
* (In case it's useful, the y scale factor sin(pi/5)/2 is an
* algebraic number. Its minimal polynomial is 256x^4 - 80x^2 + 5.)
*
* The 'coords' field of the structure will be filled in with a new
* dynamically allocated array. Any previous pointer in that field
* will be overwritten.
*/
void penrose_tiling_randomise(struct PenrosePatchParams *params, int which,
int w, int h, random_state *rs);
/*
* Validate a PenrosePatchParams to ensure it contains no illegal
* coordinates. Returns NULL if it's acceptable, or an error string if
* not.
*/
const char *penrose_tiling_params_invalid(
const struct PenrosePatchParams *params, int which);
/*
* Generate the actual set of Penrose tiles from a PenrosePatchParams,
* passing each one to a callback. The callback receives the vertices
* of each point, in the form of an array of 4*4 integers. Each vertex
* is represented by four consecutive integers in this array, with the
* first two giving the x coordinate and the last two the y
* coordinate. Each pair of integers a,b represent a single coordinate
* whose value is a + b*sqrt(5). The units of measurement for x and y
* are as described above.
*/
typedef void (*penrose_tile_callback_fn)(void *ctx, const int *coords);
#define PENROSE_NVERTICES 4
void penrose_tiling_generate(
const struct PenrosePatchParams *params, int w, int h,
penrose_tile_callback_fn cb, void *cbctx);
#endif