shithub: puzzles

Download patch

ref: 71e1776094aa9240e9772b7fbc99dd5e2f4e1acb
parent: 0bd1a8057841386754f9f4a8a268616c7ce80e80
author: Simon Tatham <anakin@pobox.com>
date: Sun Apr 2 06:20:37 EDT 2023

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.

--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -271,7 +271,6 @@
 cliprogram(matching matching.c COMPILE_DEFINITIONS STANDALONE_MATCHING_TEST)
 cliprogram(combi combi.c COMPILE_DEFINITIONS STANDALONE_COMBI_TEST)
 cliprogram(divvy divvy.c COMPILE_DEFINITIONS TESTMODE)
-cliprogram(hat-test hat.c COMPILE_DEFINITIONS TEST_HAT)
 cliprogram(penrose-test penrose.c COMPILE_DEFINITIONS TEST_PENROSE)
 cliprogram(penrose-vector-test penrose.c COMPILE_DEFINITIONS TEST_VECTORS)
 cliprogram(sort-test sort.c COMPILE_DEFINITIONS SORT_TEST)
--- a/auxiliary/CMakeLists.txt
+++ b/auxiliary/CMakeLists.txt
@@ -1,1 +1,2 @@
 cliprogram(hatgen hatgen.c COMPILE_DEFINITIONS TEST_HAT)
+cliprogram(hat-test hat-test.c)
--- /dev/null
+++ b/auxiliary/hat-test.c
@@ -1,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;
+}
--- /dev/null
+++ b/hat-internal.h
@@ -1,0 +1,271 @@
+/*
+ * Internal definitions for the hat.c tiling generator, shared between
+ * hat.c itself and hat-test.c.
+ */
+
+#include "puzzles.h"
+
+/*
+ * Coordinate system:
+ *
+ * The output of this code lives on the tiling known to grid.c as
+ * 'Kites', which can be viewed as a tiling of hexagons each of which
+ * is subdivided into six kites sharing their pointy vertex, or
+ * (equivalently) a tiling of equilateral triangles each subdivided
+ * into three kits sharing their blunt vertex.
+ *
+ * We express coordinates in this system relative to the basis (1, r)
+ * where r = (1 + sqrt(3)i) / 2 is a primitive 6th root of unity. This
+ * gives us a system in which two integer coordinates can address any
+ * grid point, provided we scale up so that the side length of the
+ * equilateral triangles in the tiling is 6.
+ */
+
+typedef struct Point {
+    int x, y;                          /* represents x + yr */
+} Point;
+
+static inline Point pointscale(int scale, Point a)
+{
+    Point r = { scale * a.x, scale * a.y };
+    return r;
+}
+
+static inline Point pointadd(Point a, Point b)
+{
+    Point r = { a.x + b.x, a.y + b.y };
+    return r;
+}
+
+/*
+ * We identify a single kite by the coordinates of its four vertices.
+ * This allows us to construct the coordinates of an adjacent kite by
+ * taking affine transformations of the original kite's vertices.
+ *
+ * This is a useful way to do it because it means that if you reflect
+ * the kite (by swapping its left and right vertices) then these
+ * transformations also perform in a reflected way. This will be
+ * useful in the code below that outputs the coordinates of each hat,
+ * because this way it can work by walking around its 8 kites using a
+ * fixed set of steps, and if the hat is reflected, then we just
+ * reflect the starting kite before doing that, and everything still
+ * works.
+ */
+
+typedef struct Kite {
+    Point centre, left, right, outer;
+} Kite;
+
+static inline Kite kite_left(Kite k)
+{
+    Kite r;
+    r.centre = k.centre;
+    r.right = k.left;
+    r.outer = pointadd(pointscale(2, k.left), pointscale(-1, k.outer));
+    r.left = pointadd(pointadd(k.centre, k.left), pointscale(-1, k.right));
+    return r;
+}
+
+static inline Kite kite_right(Kite k)
+{
+    Kite r;
+    r.centre = k.centre;
+    r.left = k.right;
+    r.outer = pointadd(pointscale(2, k.right), pointscale(-1, k.outer));
+    r.right = pointadd(pointadd(k.centre, k.right), pointscale(-1, k.left));
+    return r;
+}
+
+static inline Kite kite_forward_left(Kite k)
+{
+    Kite r;
+    r.outer = k.outer;
+    r.right = k.left;
+    r.centre = pointadd(pointscale(2, k.left), pointscale(-1, k.centre));
+    r.left = pointadd(pointadd(k.right, k.left), pointscale(-1, k.centre));
+    return r;
+}
+
+static inline Kite kite_forward_right(Kite k)
+{
+    Kite r;
+    r.outer = k.outer;
+    r.left = k.right;
+    r.centre = pointadd(pointscale(2, k.right), pointscale(-1, k.centre));
+    r.right = pointadd(pointadd(k.left, k.right), pointscale(-1, k.centre));
+    return r;
+}
+
+typedef enum KiteStep { KS_LEFT, KS_RIGHT, KS_F_LEFT, KS_F_RIGHT } KiteStep;
+
+static inline Kite kite_step(Kite k, KiteStep step)
+{
+    switch (step) {
+      case KS_LEFT: return kite_left(k);
+      case KS_RIGHT: return kite_right(k);
+      case KS_F_LEFT: return kite_forward_left(k);
+      default /* case KS_F_RIGHT */: return kite_forward_right(k);
+    }
+}
+
+/*
+ * Functiond to enumerate the kites in a rectangular region, in a
+ * serpentine-raster fashion so that every kite delivered shares an
+ * edge with a recent previous one.
+ */
+#define KE_NKEEP 3
+typedef struct KiteEnum {
+    /* Fields private to the enumerator */
+    int state;
+    int x, y, w, h;
+    unsigned curr_index;
+
+    /* Fields the client can legitimately read out */
+    Kite *curr;
+    Kite recent[KE_NKEEP];
+    unsigned last_index;
+    KiteStep last_step; /* step that got curr from recent[last_index] */
+} KiteEnum;
+void hat_kiteenum_first(KiteEnum *s, int w, int h);
+bool hat_kiteenum_next(KiteEnum *s);
+
+/*
+ * Assorted useful definitions.
+ */
+typedef enum TileType { TT_H, TT_T, TT_P, TT_F, TT_KITE, TT_HAT } TileType;
+static const char tilechars[] = "HTPF";
+
+#define HAT_KITES 8     /* number of kites in a hat */
+#define MT_MAXEXPAND 13 /* largest number of metatiles in any expansion */
+
+/*
+ * Definitions for the autogenerated hat-tables.h header file that
+ * defines all the lookup tables.
+ */
+typedef struct KitemapEntry {
+    int kite, hat, meta;               /* all -1 if impossible */
+} KitemapEntry;
+
+typedef struct MetamapEntry {
+    int meta, meta2;
+} MetamapEntry;
+
+static inline size_t kitemap_index(KiteStep step, unsigned kite,
+                                   unsigned hat, unsigned meta)
+{
+    return step + 4 * (kite + 8 * (hat + 4 * meta));
+}
+
+static inline size_t metamap_index(unsigned meta, unsigned meta2)
+{
+    return meta2 * MT_MAXEXPAND + meta;
+}
+
+/*
+ * Coordinate system for tracking kites within a randomly selected
+ * part of the recursively expanded hat tiling.
+ *
+ * HatCoords will store an array of HatCoord, in little-endian
+ * arrangement. So hc->c[0] will always have type TT_KITE and index a
+ * single kite within a hat; hc->c[1] will have type TT_HAT and index
+ * a hat within a first-order metatile; hc->c[2] will be the smallest
+ * metatile containing this hat, and hc->c[3, 4, 5, ...] will be
+ * higher-order metatiles as needed.
+ *
+ * The last coordinate stored, hc->c[hc->nc-1], will have a tile type
+ * but no index (represented by index==-1). This means "we haven't
+ * decided yet what this level of metatile needs to be". If we need to
+ * refer to this level during the hatctx_step algorithm, we make it up
+ * at random, based on a table of what metatiles each type can
+ * possibly be part of, at what index.
+ */
+typedef struct HatCoord {
+    int index; /* index within that tile, or -1 if not yet known */
+    TileType type;  /* type of this tile */
+} HatCoord;
+
+typedef struct HatCoords {
+    HatCoord *c;
+    size_t nc, csize;
+} HatCoords;
+
+HatCoords *hat_coords_new(void);
+void hat_coords_free(HatCoords *hc);
+void hat_coords_make_space(HatCoords *hc, size_t size);
+HatCoords *hat_coords_copy(HatCoords *hc_in);
+
+#ifdef HAT_COORDS_DEBUG
+static inline void hat_coords_debug(const char *prefix, HatCoords *hc,
+                                    const char *suffix)
+{
+    const char *sep = "";
+    static const char *const types[] = {"H","T","P","F","kite","hat"};
+
+    fputs(prefix, stderr);
+    for (size_t i = 0; i < hc->nc; i++) {
+        fprintf(stderr, "%s %s ", sep, types[hc->c[i].type]);
+        sep = " .";
+        if (hc->c[i].index == -1)
+            fputs("?", stderr);
+        else
+            fprintf(stderr, "%d", hc->c[i].index);
+    }
+    fputs(suffix, stderr);
+}
+#else
+#define hat_coords_debug(p,c,s) ((void)0)
+#endif
+
+/*
+ * HatContext is the shared context of a whole run of the algorithm.
+ * Its 'prototype' HatCoords object represents the coordinates of the
+ * starting kite, and is extended as necessary; any other HatCoord
+ * 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.
+ *
+ * (Of course another approach would be to reject grid descriptions
+ * that didn't define enough coordinates! But that would involve a
+ * whole extra iteration over the whole grid region just for
+ * validation, and that seems like more timewasting than really
+ * needed. So we tolerate short descriptions, and do something
+ * deterministic with them.)
+ */
+
+typedef struct HatContext {
+    random_state *rs;
+    HatCoords *prototype;
+} HatContext;
+
+void hatctx_init_random(HatContext *ctx, random_state *rs);
+void hatctx_cleanup(HatContext *ctx);
+HatCoords *hatctx_initial_coords(HatContext *ctx);
+void hatctx_extend_coords(HatContext *ctx, HatCoords *hc, size_t n);
+HatCoords *hatctx_step(HatContext *ctx, HatCoords *hc_in, KiteStep step);
+
+/*
+ * Subroutine of hat_tiling_generate, called for each kite in the grid
+ * as we iterate over it, to decide whether to generate an output hat
+ * and pass it to the client. Exposed in this header file so that
+ * hat-test can reuse it.
+ *
+ * We do this by starting from kite #0 of each hat, and tracing round
+ * the boundary. If the whole boundary is within the caller's bounding
+ * region, we return it; if it goes off the edge, we don't.
+ *
+ * (Of course, every hat we _do_ want to return will have all its
+ * kites inside the rectangle, so its kite #0 will certainly be caught
+ * by this iteration.)
+ */
+
+typedef void (*internal_hat_callback_fn)(void *ctx, Kite kite0, HatCoords *hc,
+                                         int *coords);
+void maybe_report_hat(int w, int h, Kite kite, HatCoords *hc,
+                      internal_hat_callback_fn cb, void *cbctx);
--- a/hat.c
+++ b/hat.c
@@ -20,131 +20,10 @@
 
 #include "puzzles.h"
 #include "hat.h"
+#include "hat-internal.h"
 
-/*
- * Coordinate system:
- *
- * The output of this code lives on the tiling known to grid.c as
- * 'Kites', which can be viewed as a tiling of hexagons each of which
- * is subdivided into six kites sharing their pointy vertex, or
- * (equivalently) a tiling of equilateral triangles each subdivided
- * into three kits sharing their blunt vertex.
- *
- * We express coordinates in this system relative to the basis (1, r)
- * where r = (1 + sqrt(3)i) / 2 is a primitive 6th root of unity. This
- * gives us a system in which two integer coordinates can address any
- * grid point, provided we scale up so that the side length of the
- * equilateral triangles in the tiling is 6.
- */
-
-typedef struct Point {
-    int x, y;                          /* represents x + yr */
-} Point;
-
-static inline Point pointscale(int scale, Point a)
+void hat_kiteenum_first(KiteEnum *s, int w, int h)
 {
-    Point r = { scale * a.x, scale * a.y };
-    return r;
-}
-
-static inline Point pointadd(Point a, Point b)
-{
-    Point r = { a.x + b.x, a.y + b.y };
-    return r;
-}
-
-/*
- * We identify a single kite by the coordinates of its four vertices.
- * This allows us to construct the coordinates of an adjacent kite by
- * taking affine transformations of the original kite's vertices.
- *
- * This is a useful way to do it because it means that if you reflect
- * the kite (by swapping its left and right vertices) then these
- * transformations also perform in a reflected way. This will be
- * useful in the code below that outputs the coordinates of each hat,
- * because this way it can work by walking around its 8 kites using a
- * fixed set of steps, and if the hat is reflected, then we just
- * reflect the starting kite before doing that, and everything still
- * works.
- */
-
-typedef struct Kite {
-    Point centre, left, right, outer;
-} Kite;
-
-static inline Kite kite_left(Kite k)
-{
-    Kite r;
-    r.centre = k.centre;
-    r.right = k.left;
-    r.outer = pointadd(pointscale(2, k.left), pointscale(-1, k.outer));
-    r.left = pointadd(pointadd(k.centre, k.left), pointscale(-1, k.right));
-    return r;
-}
-
-static inline Kite kite_right(Kite k)
-{
-    Kite r;
-    r.centre = k.centre;
-    r.left = k.right;
-    r.outer = pointadd(pointscale(2, k.right), pointscale(-1, k.outer));
-    r.right = pointadd(pointadd(k.centre, k.right), pointscale(-1, k.left));
-    return r;
-}
-
-static inline Kite kite_forward_left(Kite k)
-{
-    Kite r;
-    r.outer = k.outer;
-    r.right = k.left;
-    r.centre = pointadd(pointscale(2, k.left), pointscale(-1, k.centre));
-    r.left = pointadd(pointadd(k.right, k.left), pointscale(-1, k.centre));
-    return r;
-}
-
-static inline Kite kite_forward_right(Kite k)
-{
-    Kite r;
-    r.outer = k.outer;
-    r.left = k.right;
-    r.centre = pointadd(pointscale(2, k.right), pointscale(-1, k.centre));
-    r.right = pointadd(pointadd(k.left, k.right), pointscale(-1, k.centre));
-    return r;
-}
-
-typedef enum KiteStep { KS_LEFT, KS_RIGHT, KS_F_LEFT, KS_F_RIGHT } KiteStep;
-
-static inline Kite kite_step(Kite k, KiteStep step)
-{
-    switch (step) {
-      case KS_LEFT: return kite_left(k);
-      case KS_RIGHT: return kite_right(k);
-      case KS_F_LEFT: return kite_forward_left(k);
-      default /* case KS_F_RIGHT */: return kite_forward_right(k);
-    }
-}
-
-/*
- * Function to enumerate the kites in a rectangular region, in a
- * serpentine-raster fashion so that every kite delivered shares an
- * edge with a recent previous one.
- */
-#define KE_NKEEP 3
-typedef struct KiteEnum {
-    /* Fields private to the enumerator */
-    int state;
-    int x, y, w, h;
-    unsigned curr_index;
-
-    /* Fields the client can legitimately read out */
-    Kite *curr;
-    Kite recent[KE_NKEEP];
-    unsigned last_index;
-    KiteStep last_step; /* step that got curr from recent[last_index] */
-} KiteEnum;
-
-static void first_kite(KiteEnum *s, int w, int h)
-{
     Kite start = { {0,0}, {0, 3}, {3, 0}, {2, 2} };
     size_t i;
 
@@ -158,7 +37,8 @@
     s->x = 0;
     s->y = 0;
 }
-static bool next_kite(KiteEnum *s)
+
+bool hat_kiteenum_next(KiteEnum *s)
 {
     unsigned lastbut1 = s->last_index;
     s->last_index = s->curr_index;
@@ -306,38 +186,6 @@
 }
 
 /*
- * Assorted useful definitions.
- */
-typedef enum TileType { TT_H, TT_T, TT_P, TT_F, TT_KITE, TT_HAT } TileType;
-static const char tilechars[] = "HTPF";
-
-#define HAT_KITES 8     /* number of kites in a hat */
-#define MT_MAXEXPAND 13 /* largest number of metatiles in any expansion */
-
-/*
- * Definitions for the autogenerated hat-tables.h header file that
- * defines all the lookup tables.
- */
-typedef struct KitemapEntry {
-    int kite, hat, meta;               /* all -1 if impossible */
-} KitemapEntry;
-
-typedef struct MetamapEntry {
-    int meta, meta2;
-} MetamapEntry;
-
-static inline size_t kitemap_index(KiteStep step, unsigned kite,
-                                   unsigned hat, unsigned meta)
-{
-    return step + 4 * (kite + 8 * (hat + 4 * meta));
-}
-
-static inline size_t metamap_index(unsigned meta, unsigned meta2)
-{
-    return meta2 * MT_MAXEXPAND + meta;
-}
-
-/*
  * The actual tables.
  */
 #include "hat-tables.h"
@@ -524,35 +372,7 @@
 #undef PROB_P
 #undef PROB_F
 
-/*
- * Coordinate system for tracking kites within a randomly selected
- * part of the recursively expanded hat tiling.
- *
- * HatCoords will store an array of HatCoord, in little-endian
- * arrangement. So hc->c[0] will always have type TT_KITE and index a
- * single kite within a hat; hc->c[1] will have type TT_HAT and index
- * a hat within a first-order metatile; hc->c[2] will be the smallest
- * metatile containing this hat, and hc->c[3, 4, 5, ...] will be
- * higher-order metatiles as needed.
- *
- * The last coordinate stored, hc->c[hc->nc-1], will have a tile type
- * but no index (represented by index==-1). This means "we haven't
- * decided yet what this level of metatile needs to be". If we need to
- * refer to this level during the step_coords algorithm, we make it up
- * at random, based on a table of what metatiles each type can
- * possibly be part of, at what index.
- */
-typedef struct HatCoord {
-    int index; /* index within that tile, or -1 if not yet known */
-    TileType type;  /* type of this tile */
-} HatCoord;
-
-typedef struct HatCoords {
-    HatCoord *c;
-    size_t nc, csize;
-} HatCoords;
-
-static HatCoords *hc_new(void)
+HatCoords *hat_coords_new(void)
 {
     HatCoords *hc = snew(HatCoords);
     hc->nc = hc->csize = 0;
@@ -560,7 +380,7 @@
     return hc;
 }
 
-static void hc_free(HatCoords *hc)
+void hat_coords_free(HatCoords *hc)
 {
     if (hc) {
         sfree(hc->c);
@@ -568,7 +388,7 @@
     }
 }
 
-static void hc_make_space(HatCoords *hc, size_t size)
+void hat_coords_make_space(HatCoords *hc, size_t size)
 {
     if (hc->csize < size) {
         hc->csize = hc->csize * 5 / 4 + 16;
@@ -578,10 +398,10 @@
     }
 }
 
-static HatCoords *hc_copy(HatCoords *hc_in)
+HatCoords *hat_coords_copy(HatCoords *hc_in)
 {
-    HatCoords *hc_out = hc_new();
-    hc_make_space(hc_out, hc_in->nc);
+    HatCoords *hc_out = hat_coords_new();
+    hat_coords_make_space(hc_out, hc_in->nc);
     memcpy(hc_out->c, hc_in->c, hc_in->nc * sizeof(*hc_out->c));
     hc_out->nc = hc_in->nc;
     return hc_out;
@@ -615,43 +435,14 @@
     assert(value < parents[i].probability);
     return &parents[i];
 }
-
-/*
- * HatCoordContext is the shared context of a whole run of the
- * algorithm. Its 'prototype' HatCoords object represents the
- * coordinates of the starting kite, and is extended as necessary; any
- * other HatCoord 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.
- *
- * (Of course another approach would be to reject grid descriptions
- * that didn't define enough coordinates! But that would involve a
- * whole extra iteration over the whole grid region just for
- * validation, and that seems like more timewasting than really
- * needed. So we tolerate short descriptions, and do something
- * deterministic with them.)
- */
-
-typedef struct HatCoordContext {
-    random_state *rs;
-    HatCoords *prototype;
-} HatCoordContext;
-
-static void init_coords_random(HatCoordContext *ctx, random_state *rs)
+void hatctx_init_random(HatContext *ctx, random_state *rs)
 {
     const MetatilePossibleParent *starting_hat = choose_mpp(
         rs, starting_hats, lenof(starting_hats));
 
     ctx->rs = rs;
-    ctx->prototype = hc_new();
-    hc_make_space(ctx->prototype, 3);
+    ctx->prototype = hat_coords_new();
+    hat_coords_make_space(ctx->prototype, 3);
     ctx->prototype->c[2].type = starting_hat->type;
     ctx->prototype->c[2].index = -1;
     ctx->prototype->c[1].type = TT_HAT;
@@ -669,17 +460,17 @@
             metatile == 'F' ? TT_F : -1);
 }
 
-static void init_coords_params(HatCoordContext *ctx,
+static void init_coords_params(HatContext *ctx,
                                const struct HatPatchParams *hp)
 {
     size_t i;
 
     ctx->rs = NULL;
-    ctx->prototype = hc_new();
+    ctx->prototype = hat_coords_new();
 
     assert(hp->ncoords >= 3);
 
-    hc_make_space(ctx->prototype, hp->ncoords + 1);
+    hat_coords_make_space(ctx->prototype, hp->ncoords + 1);
     ctx->prototype->nc = hp->ncoords + 1;
 
     for (i = 0; i < hp->ncoords; i++)
@@ -701,9 +492,9 @@
     assert(hp->coords[0] < 8);
 }
 
-static HatCoords *initial_coords(HatCoordContext *ctx)
+HatCoords *hatctx_initial_coords(HatContext *ctx)
 {
-    return hc_copy(ctx->prototype);
+    return hat_coords_copy(ctx->prototype);
 }
 
 /*
@@ -711,10 +502,10 @@
  * ctx->prototype if needed, and extending ctx->prototype if needed in
  * order to do that.
  */
-static void ensure_coords(HatCoordContext *ctx, HatCoords *hc, size_t n)
+void hatctx_extend_coords(HatContext *ctx, HatCoords *hc, size_t n)
 {
     if (ctx->prototype->nc < n) {
-        hc_make_space(ctx->prototype, n);
+        hat_coords_make_space(ctx->prototype, n);
         while (ctx->prototype->nc < n) {
             TileType type = ctx->prototype->c[ctx->prototype->nc - 1].type;
             assert(ctx->prototype->c[ctx->prototype->nc - 1].index == -1);
@@ -733,7 +524,7 @@
         }
     }
 
-    hc_make_space(hc, n);
+    hat_coords_make_space(hc, n);
     while (hc->nc < n) {
         assert(hc->c[hc->nc - 1].index == -1);
         assert(hc->c[hc->nc - 1].type == ctx->prototype->c[hc->nc - 1].type);
@@ -744,33 +535,11 @@
     }
 }
 
-static void cleanup_coords(HatCoordContext *ctx)
+void hatctx_cleanup(HatContext *ctx)
 {
-    hc_free(ctx->prototype);
+    hat_coords_free(ctx->prototype);
 }
 
-#ifdef DEBUG_COORDS
-static inline void debug_coords(const char *prefix, HatCoords *hc,
-                                const char *suffix)
-{
-    const char *sep = "";
-    static const char *const types[] = {"H","T","P","F","kite","hat"};
-
-    fputs(prefix, stderr);
-    for (size_t i = 0; i < hc->nc; i++) {
-        fprintf(stderr, "%s %s ", sep, types[hc->c[i].type]);
-        sep = " .";
-        if (hc->c[i].index == -1)
-            fputs("?", stderr);
-        else
-            fprintf(stderr, "%d", hc->c[i].index);
-    }
-    fputs(suffix, stderr);
-}
-#else
-#define debug_coords(p,c,s) ((void)0)
-#endif
-
 /*
  * The actual system for finding the coordinates of an adjacent kite.
  */
@@ -781,10 +550,10 @@
  * around the individual kites. If this fails, return NULL.
  */
 static HatCoords *try_step_coords_kitemap(
-    HatCoordContext *ctx, HatCoords *hc_in, KiteStep step)
+    HatContext *ctx, HatCoords *hc_in, KiteStep step)
 {
-    ensure_coords(ctx, hc_in, 4);
-    debug_coords("  try kitemap  ", hc_in, "\n");
+    hatctx_extend_coords(ctx, hc_in, 4);
+    hat_coords_debug("  try kitemap  ", hc_in, "\n");
     unsigned kite = hc_in->c[0].index;
     unsigned hat = hc_in->c[1].index;
     unsigned meta = hc_in->c[2].index;
@@ -796,7 +565,7 @@
          * Success! We've got coordinates for the next kite in this
          * direction.
          */
-        HatCoords *hc_out = hc_copy(hc_in);
+        HatCoords *hc_out = hat_coords_copy(hc_in);
 
         hc_out->c[2].index = ke->meta;
         hc_out->c[2].type = children[meta2type][ke->meta];
@@ -805,7 +574,7 @@
         hc_out->c[0].index = ke->kite;
         hc_out->c[0].type = TT_KITE;
 
-        debug_coords("  success!     ", hc_out, "\n");
+        hat_coords_debug("  success!     ", hc_out, "\n");
         return hc_out;
     }
 
@@ -821,14 +590,14 @@
  * metamap rewrite), return NULL.
  */
 static HatCoords *try_step_coords_metamap(
-    HatCoordContext *ctx, HatCoords *hc_in, KiteStep step, size_t depth)
+    HatContext *ctx, HatCoords *hc_in, KiteStep step, size_t depth)
 {
     HatCoords *hc_tmp = NULL, *hc_out;
 
-    ensure_coords(ctx, hc_in, depth+3);
-#ifdef DEBUG_COORDS
+    hatctx_extend_coords(ctx, hc_in, depth+3);
+#ifdef HAT_COORDS_DEBUG
     fprintf(stderr, "  try meta %-4d", (int)depth);
-    debug_coords("", hc_in, "\n");
+    hat_coords_debug("", hc_in, "\n");
 #endif
     unsigned meta_orig = hc_in->c[depth].index;
     unsigned meta2_orig = hc_in->c[depth+1].index;
@@ -845,7 +614,7 @@
         else
             hc_out = try_step_coords_kitemap(ctx, hc_curr, step);
         if (hc_out) {
-            hc_free(hc_tmp);
+            hat_coords_free(hc_tmp);
             return hc_out;
         }
 
@@ -852,7 +621,7 @@
         me = &metamap[meta3type][metamap_index(meta, meta2)];
         assert(me->meta != -1);
         if (me->meta == meta_orig && me->meta2 == meta2_orig) {
-            hc_free(hc_tmp);
+            hat_coords_free(hc_tmp);
             return NULL;
         }
 
@@ -871,7 +640,7 @@
          * just use a separate copy.
          */
         if (!hc_tmp)
-            hc_tmp = hc_copy(hc_in);
+            hc_tmp = hat_coords_copy(hc_in);
 
         hc_tmp->c[depth+1].index = meta2;
         hc_tmp->c[depth+1].type = children[meta3type][meta2];
@@ -878,7 +647,7 @@
         hc_tmp->c[depth].index = meta;
         hc_tmp->c[depth].type = children[hc_tmp->c[depth+1].type][meta];
 
-        debug_coords("  rewritten -> ", hc_tmp, "\n");
+        hat_coords_debug("  rewritten -> ", hc_tmp, "\n");
     }
 }
 
@@ -885,16 +654,15 @@
 /*
  * The top-level algorithm for finding the next tile.
  */
-static HatCoords *step_coords(HatCoordContext *ctx, HatCoords *hc_in,
-                              KiteStep step)
+HatCoords *hatctx_step(HatContext *ctx, HatCoords *hc_in, KiteStep step)
 {
     HatCoords *hc_out;
     size_t depth;
 
-#ifdef DEBUG_COORDS
+#ifdef HAT_COORDS_DEBUG
     static const char *const directions[] = {
         " left\n", " right\n", " forward left\n", " forward right\n" };
-    debug_coords("step start     ", hc_in, directions[step]);
+    hat_coords_debug("step start     ", hc_in, directions[step]);
 #endif
 
     /*
@@ -918,9 +686,10 @@
 
 /*
  * Generate a random set of parameters for a tiling of a given size.
- * To do this, we iterate over the whole tiling via first_kite and
- * next_kite, and for each kite, calculate its coordinates. But then
- * we throw the coordinates away and don't do anything with them!
+ * To do this, we iterate over the whole tiling via hat_kiteenum_first
+ * and hat_kiteenum_next, and for each kite, calculate its
+ * coordinates. But then we throw the coordinates away and don't do
+ * anything with them!
  *
  * But the side effect of _calculating_ all those coordinates is that
  * we found out how far ctx->prototype needed to be extended, and did
@@ -931,21 +700,21 @@
 void hat_tiling_randomise(struct HatPatchParams *hp, int w, int h,
                           random_state *rs)
 {
-    HatCoordContext ctx[1];
+    HatContext ctx[1];
     HatCoords *coords[KE_NKEEP];
     KiteEnum s[1];
     size_t i;
 
-    init_coords_random(ctx, rs);
+    hatctx_init_random(ctx, rs);
     for (i = 0; i < lenof(coords); i++)
         coords[i] = NULL;
 
-    first_kite(s, w, h);
-    coords[s->curr_index] = initial_coords(ctx);
+    hat_kiteenum_first(s, w, h);
+    coords[s->curr_index] = hatctx_initial_coords(ctx);
 
-    while (next_kite(s)) {
-        hc_free(coords[s->curr_index]);
-        coords[s->curr_index] = step_coords(
+    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);
     }
 
@@ -955,9 +724,9 @@
         hp->coords[i] = ctx->prototype->c[i].index;
     hp->final_metatile = tilechars[ctx->prototype->c[hp->ncoords].type];
 
-    cleanup_coords(ctx);
+    hatctx_cleanup(ctx);
     for (i = 0; i < lenof(coords); i++)
-        hc_free(coords[i]);
+        hat_coords_free(coords[i]);
 }
 
 const char *hat_tiling_params_invalid(const struct HatPatchParams *hp)
@@ -985,24 +754,8 @@
     return NULL;
 }
 
-/*
- * For each kite generated by hat_tiling_generate, potentially
- * generate an output hat and give it to our caller.
- *
- * We do this by starting from kite #0 of each hat, and tracing round
- * the boundary. If the whole boundary is within the caller's bounding
- * region, we return it; if it goes off the edge, we don't.
- *
- * (Of course, every hat we _do_ want to return will have all its
- * kites inside the rectangle, so its kite #0 will certainly be caught
- * by this iteration.)
- */
-
-typedef void (*internal_hat_callback_fn)(void *ctx, Kite kite0, HatCoords *hc,
-                                         int *coords);
-
-static void maybe_report_hat(int w, int h, Kite kite, HatCoords *hc,
-                             internal_hat_callback_fn cb, void *cbctx)
+void maybe_report_hat(int w, int h, Kite kite, HatCoords *hc,
+                      internal_hat_callback_fn cb, void *cbctx)
 {
     Kite kite0;
     Point vertices[14];
@@ -1095,7 +848,7 @@
 void hat_tiling_generate(const struct HatPatchParams *hp, int w, int h,
                          hat_tile_callback_fn cb, void *cbctx)
 {
-    HatCoordContext ctx[1];
+    HatContext ctx[1];
     HatCoords *coords[KE_NKEEP];
     KiteEnum s[1];
     size_t i;
@@ -1108,633 +861,20 @@
     for (i = 0; i < lenof(coords); i++)
         coords[i] = NULL;
 
-    first_kite(s, w, h);
-    coords[s->curr_index] = initial_coords(ctx);
+    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],
                      report_hat, report_hat_ctx);
 
-    while (next_kite(s)) {
-        hc_free(coords[s->curr_index]);
-        coords[s->curr_index] = step_coords(
+    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],
                          report_hat, report_hat_ctx);
     }
 
-    cleanup_coords(ctx);
+    hatctx_cleanup(ctx);
     for (i = 0; i < lenof(coords); i++)
-        hc_free(coords[i]);
+        hat_coords_free(coords[i]);
 }
-
-#ifdef TEST_HAT
-
-#include <stdarg.h>
-
-static HatCoords *hc_construct_v(TileType type, va_list ap)
-{
-    HatCoords *hc = hc_new();
-    while (true) {
-        int index = va_arg(ap, int);
-
-        hc_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 *hc_construct(TileType type, ...)
-{
-    HatCoords *hc;
-    va_list ap;
-
-    va_start(ap, type);
-    hc = hc_construct_v(type, ap);
-    va_end(ap);
-
-    return hc;
-}
-
-static bool hc_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 hc_expect(const char *file, int line, HatCoords *hc,
-                      TileType type, ...)
-{
-    bool equal;
-    va_list ap;
-    HatCoords *hce;
-
-    va_start(ap, type);
-    hce = hc_construct_v(type, ap);
-    va_end(ap);
-
-    equal = hc_equal(hc, hce);
-
-    if (!equal) {
-        fprintf(stderr, "%s:%d: coordinate mismatch\n", file, line);
-        debug_coords("  expected: ", hce, "\n");
-        debug_coords("  actual:   ", hc, "\n");
-    }
-
-    hc_free(hce);
-    return equal;
-}
-
-#define EXPECT(hc, ...) do {                                    \
-        if (!hc_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,
-};
-
-/*
- * 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,
-    HatCoordContext *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.
-     */
-    ensure_coords(ctx, prevc, currc->nc);
-    ensure_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 step_coords 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 = step_coords(
-        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;
-}
-
-static bool unit_tests(void)
-{
-    int fails = 0;
-    HatCoordContext ctx[1];
-    HatCoords *hc_in, *hc_out;
-
-    ctx->rs = NULL;
-    ctx->prototype = hc_construct(TT_KITE, 0, TT_HAT, 0, TT_H, -1);
-
-    /* Simple steps within a hat */
-
-    hc_in = hc_construct(TT_KITE, 6, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_out = step_coords(ctx, hc_in, KS_LEFT);
-    EXPECT(hc_out, TT_KITE, 5, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_free(hc_in);
-    hc_free(hc_out);
-
-    hc_in = hc_construct(TT_KITE, 6, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_out = step_coords(ctx, hc_in, KS_RIGHT);
-    EXPECT(hc_out, TT_KITE, 7, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_free(hc_in);
-    hc_free(hc_out);
-
-    hc_in = hc_construct(TT_KITE, 5, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_out = step_coords(ctx, hc_in, KS_F_LEFT);
-    EXPECT(hc_out, TT_KITE, 2, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_free(hc_in);
-    hc_free(hc_out);
-
-    hc_in = hc_construct(TT_KITE, 5, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_out = step_coords(ctx, hc_in, KS_F_RIGHT);
-    EXPECT(hc_out, TT_KITE, 1, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_free(hc_in);
-    hc_free(hc_out);
-
-    /* Step between hats in the same kitemap, which can change the
-     * metatile type at layer 2 */
-
-    hc_in = hc_construct(TT_KITE, 6, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_out = step_coords(ctx, hc_in, KS_F_LEFT);
-    EXPECT(hc_out, TT_KITE, 3, TT_HAT, 0, TT_H, 0, TT_H, -1);
-    hc_free(hc_in);
-    hc_free(hc_out);
-
-    hc_in = hc_construct(TT_KITE, 7, TT_HAT, 2, TT_H, 1, TT_H, -1);
-    hc_out = step_coords(ctx, hc_in, KS_F_RIGHT);
-    EXPECT(hc_out, TT_KITE, 4, TT_HAT, 0, TT_T, 3, TT_H, -1);
-    hc_free(hc_in);
-    hc_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 = hc_construct(TT_KITE, 6, TT_HAT, 0, TT_P, 2, TT_P, 3, TT_P, -1);
-    hc_out = step_coords(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);
-    hc_free(hc_in);
-    hc_free(hc_out);
-
-    cleanup_coords(ctx);
-    return fails == 0;
-}
-
-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];
-    HatCoordContext 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));
-    init_coords_random(ctx, rs);
-
-    bbox->started = false;
-    dctx->bbox = bbox;
-
-    first_kite(s, w, h);
-    coords[s->curr_index] = initial_coords(ctx);
-    maybe_report_hat(w, h, *s->curr, coords[s->curr_index],
-                     bbox_add_hat, dctx);
-    while (next_kite(s)) {
-        hc_free(coords[s->curr_index]);
-        coords[s->curr_index] = step_coords(
-            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++) {
-        hc_free(coords[i]);
-        coords[i] = NULL;
-    }
-
-    header(dctx);
-
-    first_kite(s, w, h);
-    coords[s->curr_index] = 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 (next_kite(s)) {
-        hc_free(coords[s->curr_index]);
-        coords[s->curr_index] = step_coords(
-            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++) {
-        hc_free(coords[i]);
-        coords[i] = NULL;
-    }
-
-    trailer(dctx);
-
-    cleanup_coords(ctx);
-
-    return 0;
-}
-#endif