diff options
| author | Simon Tatham <anakin@pobox.com> | 2023-04-02 10:20:37 +0100 |
|---|---|---|
| committer | Simon Tatham <anakin@pobox.com> | 2023-04-02 14:35:12 +0100 |
| commit | 71e1776094aa9240e9772b7fbc99dd5e2f4e1acb (patch) | |
| tree | 8d8b1772914ba2aa5d523753cf779362e7fca64e /auxiliary | |
| parent | 0bd1a8057841386754f9f4a8a268616c7ce80e80 (diff) | |
| download | puzzles-71e1776094aa9240e9772b7fbc99dd5e2f4e1acb.zip puzzles-71e1776094aa9240e9772b7fbc99dd5e2f4e1acb.tar.gz puzzles-71e1776094aa9240e9772b7fbc99dd5e2f4e1acb.tar.bz2 puzzles-71e1776094aa9240e9772b7fbc99dd5e2f4e1acb.tar.xz | |
Move hat-test into its own source file.
I noticed while hacking on hat-test recently that it's quite awkward
to be compiling a test main() program that lives in a source file also
built into the Puzzles support library, because every modification to
main() also triggers a rebuild of the library, and thence of all the
actual puzzles. So it's better if such a test main() has its own
source file.
In order to make hat-test work standalone, I've had to move a lot of
hat.c's internal declarations out into a second header file. This also
means making a bunch of internal functions global, which means they're
also in the namespace of programs other than hat-test, which means in
turn that they should have names with less implicit context.
Diffstat (limited to 'auxiliary')
| -rw-r--r-- | auxiliary/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | auxiliary/hat-test.c | 622 |
2 files changed, 623 insertions, 0 deletions
diff --git a/auxiliary/CMakeLists.txt b/auxiliary/CMakeLists.txt index 82e8c82..d78b60a 100644 --- a/auxiliary/CMakeLists.txt +++ b/auxiliary/CMakeLists.txt @@ -1 +1,2 @@ cliprogram(hatgen hatgen.c COMPILE_DEFINITIONS TEST_HAT) +cliprogram(hat-test hat-test.c) diff --git a/auxiliary/hat-test.c b/auxiliary/hat-test.c new file mode 100644 index 0000000..8104161 --- /dev/null +++ b/auxiliary/hat-test.c @@ -0,0 +1,622 @@ +/* + * Standalone test program for hat.c, which generates patches of hat + * tiling in multiple output formats without also generating a Loopy + * puzzle around them. + */ + +#include <assert.h> +#include <math.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +#include "hat-internal.h" + +static HatCoords *hat_coords_construct_v(TileType type, va_list ap) +{ + HatCoords *hc = hat_coords_new(); + while (true) { + int index = va_arg(ap, int); + + hat_coords_make_space(hc, hc->nc + 1); + hc->c[hc->nc].type = type; + hc->c[hc->nc].index = index; + hc->nc++; + + if (index < 0) + return hc; + + type = va_arg(ap, TileType); + } +} + +static HatCoords *hat_coords_construct(TileType type, ...) +{ + HatCoords *hc; + va_list ap; + + va_start(ap, type); + hc = hat_coords_construct_v(type, ap); + va_end(ap); + + return hc; +} + +static bool hat_coords_equal(HatCoords *hc1, HatCoords *hc2) +{ + size_t i; + + if (hc1->nc != hc2->nc) + return false; + + for (i = 0; i < hc1->nc; i++) { + if (hc1->c[i].type != hc2->c[i].type || + hc1->c[i].index != hc2->c[i].index) + return false; + } + + return true; +} + +static bool hat_coords_expect(const char *file, int line, HatCoords *hc, + TileType type, ...) +{ + bool equal; + va_list ap; + HatCoords *hce; + + va_start(ap, type); + hce = hat_coords_construct_v(type, ap); + va_end(ap); + + equal = hat_coords_equal(hc, hce); + + if (!equal) { + fprintf(stderr, "%s:%d: coordinate mismatch\n", file, line); + hat_coords_debug(" expected: ", hce, "\n"); + hat_coords_debug(" actual: ", hc, "\n"); + } + + hat_coords_free(hce); + return equal; +} + +#define EXPECT(hc, ...) do { \ + if (!hat_coords_expect(__FILE__, __LINE__, hc, __VA_ARGS__)) \ + fails++; \ + } while (0) + +/* + * For four-colouring the tiling: these tables give a colouring of + * each kitemap, with colour 3 assigned to the reflected tiles in the + * middle of the H, and 0,1,2 chosen arbitrarily. + */ + +static const int fourcolours_H[] = { + /* 0 */ 0, 2, 1, 3, + /* 1 */ 1, 0, 2, 3, + /* 2 */ 0, 2, 1, 3, + /* 3 */ 1, -1, -1, -1, + /* 4 */ 1, 2, -1, -1, + /* 5 */ 1, 2, -1, -1, + /* 6 */ 2, 1, -1, -1, + /* 7 */ 0, 1, -1, -1, + /* 8 */ 2, 0, -1, -1, + /* 9 */ 2, 0, -1, -1, + /* 10 */ 0, 1, -1, -1, + /* 11 */ 0, 1, -1, -1, + /* 12 */ 2, 0, -1, -1, +}; +static const int fourcolours_T[] = { + /* 0 */ 1, 2, 0, 3, + /* 1 */ 2, 1, -1, -1, + /* 2 */ 0, 1, -1, -1, + /* 3 */ 0, 2, -1, -1, + /* 4 */ 2, 0, -1, -1, + /* 5 */ 0, 1, -1, -1, + /* 6 */ 1, 2, -1, -1, +}; +static const int fourcolours_P[] = { + /* 0 */ 2, 1, 0, 3, + /* 1 */ 1, 2, 0, 3, + /* 2 */ 2, 1, -1, -1, + /* 3 */ 0, 2, -1, -1, + /* 4 */ 0, 1, -1, -1, + /* 5 */ 1, 2, -1, -1, + /* 6 */ 2, 0, -1, -1, + /* 7 */ 0, 1, -1, -1, + /* 8 */ 1, 0, -1, -1, + /* 9 */ 2, 1, -1, -1, + /* 10 */ 0, 2, -1, -1, +}; +static const int fourcolours_F[] = { + /* 0 */ 2, 0, 1, 3, + /* 1 */ 0, 2, 1, 3, + /* 2 */ 1, 2, -1, -1, + /* 3 */ 1, 0, -1, -1, + /* 4 */ 0, 2, -1, -1, + /* 5 */ 2, 1, -1, -1, + /* 6 */ 2, 0, -1, -1, + /* 7 */ 0, 1, -1, -1, + /* 8 */ 0, 1, -1, -1, + /* 9 */ 2, 0, -1, -1, + /* 10 */ 1, 2, -1, -1, +}; +static const int *const fourcolours[] = { + fourcolours_H, fourcolours_T, fourcolours_P, fourcolours_F, +}; + +static bool unit_tests(void) +{ + int fails = 0; + HatContext ctx[1]; + HatCoords *hc_in, *hc_out; + + ctx->rs = NULL; + ctx->prototype = hat_coords_construct(TT_KITE, 0, TT_HAT, 0, TT_H, -1); + + /* Simple steps within a hat */ + + hc_in = hat_coords_construct(TT_KITE, 6, TT_HAT, 2, TT_H, 1, TT_H, -1); + hc_out = hatctx_step(ctx, hc_in, KS_LEFT); + EXPECT(hc_out, TT_KITE, 5, TT_HAT, 2, TT_H, 1, TT_H, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + hc_in = hat_coords_construct(TT_KITE, 6, TT_HAT, 2, TT_H, 1, TT_H, -1); + hc_out = hatctx_step(ctx, hc_in, KS_RIGHT); + EXPECT(hc_out, TT_KITE, 7, TT_HAT, 2, TT_H, 1, TT_H, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + hc_in = hat_coords_construct(TT_KITE, 5, TT_HAT, 2, TT_H, 1, TT_H, -1); + hc_out = hatctx_step(ctx, hc_in, KS_F_LEFT); + EXPECT(hc_out, TT_KITE, 2, TT_HAT, 2, TT_H, 1, TT_H, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + hc_in = hat_coords_construct(TT_KITE, 5, TT_HAT, 2, TT_H, 1, TT_H, -1); + hc_out = hatctx_step(ctx, hc_in, KS_F_RIGHT); + EXPECT(hc_out, TT_KITE, 1, TT_HAT, 2, TT_H, 1, TT_H, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + /* Step between hats in the same kitemap, which can change the + * metatile type at layer 2 */ + + hc_in = hat_coords_construct(TT_KITE, 6, TT_HAT, 2, TT_H, 1, TT_H, -1); + hc_out = hatctx_step(ctx, hc_in, KS_F_LEFT); + EXPECT(hc_out, TT_KITE, 3, TT_HAT, 0, TT_H, 0, TT_H, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + hc_in = hat_coords_construct(TT_KITE, 7, TT_HAT, 2, TT_H, 1, TT_H, -1); + hc_out = hatctx_step(ctx, hc_in, KS_F_RIGHT); + EXPECT(hc_out, TT_KITE, 4, TT_HAT, 0, TT_T, 3, TT_H, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + /* Step off the edge of one kitemap, necessitating a metamap + * rewrite of layers 2,3 to get into a different kitemap where + * that step can be made */ + + hc_in = hat_coords_construct(TT_KITE, 6, TT_HAT, 0, TT_P, 2, TT_P, 3, + TT_P, -1); + hc_out = hatctx_step(ctx, hc_in, KS_F_RIGHT); + /* Working: + * kite 6 . hat 0 . P 2 . P 3 . P ? + * -> kite 6 . hat 0 . P 6 . H 0 . P ? (P metamap says 2.3 = 6.0) + */ + EXPECT(hc_out, TT_KITE, 7, TT_HAT, 1, TT_H, 1, TT_H, 0, TT_P, -1); + hat_coords_free(hc_in); + hat_coords_free(hc_out); + + hatctx_cleanup(ctx); + return fails == 0; +} + +/* + * Structure that describes how the colours in the above maps are + * translated to output colours. This will vary with each kitemap our + * coordinates pass through, in order to maintain consistency. + */ +typedef struct FourColourMap { + unsigned char map[4]; +} FourColourMap; + +/* + * Make an initial FourColourMap by choosing the initial permutation + * of the three 'normal' hat colours randomly. + */ +static inline FourColourMap fourcolourmap_initial(random_state *rs) +{ + FourColourMap f; + unsigned i; + + /* Start with the identity mapping */ + for (i = 0; i < 4; i++) + f.map[i] = i; + + /* Randomly permute colours 0,1,2, leaving 3 as the distinguished + * colour for reflected hats */ + shuffle(f.map, 3, sizeof(f.map[0]), rs); + + return f; +} + +static inline FourColourMap fourcolourmap_update( + FourColourMap prevm, HatCoords *prevc, HatCoords *currc, KiteStep step, + HatContext *ctx) +{ + size_t i, m1, m2; + const int *f1, *f2; + unsigned sum; + int missing; + FourColourMap newm; + HatCoords *prev2c; + + /* + * If prevc and currc are in the same kitemap anyway, that's the + * easy case: the colour map for the new kitemap is the same as + * for the old one, because they're the same kitemap. + */ + hatctx_extend_coords(ctx, prevc, currc->nc); + hatctx_extend_coords(ctx, currc, prevc->nc); + for (i = 3; i < prevc->nc; i++) + if (currc->c[i].index != prevc->c[i].index) + goto mismatch; + return prevm; + mismatch: + + /* + * The hatctx_step algorithm guarantees that the _new_ coordinate + * currc is expected to be in a kitemap containing both this kite + * and the previous one (because it first transformed the previous + * coordinate until it _could_ take a step within the same + * kitemap, and then did). + * + * So if we reverse the last step we took, we should get a second + * HatCoords describing the same kite as prevc but showing its + * position in the _new_ kitemap. This lets us figure out a pair + * of corresponding metatile indices within the old and new + * kitemaps (by looking at which metatile prevc and prev2c claim + * to be in). + * + * That metatile will also always be a P or an F (because all + * metatiles overlapping the next kitemap are of those types), + * which means it will have two hats in it. And those hats will be + * adjacent, so differently coloured. Hence, we have enough + * information to decide how two of the new kitemap's three normal + * colours map to the colours we were using in the old kitemap - + * and then the third is determined by process of elimination. + */ + prev2c = hatctx_step( + ctx, currc, (step == KS_LEFT ? KS_RIGHT : + step == KS_RIGHT ? KS_LEFT : + step == KS_F_LEFT ? KS_F_RIGHT : KS_F_LEFT)); + + /* Metatile indices within the old and new kitemaps */ + m1 = prevc->c[2].index; + m2 = prev2c->c[2].index; + + /* The colourings of those metatiles' hats in our fixed fourcolours[] */ + f1 = fourcolours[prevc->c[3].type] + 4*m1; + f2 = fourcolours[prev2c->c[3].type] + 4*m2; + + /* + * Start making our new output map, filling in all three normal + * colours to 255 = "don't know yet". + */ + newm.map[3] = 3; + newm.map[0] = newm.map[1] = newm.map[2] = 255; + + /* + * Iterate over the tile colourings in fourcolours[] for these + * metatiles, matching up our mappings. + */ + for (i = 0; i < 4; i++) { + /* They should be the same metatile, so have same number of hats! */ + assert((f1[i] == -1) == (f2[i] == -1)); + + if (f1[i] != 255) + newm.map[f2[i]] = prevm.map[f1[i]]; + } + + /* + * We expect to have filled in exactly two of the three normal + * colours. Find the missing index, and fill in its colour by + * arithmetic (using the fact that the three colours add up to 3). + */ + sum = 0; + missing = -1; + for (i = 0; i < 3; i++) { + if (newm.map[i] == 255) { + assert(missing == -1); /* shouldn't have two missing colours */ + missing = i; + } else { + sum += newm.map[i]; + } + } + assert(missing != -1); + assert(0 < sum && sum <= 3); + newm.map[missing] = 3 - sum; + + return newm; +} + +typedef struct pspoint { + float x, y; +} pspoint; + +typedef struct psbbox { + bool started; + pspoint bl, tr; +} psbbox; + +static inline void psbbox_add(psbbox *bbox, pspoint p) +{ + if (!bbox->started || bbox->bl.x > p.x) + bbox->bl.x = p.x; + if (!bbox->started || bbox->tr.x < p.x) + bbox->tr.x = p.x; + if (!bbox->started || bbox->bl.y > p.y) + bbox->bl.y = p.y; + if (!bbox->started || bbox->tr.y < p.y) + bbox->tr.y = p.y; + bbox->started = true; +} + +typedef enum OutFmt { OF_POSTSCRIPT, OF_PYTHON } OutFmt; +typedef enum ColourMode { CM_SEMANTIC, CM_FOURCOLOUR } ColourMode; + +typedef struct drawctx { + OutFmt outfmt; + ColourMode colourmode; + psbbox *bbox; + KiteEnum *kiteenum; + FourColourMap fourcolourmap[KE_NKEEP]; +} drawctx; + +static void bbox_add_hat(void *vctx, Kite kite0, HatCoords *hc, int *coords) +{ + drawctx *ctx = (drawctx *)vctx; + pspoint p; + size_t i; + + for (i = 0; i < 14; i++) { + p.x = coords[2*i] * 1.5; + p.y = coords[2*i+1] * sqrt(0.75); + psbbox_add(ctx->bbox, p); + } +} + +static void header(drawctx *ctx) +{ + switch (ctx->outfmt) { + case OF_POSTSCRIPT: { + float xext = ctx->bbox->tr.x - ctx->bbox->bl.x; + float yext = ctx->bbox->tr.y - ctx->bbox->bl.y; + float ext = (xext > yext ? xext : yext); + float scale = 500 / ext; + float ox = 287 - scale * (ctx->bbox->bl.x + ctx->bbox->tr.x) / 2; + float oy = 421 - scale * (ctx->bbox->bl.y + ctx->bbox->tr.y) / 2; + + printf("%%!PS-Adobe-2.0\n%%%%Creator: hat-test from Simon Tatham's " + "Portable Puzzle Collection\n%%%%Pages: 1\n" + "%%%%BoundingBox: %f %f %f %f\n" + "%%%%EndComments\n%%%%Page: 1 1\n", + ox + scale * ctx->bbox->bl.x - 20, + oy + scale * ctx->bbox->bl.y - 20, + ox + scale * ctx->bbox->tr.x + 20, + oy + scale * ctx->bbox->tr.y + 20); + + printf("%f %f translate %f dup scale\n", ox, oy, scale); + printf("%f setlinewidth\n", scale * 0.03); + printf("0 setgray 1 setlinejoin 1 setlinecap\n"); + break; + } + default: + break; + } +} + +static void draw_hat(void *vctx, Kite kite0, HatCoords *hc, int *coords) +{ + drawctx *ctx = (drawctx *)vctx; + pspoint p; + size_t i; + int orientation; + + /* + * Determine an index for the hat's orientation, based on the axis + * of symmetry of its kite #0. + */ + { + int dx = kite0.outer.x - kite0.centre.x; + int dy = kite0.outer.y - kite0.centre.y; + orientation = 0; + while (dx < 0 || dy < 0) { + int newdx = dx + dy; + int newdy = -dx; + dx = newdx; + dy = newdy; + orientation++; + assert(orientation < 6); + } + } + + switch (ctx->outfmt) { + case OF_POSTSCRIPT: { + const char *colour; + + printf("newpath"); + for (i = 0; i < 14; i++) { + p.x = coords[2*i] * 1.5; + p.y = coords[2*i+1] * sqrt(0.75); + printf(" %f %f %s", p.x, p.y, i ? "lineto" : "moveto"); + } + printf(" closepath gsave"); + + switch (ctx->colourmode) { + case CM_SEMANTIC: + if (hc->c[2].type == TT_H) { + colour = (hc->c[1].index == 3 ? "0 0.5 0.8 setrgbcolor" : + "0.6 0.8 1 setrgbcolor"); + } else if (hc->c[2].type == TT_F) { + colour = "0.7 setgray"; + } else { + colour = "1 setgray"; + } + break; + + default /* case CM_FOURCOLOUR */: { + /* + * Determine the colour of this tile by translating the + * fixed colour from fourcolours[] through our current + * FourColourMap. + */ + FourColourMap f = ctx->fourcolourmap[ctx->kiteenum->curr_index]; + const int *m = fourcolours[hc->c[3].type]; + static const char *const colours[] = { + "1 0.7 0.7 setrgbcolor", + "1 1 0.7 setrgbcolor", + "0.7 1 0.7 setrgbcolor", + "0.6 0.6 1 setrgbcolor", + }; + colour = colours[f.map[m[hc->c[2].index * 4 + hc->c[1].index]]]; + break; + } + } + printf(" %s fill grestore", colour); + printf(" stroke\n"); + break; + } + case OF_PYTHON: { + printf("hat('%c', %d, %d, [", "HTPF"[hc->c[2].type], hc->c[1].index, + orientation); + for (i = 0; i < 14; i++) + printf("%s(%d,%d)", i ? ", " : "", coords[2*i], coords[2*i+1]); + printf("])\n"); + break; + } + } +} + +static void trailer(drawctx *dctx) +{ + switch (dctx->outfmt) { + case OF_POSTSCRIPT: { + printf("showpage\n"); + printf("%%%%Trailer\n"); + printf("%%%%EOF\n"); + break; + } + default: + break; + } +} + +int main(int argc, char **argv) +{ + psbbox bbox[1]; + KiteEnum s[1]; + HatContext ctx[1]; + HatCoords *coords[KE_NKEEP]; + random_state *rs; + const char *random_seed = "12345"; + int w = 10, h = 10; + int argpos = 0; + size_t i; + drawctx dctx[1]; + + dctx->outfmt = OF_POSTSCRIPT; + dctx->colourmode = CM_SEMANTIC; + dctx->kiteenum = s; + + while (--argc > 0) { + const char *arg = *++argv; + if (!strcmp(arg, "--help")) { + printf(" usage: hat-test [options] [<width>] [<height>]\n" + "options: --python write a Python function call per hat\n" + " --seed=STR vary the starting random seed\n" + " also: hat-test --test\n"); + return 0; + } else if (!strcmp(arg, "--test")) { + return unit_tests() ? 0 : 1; + } else if (!strcmp(arg, "--python")) { + dctx->outfmt = OF_PYTHON; + } else if (!strcmp(arg, "--fourcolour")) { + dctx->colourmode = CM_FOURCOLOUR; + } else if (!strncmp(arg, "--seed=", 7)) { + random_seed = arg+7; + } else if (arg[0] == '-') { + fprintf(stderr, "unrecognised option '%s'\n", arg); + return 1; + } else { + switch (argpos++) { + case 0: + w = atoi(arg); + break; + case 1: + h = atoi(arg); + break; + default: + fprintf(stderr, "unexpected extra argument '%s'\n", arg); + return 1; + } + } + } + + for (i = 0; i < lenof(coords); i++) + coords[i] = NULL; + + rs = random_new(random_seed, strlen(random_seed)); + hatctx_init_random(ctx, rs); + + bbox->started = false; + dctx->bbox = bbox; + + hat_kiteenum_first(s, w, h); + coords[s->curr_index] = hatctx_initial_coords(ctx); + maybe_report_hat(w, h, *s->curr, coords[s->curr_index], + bbox_add_hat, dctx); + while (hat_kiteenum_next(s)) { + hat_coords_free(coords[s->curr_index]); + coords[s->curr_index] = hatctx_step( + ctx, coords[s->last_index], s->last_step); + maybe_report_hat(w, h, *s->curr, coords[s->curr_index], + bbox_add_hat, dctx); + } + for (i = 0; i < lenof(coords); i++) { + hat_coords_free(coords[i]); + coords[i] = NULL; + } + + header(dctx); + + hat_kiteenum_first(s, w, h); + coords[s->curr_index] = hatctx_initial_coords(ctx); + dctx->fourcolourmap[s->curr_index] = fourcolourmap_initial(rs); + maybe_report_hat(w, h, *s->curr, coords[s->curr_index], + draw_hat, dctx); + while (hat_kiteenum_next(s)) { + hat_coords_free(coords[s->curr_index]); + coords[s->curr_index] = hatctx_step( + ctx, coords[s->last_index], s->last_step); + dctx->fourcolourmap[s->curr_index] = fourcolourmap_update( + dctx->fourcolourmap[s->last_index], coords[s->last_index], + coords[s->curr_index], s->last_step, ctx); + maybe_report_hat(w, h, *s->curr, coords[s->curr_index], + draw_hat, dctx); + } + for (i = 0; i < lenof(coords); i++) { + hat_coords_free(coords[i]); + coords[i] = NULL; + } + + trailer(dctx); + + hatctx_cleanup(ctx); + + return 0; +} |