mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-19 23:21:31 -07:00
Files

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.
290 lines
8.2 KiB
C
290 lines
8.2 KiB
C
#include "penrose.h"
|
|
|
|
static inline unsigned num_subtriangles(char t)
|
|
{
|
|
return (t == 'A' || t == 'B' || t == 'X' || t == 'Y') ? 3 : 2;
|
|
}
|
|
|
|
static inline unsigned sibling_edge(char t)
|
|
{
|
|
switch (t) {
|
|
case 'A': case 'U': return 2;
|
|
case 'B': case 'V': return 1;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Coordinate system for tracking Penrose-tile half-triangles.
|
|
* PenroseCoords simply stores an array of triangle types.
|
|
*/
|
|
typedef struct PenroseCoords {
|
|
char *c;
|
|
size_t nc, csize;
|
|
} PenroseCoords;
|
|
|
|
PenroseCoords *penrose_coords_new(void);
|
|
void penrose_coords_free(PenroseCoords *pc);
|
|
void penrose_coords_make_space(PenroseCoords *pc, size_t size);
|
|
PenroseCoords *penrose_coords_copy(PenroseCoords *pc_in);
|
|
|
|
/*
|
|
* Coordinate system for locating Penrose tiles in the plane.
|
|
*
|
|
* The 'Point' structure represents a single point by means of an
|
|
* integer linear combination of {1, t, t^2, t^3}, where t is the
|
|
* complex number exp(i pi/5) representing 1/10 of a turn about the
|
|
* origin.
|
|
*
|
|
* The 'PenroseTriangle' structure represents a half-tile triangle,
|
|
* giving both the locations of its vertices and its combinatorial
|
|
* coordinates. It also contains a linked-list pointer and a boolean
|
|
* flag, used during breadth-first search to generate all the tiles in
|
|
* an area and report them exactly once.
|
|
*/
|
|
typedef struct Point {
|
|
int coeffs[4];
|
|
} Point;
|
|
typedef struct PenroseTriangle PenroseTriangle;
|
|
struct PenroseTriangle {
|
|
Point vertices[3];
|
|
PenroseCoords *pc;
|
|
PenroseTriangle *next; /* used in breadth-first search */
|
|
bool reported;
|
|
};
|
|
|
|
/* Fill in all the coordinates of a triangle starting from any single edge.
|
|
* Requires tri->pc to have been filled in, so that we know which shape of
|
|
* triangle we're placing. */
|
|
void penrose_place(PenroseTriangle *tri, Point u, Point v, int index_of_u);
|
|
|
|
/* Free a PenroseHalf and its contained coordinates, or a whole PenroseTile */
|
|
void penrose_free(PenroseTriangle *tri);
|
|
|
|
/*
|
|
* A Point is really a complex number, so we can add, subtract and
|
|
* multiply them.
|
|
*/
|
|
static inline Point point_add(Point a, Point b)
|
|
{
|
|
Point r;
|
|
size_t i;
|
|
for (i = 0; i < 4; i++)
|
|
r.coeffs[i] = a.coeffs[i] + b.coeffs[i];
|
|
return r;
|
|
}
|
|
static inline Point point_sub(Point a, Point b)
|
|
{
|
|
Point r;
|
|
size_t i;
|
|
for (i = 0; i < 4; i++)
|
|
r.coeffs[i] = a.coeffs[i] - b.coeffs[i];
|
|
return r;
|
|
}
|
|
static inline Point point_mul_by_t(Point x)
|
|
{
|
|
Point r;
|
|
/* Multiply by t by using the identity t^4 - t^3 + t^2 - t + 1 = 0,
|
|
* so t^4 = t^3 - t^2 + t - 1 */
|
|
r.coeffs[0] = -x.coeffs[3];
|
|
r.coeffs[1] = x.coeffs[0] + x.coeffs[3];
|
|
r.coeffs[2] = x.coeffs[1] - x.coeffs[3];
|
|
r.coeffs[3] = x.coeffs[2] + x.coeffs[3];
|
|
return r;
|
|
}
|
|
static inline Point point_mul(Point a, Point b)
|
|
{
|
|
size_t i, j;
|
|
Point r;
|
|
|
|
/* Initialise r to be a, scaled by b's t^3 term */
|
|
for (j = 0; j < 4; j++)
|
|
r.coeffs[j] = a.coeffs[j] * b.coeffs[3];
|
|
|
|
/* Now iterate r = t*r + (next coefficient down), by Horner's rule */
|
|
for (i = 3; i-- > 0 ;) {
|
|
r = point_mul_by_t(r);
|
|
for (j = 0; j < 4; j++)
|
|
r.coeffs[j] += a.coeffs[j] * b.coeffs[i];
|
|
}
|
|
|
|
return r;
|
|
}
|
|
static inline bool point_equal(Point a, Point b)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < 4; i++)
|
|
if (a.coeffs[i] != b.coeffs[i])
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Return the Point corresponding to a rotation of s steps around the
|
|
* origin, i.e. a rotation by 36*s degrees or s*pi/5 radians.
|
|
*/
|
|
static inline Point point_rot(int s)
|
|
{
|
|
Point r = {{ 1, 0, 0, 0 }};
|
|
Point tpower = {{ 0, 1, 0, 0 }};
|
|
|
|
/* Reduce to a sensible range */
|
|
s = s % 10;
|
|
if (s < 0)
|
|
s += 10;
|
|
|
|
while (true) {
|
|
if (s & 1)
|
|
r = point_mul(r, tpower);
|
|
s >>= 1;
|
|
if (!s)
|
|
break;
|
|
tpower = point_mul(tpower, tpower);
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
/*
|
|
* PenroseContext is the shared context of a whole run of the
|
|
* algorithm. Its 'prototype' PenroseCoords object represents the
|
|
* coordinates of the starting triangle, and is extended as necessary;
|
|
* any other PenroseCoord that needs extending will copy the
|
|
* higher-order values from ctx->prototype as needed, so that once
|
|
* each choice has been made, it remains consistent.
|
|
*
|
|
* When we're inventing a random piece of tiling in the first place,
|
|
* we append to ctx->prototype by choosing a random (but legal)
|
|
* higher-level metatile for the current topmost one to turn out to be
|
|
* part of. When we're replaying a generation whose parameters are
|
|
* already stored, we don't have a random_state, and we make fixed
|
|
* decisions if not enough coordinates were provided, as in the
|
|
* corresponding hat.c system.
|
|
*
|
|
* For a normal (non-testing) caller, penrosectx_generate() is the
|
|
* main useful function. It breadth-first searches a whole area to
|
|
* generate all the triangles in it, starting from a (typically
|
|
* central) one with the coordinates of ctx->prototype. It takes two
|
|
* callback function: one that checks whether a triangle is within the
|
|
* bounds of the target area (and therefore the search should continue
|
|
* exploring its neighbours), and another that reports a full Penrose
|
|
* tile once both of its halves have been found and determined to be
|
|
* in bounds.
|
|
*/
|
|
typedef struct PenroseContext {
|
|
random_state *rs;
|
|
bool must_free_rs;
|
|
unsigned start_vertex; /* which vertex of 'prototype' is at the origin? */
|
|
int orientation; /* orientation to put in PenrosePatchParams */
|
|
PenroseCoords *prototype;
|
|
} PenroseContext;
|
|
|
|
void penrosectx_init_random(PenroseContext *ctx, random_state *rs, int which);
|
|
void penrosectx_init_from_params(
|
|
PenroseContext *ctx, const struct PenrosePatchParams *ps);
|
|
void penrosectx_cleanup(PenroseContext *ctx);
|
|
PenroseCoords *penrosectx_initial_coords(PenroseContext *ctx);
|
|
void penrosectx_extend_coords(PenroseContext *ctx, PenroseCoords *pc,
|
|
size_t n);
|
|
void penrosectx_step(PenroseContext *ctx, PenroseCoords *pc,
|
|
unsigned edge, unsigned *outedge);
|
|
void penrosectx_generate(
|
|
PenroseContext *ctx,
|
|
bool (*inbounds)(void *inboundsctx,
|
|
const PenroseTriangle *tri), void *inboundsctx,
|
|
void (*tile)(void *tilectx, const Point *vertices), void *tilectx);
|
|
|
|
/* Subroutines that step around the tiling specified by a PenroseCtx,
|
|
* delivering both plane and combinatorial coordinates as they go */
|
|
PenroseTriangle *penrose_initial(PenroseContext *ctx);
|
|
PenroseTriangle *penrose_adjacent(PenroseContext *ctx,
|
|
const PenroseTriangle *src_spec,
|
|
unsigned src_edge, unsigned *dst_edge);
|
|
|
|
/* For extracting the point coordinates */
|
|
typedef struct Coord {
|
|
int c1, cr5; /* coefficients of 1 and sqrt(5) respectively */
|
|
} Coord;
|
|
|
|
static inline Coord point_x(Point p)
|
|
{
|
|
Coord x = {
|
|
4 * p.coeffs[0] + p.coeffs[1] - p.coeffs[2] + p.coeffs[3],
|
|
p.coeffs[1] + p.coeffs[2] - p.coeffs[3],
|
|
};
|
|
return x;
|
|
}
|
|
|
|
static inline Coord point_y(Point p)
|
|
{
|
|
Coord y = {
|
|
2 * p.coeffs[1] + p.coeffs[2] + p.coeffs[3],
|
|
p.coeffs[2] + p.coeffs[3],
|
|
};
|
|
return y;
|
|
}
|
|
|
|
static inline int coord_sign(Coord x)
|
|
{
|
|
if (x.c1 == 0 && x.cr5 == 0)
|
|
return 0;
|
|
if (x.c1 >= 0 && x.cr5 >= 0)
|
|
return +1;
|
|
if (x.c1 <= 0 && x.cr5 <= 0)
|
|
return -1;
|
|
|
|
if (x.c1 * x.c1 > 5 * x.cr5 * x.cr5)
|
|
return x.c1 < 0 ? -1 : +1;
|
|
else
|
|
return x.cr5 < 0 ? -1 : +1;
|
|
}
|
|
|
|
static inline Coord coord_construct(int c1, int cr5)
|
|
{
|
|
Coord c = { c1, cr5 };
|
|
return c;
|
|
}
|
|
|
|
static inline Coord coord_integer(int c1)
|
|
{
|
|
return coord_construct(c1, 0);
|
|
}
|
|
|
|
static inline Coord coord_add(Coord a, Coord b)
|
|
{
|
|
Coord sum;
|
|
sum.c1 = a.c1 + b.c1;
|
|
sum.cr5 = a.cr5 + b.cr5;
|
|
return sum;
|
|
}
|
|
|
|
static inline Coord coord_sub(Coord a, Coord b)
|
|
{
|
|
Coord diff;
|
|
diff.c1 = a.c1 - b.c1;
|
|
diff.cr5 = a.cr5 - b.cr5;
|
|
return diff;
|
|
}
|
|
|
|
static inline Coord coord_mul(Coord a, Coord b)
|
|
{
|
|
Coord prod;
|
|
prod.c1 = a.c1 * b.c1 + 5 * a.cr5 * b.cr5;
|
|
prod.cr5 = a.c1 * b.cr5 + a.cr5 * b.c1;
|
|
return prod;
|
|
}
|
|
|
|
static inline Coord coord_abs(Coord a)
|
|
{
|
|
int sign = coord_sign(a);
|
|
Coord abs;
|
|
abs.c1 = a.c1 * sign;
|
|
abs.cr5 = a.cr5 * sign;
|
|
return abs;
|
|
}
|
|
|
|
static inline int coord_cmp(Coord a, Coord b)
|
|
{
|
|
return coord_sign(coord_sub(a, b));
|
|
}
|