shithub: puzzles

ref: 59ac8a69af1330a6ea482154b05bc23ec33b594d
dir: /palisade.c/

View raw version
/* -*- indent-tabs-mode: nil; tab-width: 1000 -*- */

/*
 * palisade.c: Nikoli's `Five Cells' puzzle.
 *
 * See http://nikoli.co.jp/en/puzzles/five_cells.html
 */

/* TODO:
 *
 * - better solver: implement the sketched-out deductions
 *
 * - improve the victory flash?
 *    - the LINE_NOs look ugly against COL_FLASH.
 *    - white-blink the edges (instead), a la loopy?
 */

#include <assert.h>
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "puzzles.h"

#define setmem(ptr, byte, len) memset((ptr), (byte), (len) * sizeof (ptr)[0])
#define scopy(dst, src, len) memcpy((dst), (src), (len) * sizeof (dst)[0])
#define dupmem(p, n) memcpy(smalloc(n * sizeof (*p)), p, n * sizeof (*p))
#define snewa(ptr, len) (ptr) = smalloc((len) * sizeof (*ptr))
#define clone(ptr) (dupmem((ptr), 1))

static char *string(int n, const char *fmt, ...)
{
    va_list va;
    char *ret;
    int m;
    va_start(va, fmt);
    m = vsprintf(snewa(ret, n + 1), fmt, va);
    va_end(va);
    if (m > n) fatal("memory corruption");
    return ret;
}

struct game_params {
    int w, h, k;
};

typedef char clue;
typedef unsigned char borderflag;

typedef struct shared_state {
    game_params params;
    clue *clues;
    int refcount;
} shared_state;

struct game_state {
    shared_state *shared;
    borderflag *borders; /* length w*h */

    bool completed, cheated;
};

#define DEFAULT_PRESET 0
static struct game_params presets[] = {
    {5, 5, 5}, {8, 6, 6}, {10, 8, 8}, {15, 12, 10}
    /* I definitely want 5x5n5 since that gives "Five Cells" its name.
     * But how about the others?  By which criteria do I choose? */
};

static game_params *default_params(void)
{
    return clone(&presets[DEFAULT_PRESET]);
}

static bool game_fetch_preset(int i, char **name, game_params **params)
{
    if (i < 0 || i >= lenof(presets)) return false;

    *params = clone(&presets[i]);
    *name = string(60, "%d x %d, regions of size %d",
                   presets[i].w, presets[i].h, presets[i].k);

    return true;
}

static void free_params(game_params *params)
{
    sfree(params);
}

static game_params *dup_params(const game_params *params)
{
    return clone(params);
}

static void decode_params(game_params *params, char const *string)
{
    params->w = params->h = params->k = atoi(string);
    while (*string && isdigit((unsigned char)*string)) ++string;
    if (*string == 'x') {
        params->h = atoi(++string);
        while (*string && isdigit((unsigned char)*string)) ++string;
    }
    if (*string == 'n') params->k = atoi(++string);
}

static char *encode_params(const game_params *params, bool full)
{
    return string(40, "%dx%dn%d", params->w, params->h, params->k);
}

#define CONFIG(i, nm, ty, iv, sv) \
    (ret[i].name = nm, ret[i].type = ty, ret[i].ival = iv, ret[i].sval = sv)

static config_item *game_configure(const game_params *params)
{
    config_item *ret = snewn(4, config_item);

    ret[0].name = "Width";
    ret[0].type = C_STRING;
    ret[0].u.string.sval = string(20, "%d", params->w);

    ret[1].name = "Height";
    ret[1].type = C_STRING;
    ret[1].u.string.sval = string(20, "%d", params->h);

    ret[2].name = "Region size";
    ret[2].type = C_STRING;
    ret[2].u.string.sval = string(20, "%d", params->k);

    ret[3].name = NULL;
    ret[3].type = C_END;

    return ret;
}

static game_params *custom_params(const config_item *cfg)
{
    game_params *params = snew(game_params);

    params->w = atoi(cfg[0].u.string.sval);
    params->h = atoi(cfg[1].u.string.sval);
    params->k = atoi(cfg[2].u.string.sval);

    return params;
}

/* +---+  <<  The one possible domino (up to symmetry).      +---+---+
 * | 3 |                                                     | 3 | 3 |
 * |   |   If two dominos are adjacent as depicted here  >>  +---+---+
 * | 3 |   then it's ambiguous whether the edge between      | 3 | 3 |
 * +---+   the dominos is horizontal or vertical.            +---+---+
 */

static const char *validate_params(const game_params *params, bool full)
{
    int w = params->w, h = params->h, k = params->k, wh = w * h;

    if (k < 1) return "Region size must be at least one";
    if (w < 1) return "Width must be at least one";
    if (h < 1) return "Height must be at least one";
    if (wh % k) return "Region size must divide grid area";

    if (!full) return NULL; /* succeed partial validation */

    /* MAYBE FIXME: we (just?) don't have the UI for winning these. */
    if (k == wh) return "Region size must be less than the grid area";
    assert (k < wh); /* or wh % k != 0 */

    if (k == 2 && w != 1 && h != 1)
        return "Region size can't be two unless width or height is one";

    return NULL; /* succeed full validation */
}

/* --- Solver ------------------------------------------------------- */

/* the solver may write at will to these arrays, but shouldn't free them */
/* it's up to the client to dup/free as needed */
typedef struct solver_ctx {
    const game_params *params;  /* also in shared_state */
    clue *clues;                /* also in shared_state */
    borderflag *borders;        /* also in game_state */
    int *dsf;                   /* particular to the solver */
} solver_ctx;

/* Deductions:
 *
 * - If two adjacent clues do not have a border between them, this
 *   gives a lower limit on the size of their region (which is also an
 *   upper limit if both clues are 3).  Rule out any non-border which
 *   would make its region either too large or too small.
 *
 * - If a clue, k, is adjacent to k borders or (4 - k) non-borders,
 *   the remaining edges incident to the clue are readily decided.
 *
 * - If a region has only one other region (e.g. square) to grow into
 *   and it's not of full size yet, grow it into that one region.
 *
 * - If two regions are adjacent and their combined size would be too
 *   large, put an edge between them.
 *
 * - If a border is adjacent to two non-borders, its last vertex-mate
 *   must also be a border.  If a maybe-border is adjacent to three
 *   nonborders, the maybe-border is a non-border.
 *
 * - If a clue square is adjacent to several squares belonging to the
 *   same region, and enabling (disabling) those borders would violate
 *   the clue, those borders must be disabled (enabled).
 *
 * - If there's a path crossing only non-borders between two squares,
 *   the maybe-border between them is a non-border.
 *   (This is implicitly computed in the dsf representation)
 */

/* TODO deductions:
 *
 * If a vertex is adjacent to a LINE_YES and (4-3)*LINE_NO, at least
 * one of the last two edges are LINE_YES.  If they're adjacent to a
 * 1, then the other two edges incident to that 1 are LINE_NO.
 *
 * For each square: set all as unknown, then for each k-omino and each
 * way of placing it on that square, if that way is consistent with
 * the board, mark its edges and interior as possible LINE_YES and
 * LINE_NO, respectively.  When all k-ominos are through, see what
 * isn't possible and remove those impossibilities from the board.
 * (Sounds pretty nasty for k > 4 or so.)
 *
 * A black-bordered subregion must have a size divisible by k.  So,
 * draw a graph with one node per dsf component and edges between
 * those dsf components which have adjacent squares.  Identify cut
 * vertices and edges.  If a cut-vertex-delimited component contains a
 * number of squares not divisible by k, cut vertex not included, then
 * the cut vertex must belong to the component.  If it has exactly one
 * edge _out_ of the component, the line(s) corresponding to that edge
 * are all LINE_YES (i.e. a BORDER()).
 * (This sounds complicated, but visually it is rather easy.)
 *
 * [Look at loopy and see how the at-least/-most k out of m edges
 * thing is done.  See how it is propagated across multiple squares.]
 */

#define EMPTY (~0)

#define BIT(i) (1 << (i))
#define BORDER(i) BIT(i)
#define BORDER_U BORDER(0)
#define BORDER_R BORDER(1)
#define BORDER_D BORDER(2)
#define BORDER_L BORDER(3)
#define FLIP(i) ((i) ^ 2)
#define BORDER_MASK (BORDER_U|BORDER_R|BORDER_D|BORDER_L)
#define DISABLED(border) ((border) << 4)
#define UNDISABLED(border) ((border) >> 4)

static const int dx[4] = { 0, +1,  0, -1};
static const int dy[4] = {-1,  0, +1,  0};
static const int bitcount[16] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
/* bitcount[x & BORDER_MASK] == number of enabled borders */

#define COMPUTE_J (-1)

static void connect(solver_ctx *ctx, int i, int j)
{
    dsf_merge(ctx->dsf, i, j);
}

static bool connected(solver_ctx *ctx, int i, int j, int dir)
{
    if (j == COMPUTE_J) j = i + dx[dir] + ctx->params->w*dy[dir];
    return dsf_canonify(ctx->dsf, i) == dsf_canonify(ctx->dsf, j);
}

static void disconnect(solver_ctx *ctx, int i, int j, int dir)
{
    if (j == COMPUTE_J) j = i + dx[dir] + ctx->params->w*dy[dir];
    ctx->borders[i] |= BORDER(dir);
    ctx->borders[j] |= BORDER(FLIP(dir));
}

static bool disconnected(solver_ctx *ctx, int i, int j, int dir)
{
    assert (j == COMPUTE_J || j == i + dx[dir] + ctx->params->w*dy[dir]);
    return ctx->borders[i] & BORDER(dir);
}

static bool maybe(solver_ctx *ctx, int i, int j, int dir)
{
    assert (j == COMPUTE_J || j == i + dx[dir] + ctx->params->w*dy[dir]);
    return !disconnected(ctx, i, j, dir) && !connected(ctx, i, j, dir);
    /* the ordering is important: disconnected works for invalid
     * squares (i.e. out of bounds), connected doesn't. */
}

static void solver_connected_clues_versus_region_size(solver_ctx *ctx)
{
    int w = ctx->params->w, h = ctx->params->h, wh = w*h, i, dir;

    /* If i is connected to j and i has borders with p of the
     * remaining three squares and j with q of the remaining three
     * squares, then the region has size at least 1+(3-p) + 1+(3-q).
     * If p = q = 3 then the region has size exactly 2. */

    for (i = 0; i < wh; ++i) {
        if (ctx->clues[i] == EMPTY) continue;
        for (dir = 0; dir < 4; ++dir) {
            int j = i + dx[dir] + w*dy[dir];
            if (disconnected(ctx, i, j, dir)) continue;
            if (ctx->clues[j] == EMPTY) continue;
            if ((8 - ctx->clues[i] - ctx->clues[j] > ctx->params->k) ||
                (ctx->clues[i] == 3 && ctx->clues[j] == 3 &&
                 ctx->params->k != 2))
            {
                disconnect(ctx, i, j, dir);
                /* changed = true, but this is a one-shot... */
            }
        }
    }
}

static bool solver_number_exhausted(solver_ctx *ctx)
{
    int w = ctx->params->w, h = ctx->params->h, wh = w*h, i, dir, off;
    bool changed = false;

    for (i = 0; i < wh; ++i) {
        if (ctx->clues[i] == EMPTY) continue;

        if (bitcount[(ctx->borders[i] & BORDER_MASK)] == ctx->clues[i]) {
            for (dir = 0; dir < 4; ++dir) {
                int j = i + dx[dir] + w*dy[dir];
                if (!maybe(ctx, i, j, dir)) continue;
                connect(ctx, i, j);
                changed = true;
            }
            continue;
        }

        for (off = dir = 0; dir < 4; ++dir) {
            int j = i + dx[dir] + w*dy[dir];
            if (!disconnected(ctx, i, j, dir) && connected(ctx, i, j, dir))
                ++off; /* ^^^ bounds checking before ^^^^^ */
        }

        if (ctx->clues[i] == 4 - off)
            for (dir = 0; dir < 4; ++dir) {
                int j = i + dx[dir] + w*dy[dir];
                if (!maybe(ctx, i, j, dir)) continue;
                disconnect(ctx, i, j, dir);
                changed = true;
            }
    }

    return changed;
}

static bool solver_not_too_big(solver_ctx *ctx)
{
    int w = ctx->params->w, h = ctx->params->h, wh = w*h, i, dir;
    bool changed = false;

    for (i = 0; i < wh; ++i) {
        int size = dsf_size(ctx->dsf, i);
        for (dir = 0; dir < 4; ++dir) {
            int j = i + dx[dir] + w*dy[dir];
            if (!maybe(ctx, i, j, dir)) continue;
            if (size + dsf_size(ctx->dsf, j) <= ctx->params->k) continue;
            disconnect(ctx, i, j, dir);
            changed = true;
        }
    }

    return changed;
}

static bool solver_not_too_small(solver_ctx *ctx)
{
    int w = ctx->params->w, h = ctx->params->h, wh = w*h, i, dir;
    int *outs, k = ctx->params->k, ci;
    bool changed = false;

    snewa(outs, wh);
    setmem(outs, -1, wh);

    for (i = 0; i < wh; ++i) {
        ci = dsf_canonify(ctx->dsf, i);
        if (dsf_size(ctx->dsf, ci) == k) continue;
        for (dir = 0; dir < 4; ++dir) {
            int j = i + dx[dir] + w*dy[dir];
            if (!maybe(ctx, i, j, dir)) continue;
            if (outs[ci] == -1) outs[ci] = dsf_canonify(ctx->dsf, j);
            else if (outs[ci] != dsf_canonify(ctx->dsf, j)) outs[ci] = -2;
        }
    }

    for (i = 0; i < wh; ++i) {
        int j = outs[i];
        if (i != dsf_canonify(ctx->dsf, i)) continue;
        if (j < 0) continue;
        connect(ctx, i, j); /* only one place for i to grow */
        changed = true;
    }

    sfree(outs);
    return changed;
}

static bool solver_no_dangling_edges(solver_ctx *ctx)
{
    int w = ctx->params->w, h = ctx->params->h, r, c;
    bool changed = false;

    /* for each vertex */
    for (r = 1; r < h; ++r)
        for (c = 1; c < w; ++c) {
            int i = r * w + c, j = i - w - 1, noline = 0, dir;
            int squares[4], e = -1, f = -1, de = -1, df = -1;

            /* feels hacky: I align these with BORDER_[U0 R1 D2 L3] */
            squares[1] = squares[2] = j;
            squares[0] = squares[3] = i;

            /* for each edge adjacent to the vertex */
            for (dir = 0; dir < 4; ++dir)
                if (!connected(ctx, squares[dir], COMPUTE_J, dir)) {
                    df = dir;
                    f = squares[df];
                    if (e != -1) continue;
                    e = f;
                    de = df;
                } else ++noline;

            if (4 - noline == 1) {
                assert (e != -1);
                disconnect(ctx, e, COMPUTE_J, de);
                changed = true;
                continue;
            }

            if (4 - noline != 2) continue;

            assert (e != -1);
            assert (f != -1);

            if (ctx->borders[e] & BORDER(de)) {
                if (!(ctx->borders[f] & BORDER(df))) {
                    disconnect(ctx, f, COMPUTE_J, df);
                    changed = true;
                }
            } else if (ctx->borders[f] & BORDER(df)) {
                disconnect(ctx, e, COMPUTE_J, de);
                changed = true;
            }
        }

    return changed;
}

static bool solver_equivalent_edges(solver_ctx *ctx)
{
    int w = ctx->params->w, h = ctx->params->h, wh = w*h, i, dirj;
    bool changed = false;

    /* if a square is adjacent to two connected squares, the two
     * borders (i,j) and (i,k) are either both on or both off. */

    for (i = 0; i < wh; ++i) {
        int n_on = 0, n_off = 0;
        if (ctx->clues[i] < 1 || ctx->clues[i] > 3) continue;

        if (ctx->clues[i] == 2 /* don't need it otherwise */)
            for (dirj = 0; dirj < 4; ++dirj) {
                int j = i + dx[dirj] + w*dy[dirj];
                if (disconnected(ctx, i, j, dirj)) ++n_on;
                else if (connected(ctx, i, j, dirj)) ++n_off;
            }

        for (dirj = 0; dirj < 4; ++dirj) {
            int j = i + dx[dirj] + w*dy[dirj], dirk;
            if (!maybe(ctx, i, j, dirj)) continue;

            for (dirk = dirj + 1; dirk < 4; ++dirk) {
                int k = i + dx[dirk] + w*dy[dirk];
                if (!maybe(ctx, i, k, dirk)) continue;
                if (!connected(ctx, j, k, -1)) continue;

                if (n_on + 2 > ctx->clues[i]) {
                    connect(ctx, i, j);
                    connect(ctx, i, k);
                    changed = true;
                } else if (n_off + 2 > 4 - ctx->clues[i]) {
                    disconnect(ctx, i, j, dirj);
                    disconnect(ctx, i, k, dirk);
                    changed = true;
                }
            }
        }
    }

    return changed;
}

#define UNVISITED 6

/* build connected components in `dsf', along the lines of `borders'. */
static void dfs_dsf(int i, int w, borderflag *border, int *dsf, bool black)
{
    int dir;
    for (dir = 0; dir < 4; ++dir) {
        int ii = i + dx[dir] + w*dy[dir], bdir = BORDER(dir);
        if (black ? (border[i] & bdir) : !(border[i] & DISABLED(bdir)))
            continue;
        if (dsf[ii] != UNVISITED) continue;
        dsf_merge(dsf, i, ii);
        dfs_dsf(ii, w, border, dsf, black);
    }
}

static bool is_solved(const game_params *params, clue *clues,
                      borderflag *border)
{
    int w = params->w, h = params->h, wh = w*h, k = params->k;
    int i, x, y;
    int *dsf = snew_dsf(wh);

    assert (dsf[0] == UNVISITED); /* check: UNVISITED and dsf.c match up */

    /*
     * A game is solved if:
     *
     *  - the borders drawn on the grid divide it into connected
     *    components such that every square is in a component of the
     *    correct size
     *  - the borders also satisfy the clue set
     */
    for (i = 0; i < wh; ++i) {
        if (dsf[i] == UNVISITED) dfs_dsf(i, params->w, border, dsf, true);
        if (dsf_size(dsf, i) != k) goto error;
        if (clues[i] == EMPTY) continue;
        if (clues[i] != bitcount[border[i] & BORDER_MASK]) goto error;
    }

    /*
     * ... and thirdly:
     *
     *  - there are no *stray* borders, in that every border is
     *    actually part of the division between two components.
     *    Otherwise you could cheat by finding a subdivision which did
     *    not *exceed* any clue square's counter, and then adding a
     *    few extra edges.
     */
    for (y = 0; y < h; y++) {
        for (x = 0; x < w; x++) {
            if (x+1 < w && (border[y*w+x] & BORDER_R) &&
                dsf_canonify(dsf, y*w+x) == dsf_canonify(dsf, y*w+(x+1)))
                goto error;
            if (y+1 < h && (border[y*w+x] & BORDER_D) &&
                dsf_canonify(dsf, y*w+x) == dsf_canonify(dsf, (y+1)*w+x))
                goto error;
        }
    }

    sfree(dsf);
    return true;

error:
    sfree(dsf);
    return false;
}

static bool solver(const game_params *params, clue *clues, borderflag *borders)
{
    int w = params->w, h = params->h, wh = w*h;
    bool changed;
    solver_ctx ctx;

    ctx.params = params;
    ctx.clues = clues;
    ctx.borders = borders;
    ctx.dsf = snew_dsf(wh);

    solver_connected_clues_versus_region_size(&ctx); /* idempotent */
    do {
        changed  = false;
        changed |= solver_number_exhausted(&ctx);
        changed |= solver_not_too_big(&ctx);
        changed |= solver_not_too_small(&ctx);
        changed |= solver_no_dangling_edges(&ctx);
        changed |= solver_equivalent_edges(&ctx);
    } while (changed);

    sfree(ctx.dsf);

    return is_solved(params, clues, borders);
}

/* --- Generator ---------------------------------------------------- */

static void init_borders(int w, int h, borderflag *borders)
{
    int r, c;
    setmem(borders, 0, w*h);
    for (c = 0; c < w; ++c) {
        borders[c] |= BORDER_U;
        borders[w*h-1 - c] |= BORDER_D;
    }
    for (r = 0; r < h; ++r) {
        borders[r*w] |= BORDER_L;
        borders[w*h-1 - r*w] |= BORDER_R;
    }
}

#define OUT_OF_BOUNDS(x, y, w, h) \
    ((x) < 0 || (x) >= (w) || (y) < 0 || (y) >= (h))

#define xshuffle(ptr, len, rs) shuffle((ptr), (len), sizeof (ptr)[0], (rs))

static char *new_game_desc(const game_params *params, random_state *rs,
                           char **aux, bool interactive)
{
    int w = params->w, h = params->h, wh = w*h, k = params->k;

    clue *numbers = snewn(wh + 1, clue), *p;
    borderflag *rim = snewn(wh, borderflag);
    borderflag *scratch_borders = snewn(wh, borderflag);

    char *soln = snewa(*aux, wh + 2);
    int *shuf = snewn(wh, int);
    int *dsf = NULL, i, r, c;

    int attempts = 0;

    for (i = 0; i < wh; ++i) shuf[i] = i;
    xshuffle(shuf, wh, rs);

    init_borders(w, h, rim);

    assert (!('@' & BORDER_MASK));
    *soln++ = 'S';
    soln[wh] = '\0';

    do {
        ++attempts;
        setmem(soln, '@', wh);

        sfree(dsf);
        dsf = divvy_rectangle(w, h, k, rs);

        for (r = 0; r < h; ++r)
            for (c = 0; c < w; ++c) {
                int i = r * w + c, dir;
                numbers[i] = 0;
                for (dir = 0; dir < 4; ++dir) {
                    int rr = r + dy[dir], cc = c + dx[dir], ii = rr * w + cc;
                    if (OUT_OF_BOUNDS(cc, rr, w, h) ||
                        dsf_canonify(dsf, i) != dsf_canonify(dsf, ii)) {
                        ++numbers[i];
                        soln[i] |= BORDER(dir);
                    }
                }
            }

        scopy(scratch_borders, rim, wh);
    } while (!solver(params, numbers, scratch_borders));

    for (i = 0; i < wh; ++i) {
        int j = shuf[i];
        clue copy = numbers[j];

        scopy(scratch_borders, rim, wh);
        numbers[j] = EMPTY; /* strip away unnecssary clues */
        if (!solver(params, numbers, scratch_borders))
            numbers[j] = copy;
    }

    numbers[wh] = '\0';

    sfree(scratch_borders);
    sfree(rim);
    sfree(shuf);
    sfree(dsf);

    p = numbers;
    r = 0;
    for (i = 0; i < wh; ++i) {
        if (numbers[i] != EMPTY) {
            while (r) {
                while (r > 26) {
                    *p++ = 'z';
                    r -= 26;
                }
                *p++ = 'a'-1 + r;
                r = 0;
            }
            *p++ = '0' + numbers[i];
        } else ++r;
    }
    *p++ = '\0';

    return sresize(numbers, p - numbers, clue);
}

static const char *validate_desc(const game_params *params, const char *desc)
{

    int w = params->w, h = params->h, wh = w*h, squares = 0;

    for (/* nop */; *desc; ++desc) {
        if (islower((unsigned char)*desc)) {
            squares += *desc - 'a' + 1;
        } else if (isdigit((unsigned char)*desc)) {
            if (*desc > '4') {
                static char buf[] = "Invalid (too large) number: '5'";
                assert (isdigit((unsigned char)buf[lenof(buf) - 3]));
                buf[lenof(buf) - 3] = *desc; /* ... or 6, 7, 8, 9 :-) */
                return buf;
            }
            ++squares;
        } else if (isprint((unsigned char)*desc)) {
            static char buf[] = "Invalid character in data: '?'";
            buf[lenof(buf) - 3] = *desc;
            return buf;
        } else return "Invalid (unprintable) character in data";
    }

    if (squares > wh) return "Data describes too many squares";

    return NULL;
}

static game_state *new_game(midend *me, const game_params *params,
                            const char *desc)
{
    int w = params->w, h = params->h, wh = w*h, i;
    game_state *state = snew(game_state);

    state->shared = snew(shared_state);
    state->shared->refcount = 1;
    state->shared->params = *params; /* struct copy */
    snewa(state->shared->clues, wh);

    setmem(state->shared->clues, EMPTY, wh);
    for (i = 0; *desc; ++desc) {
        if (isdigit((unsigned char)*desc)) state->shared->clues[i++] = *desc - '0';
        else if (isalpha((unsigned char)*desc)) i += *desc - 'a' + 1;
    }

    snewa(state->borders, wh);
    init_borders(w, h, state->borders);

    state->completed = (params->k == wh);
    state->cheated = false;

    return state;
}

static game_state *dup_game(const game_state *state)
{
    int wh = state->shared->params.w * state->shared->params.h;
    game_state *ret = snew(game_state);

    ret->borders = dupmem(state->borders, wh);

    ret->shared = state->shared;
    ++ret->shared->refcount;

    ret->completed = state->completed;
    ret->cheated = state->cheated;

    return ret;
}

static void free_game(game_state *state)
{
    if (--state->shared->refcount == 0) {
        sfree(state->shared->clues);
        sfree(state->shared);
    }
    sfree(state->borders);
    sfree(state);
}

static char *solve_game(const game_state *state, const game_state *currstate,
                        const char *aux, const char **error)
{
    int w = state->shared->params.w, h = state->shared->params.h, wh = w*h;
    borderflag *move;

    if (aux) return dupstr(aux);

    snewa(move, wh + 2);

    move[0] = 'S';
    init_borders(w, h, move + 1);
    move[wh + 1] = '\0';

    if (solver(&state->shared->params, state->shared->clues, move + 1)) {
        int i;
        for (i = 0; i < wh; i++)
            move[i+1] |= '@';          /* turn into sensible ASCII */
        return (char *) move;
    }

    *error = "Sorry, I can't solve this puzzle";
    sfree(move);
    return NULL;

    {
        /* compile-time-assert (borderflag is-a-kind-of char).
         *
         * depends on zero-size arrays being disallowed.  GCC says
         * ISO C forbids this, pointing to [-Werror=edantic].  Also,
         * it depends on type-checking of (obviously) dead code. */
        borderflag b[sizeof (borderflag) == sizeof (char)];
        char c = b[0]; b[0] = c;
        /* we could at least in principle put this anywhere, but it
         * seems silly to not put it where the assumption is used. */
    }
}

static bool game_can_format_as_text_now(const game_params *params)
{
    return true;
}

static char *game_text_format(const game_state *state)
{
    int w = state->shared->params.w, h = state->shared->params.h, r, c;
    int cw = 4, ch = 2, gw = cw*w + 2, gh = ch * h + 1, len = gw * gh;
    char *board;

    setmem(snewa(board, len + 1), ' ', len);
    for (r = 0; r < h; ++r) {
        for (c = 0; c < w; ++c) {
            int cell = r*ch*gw + cw*c, center = cell + gw*ch/2 + cw/2;
            int i = r * w + c, clue = state->shared->clues[i];

            if (clue != EMPTY) board[center] = '0' + clue;

            board[cell] = '+';

            if (state->borders[i] & BORDER_U)
                setmem(board + cell + 1, '-', cw - 1);
            else if (state->borders[i] & DISABLED(BORDER_U))
                board[cell + cw / 2] = 'x';

            if (state->borders[i] & BORDER_L)
                board[cell + gw] = '|';
            else if (state->borders[i] & DISABLED(BORDER_L))
                board[cell + gw] = 'x';
        }

        for (c = 0; c < ch; ++c) {
            board[(r*ch + c)*gw + gw - 2] = c ? '|' : '+';
            board[(r*ch + c)*gw + gw - 1] = '\n';
        }
    }

    scopy(board + len - gw, board, gw);
    board[len] = '\0';

    return board;
}

struct game_ui {
    int x, y;
    bool show;
};

static game_ui *new_ui(const game_state *state)
{
    game_ui *ui = snew(game_ui);
    ui->x = ui->y = 0;
    ui->show = false;
    return ui;
}

static void free_ui(game_ui *ui)
{
    sfree(ui);
}

static char *encode_ui(const game_ui *ui)
{
    return NULL;
}

static void decode_ui(game_ui *ui, const char *encoding)
{
    assert (encoding == NULL);
}

static void game_changed_state(game_ui *ui, const game_state *oldstate,
                               const game_state *newstate)
{
}

typedef unsigned short dsflags;

struct game_drawstate {
    int tilesize;
    dsflags *grid;
};

#define TILESIZE (ds->tilesize)
#define MARGIN (ds->tilesize / 2)
#define WIDTH (1 + (TILESIZE >= 16) + (TILESIZE >= 32) + (TILESIZE >= 64))
#define CENTER ((ds->tilesize / 2) + WIDTH/2)

#define FROMCOORD(x) (((x) - MARGIN) / TILESIZE)

enum {MAYBE_LEFT, MAYBE_RIGHT, ON_LEFT, ON_RIGHT, OFF_LEFT, OFF_RIGHT};

static char *interpret_move(const game_state *state, game_ui *ui,
                            const game_drawstate *ds, int x, int y, int button)
{
    int w = state->shared->params.w, h = state->shared->params.h;
    bool control = button & MOD_CTRL, shift = button & MOD_SHFT;

    button &= ~MOD_MASK;

    if (button == LEFT_BUTTON || button == RIGHT_BUTTON) {
        int gx = FROMCOORD(x), gy = FROMCOORD(y), possible = BORDER_MASK;
        int px = (x - MARGIN) % TILESIZE, py = (y - MARGIN) % TILESIZE;
        int hx, hy, dir, i;

        if (OUT_OF_BOUNDS(gx, gy, w, h)) return NULL;

        ui->x = gx;
        ui->y = gy;

        /* find edge closest to click point */
        possible &=~ (2*px < TILESIZE ? BORDER_R : BORDER_L);
        possible &=~ (2*py < TILESIZE ? BORDER_D : BORDER_U);
        px = min(px, TILESIZE - px);
        py = min(py, TILESIZE - py);
        possible &=~ (px < py ? (BORDER_U|BORDER_D) : (BORDER_L|BORDER_R));

        for (dir = 0; dir < 4 && BORDER(dir) != possible; ++dir);
        if (dir == 4) return NULL; /* there's not exactly one such edge */

        hx = gx + dx[dir];
        hy = gy + dy[dir];

        if (OUT_OF_BOUNDS(hx, hy, w, h)) return NULL;

        ui->show = false;

        i = gy * w + gx;
        switch ((button == RIGHT_BUTTON) |
                ((state->borders[i] & BORDER(dir)) >> dir << 1) |
                ((state->borders[i] & DISABLED(BORDER(dir))) >> dir >> 2)) {

        case MAYBE_LEFT:
        case ON_LEFT:
        case ON_RIGHT:
            return string(80, "F%d,%d,%dF%d,%d,%d",
                          gx, gy, BORDER(dir),
                          hx, hy, BORDER(FLIP(dir)));

        case MAYBE_RIGHT:
        case OFF_LEFT:
        case OFF_RIGHT:
            return string(80, "F%d,%d,%dF%d,%d,%d",
                          gx, gy, DISABLED(BORDER(dir)),
                          hx, hy, DISABLED(BORDER(FLIP(dir))));
        }
    }

    if (IS_CURSOR_MOVE(button)) {
        ui->show = true;
        if (control || shift) {
            borderflag flag = 0, newflag;
            int dir, i =  ui->y * w + ui->x;
            x = ui->x;
            y = ui->y;
            move_cursor(button, &x, &y, w, h, false);
            if (OUT_OF_BOUNDS(x, y, w, h)) return NULL;

            for (dir = 0; dir < 4; ++dir)
                if (dx[dir] == x - ui->x && dy[dir] == y - ui->y) break;
            if (dir == 4) return NULL; /* how the ... ?! */

            if (control) flag |= BORDER(dir);
            if (shift) flag |= DISABLED(BORDER(dir));

            newflag = state->borders[i] ^ flag;
            if (newflag & BORDER(dir) && newflag & DISABLED(BORDER(dir)))
                return NULL;

            newflag = 0;
            if (control) newflag |= BORDER(FLIP(dir));
            if (shift) newflag |= DISABLED(BORDER(FLIP(dir)));
            return string(80, "F%d,%d,%dF%d,%d,%d",
                          ui->x, ui->y, flag, x, y, newflag);
        } else {
            move_cursor(button, &ui->x, &ui->y, w, h, false);
            return UI_UPDATE;
        }
    }

    return NULL;
}

static game_state *execute_move(const game_state *state, const char *move)
{
    int w = state->shared->params.w, h = state->shared->params.h, wh = w * h;
    game_state *ret = dup_game(state);
    int nchars, x, y, flag;

    if (*move == 'S') {
        int i;
        ++move;
        for (i = 0; i < wh && move[i]; ++i)
            ret->borders[i] =
                (move[i] & BORDER_MASK) | DISABLED(~move[i] & BORDER_MASK);
        if (i < wh || move[i]) return NULL; /* leaks `ret', then we die */
        ret->cheated = ret->completed = true;
        return ret;
    }

    while (sscanf(move, "F%d,%d,%d%n", &x, &y, &flag, &nchars) == 3 &&
           !OUT_OF_BOUNDS(x, y, w, h)) {
        move += nchars;
        ret->borders[y*w + x] ^= flag;
    }

    if (*move) return NULL; /* leaks `ret', then we die */

    if (!ret->completed)
        ret->completed = is_solved(&ret->shared->params, ret->shared->clues,
                                   ret->borders);

    return ret;
}

/* --- Drawing routines --------------------------------------------- */

static void game_compute_size(const game_params *params, int tilesize,
                              int *x, int *y)
{
    *x = (params->w + 1) * tilesize;
    *y = (params->h + 1) * tilesize;
}

static void game_set_size(drawing *dr, game_drawstate *ds,
                          const game_params *params, int tilesize)
{
    ds->tilesize = tilesize;
}

enum {
    COL_BACKGROUND,
    COL_FLASH,
    COL_GRID,
    COL_CLUE = COL_GRID,
    COL_LINE_YES = COL_GRID,
    COL_LINE_MAYBE,
    COL_LINE_NO,
    COL_ERROR,

    NCOLOURS
};

#define COLOUR(i, r, g, b) \
   ((ret[3*(i)+0] = (r)), (ret[3*(i)+1] = (g)), (ret[3*(i)+2] = (b)))
#define DARKER 0.9F

static float *game_colours(frontend *fe, int *ncolours)
{
    float *ret = snewn(3 * NCOLOURS, float);

    game_mkhighlight(fe, ret, COL_BACKGROUND, -1, COL_FLASH);

    COLOUR(COL_GRID,   0.0F, 0.0F, 0.0F); /* black */
    COLOUR(COL_ERROR,  1.0F, 0.0F, 0.0F); /* red */

    COLOUR(COL_LINE_MAYBE, /* yellow */
           ret[COL_BACKGROUND*3 + 0] * DARKER,
           ret[COL_BACKGROUND*3 + 1] * DARKER,
           0.0F);

    COLOUR(COL_LINE_NO,
           ret[COL_BACKGROUND*3 + 0] * DARKER,
           ret[COL_BACKGROUND*3 + 1] * DARKER,
           ret[COL_BACKGROUND*3 + 2] * DARKER);

    *ncolours = NCOLOURS;
    return ret;
}
#undef COLOUR

#define BORDER_ERROR(x) ((x) << 8)
#define F_ERROR_U BORDER_ERROR(BORDER_U) /* BIT( 8) */
#define F_ERROR_R BORDER_ERROR(BORDER_R) /* BIT( 9) */
#define F_ERROR_D BORDER_ERROR(BORDER_D) /* BIT(10) */
#define F_ERROR_L BORDER_ERROR(BORDER_L) /* BIT(11) */
#define F_ERROR_CLUE BIT(12)
#define F_FLASH BIT(13)
#define F_CURSOR BIT(14)

static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
{
    struct game_drawstate *ds = snew(struct game_drawstate);

    ds->tilesize = 0;
    ds->grid = NULL;

    return ds;
}

static void game_free_drawstate(drawing *dr, game_drawstate *ds)
{
    sfree(ds->grid);
    sfree(ds);
}

#define COLOUR(border)                                                  \
    (flags & BORDER_ERROR((border)) ? COL_ERROR :                       \
     flags & (border)               ? COL_LINE_YES :                    \
     flags & DISABLED((border))     ? COL_LINE_NO :                     \
                                      COL_LINE_MAYBE)

static void draw_tile(drawing *dr, game_drawstate *ds, int r, int c,
                      dsflags flags, int clue)
{
    int x = MARGIN + TILESIZE * c, y = MARGIN + TILESIZE * r;

    clip(dr, x, y, TILESIZE + WIDTH, TILESIZE + WIDTH); /* { */

    draw_rect(dr, x + WIDTH, y + WIDTH, TILESIZE - WIDTH, TILESIZE - WIDTH,
              (flags & F_FLASH ? COL_FLASH : COL_BACKGROUND));

    if (flags & F_CURSOR)
        draw_rect_corners(dr, x + CENTER, y + CENTER, TILESIZE / 3, COL_GRID);

    if (clue != EMPTY) {
        char buf[2];
        buf[0] = '0' + clue;
        buf[1] = '\0';
        draw_text(dr, x + CENTER, y + CENTER, FONT_VARIABLE,
                  TILESIZE / 2, ALIGN_VCENTRE | ALIGN_HCENTRE,
                  (flags & F_ERROR_CLUE ? COL_ERROR : COL_CLUE), buf);
    }


#define ts TILESIZE
#define w WIDTH
    draw_rect(dr, x + w,  y,      ts - w, w,      COLOUR(BORDER_U));
    draw_rect(dr, x + ts, y + w,  w,      ts - w, COLOUR(BORDER_R));
    draw_rect(dr, x + w,  y + ts, ts - w, w,      COLOUR(BORDER_D));
    draw_rect(dr, x,      y + w,  w,      ts - w, COLOUR(BORDER_L));
#undef ts
#undef w

    unclip(dr); /* } */
    draw_update(dr, x, y, TILESIZE + WIDTH, TILESIZE + WIDTH);
}

#define FLASH_TIME 0.7F

static void game_redraw(drawing *dr, game_drawstate *ds,
                        const game_state *oldstate, const game_state *state,
                        int dir, const game_ui *ui,
                        float animtime, float flashtime)
{
    int w = state->shared->params.w, h = state->shared->params.h, wh = w*h;
    int r, c, i, flash = ((int) (flashtime * 5 / FLASH_TIME)) % 2;
    int *black_border_dsf = snew_dsf(wh), *yellow_border_dsf = snew_dsf(wh);
    int k = state->shared->params.k;

    if (!ds->grid) {
        char buf[40];
        int bgw = (w+1) * ds->tilesize, bgh = (h+1) * ds->tilesize;
        draw_rect(dr, 0, 0, bgw, bgh, COL_BACKGROUND);

        for (r = 0; r <= h; ++r)
            for (c = 0; c <= w; ++c)
                draw_rect(dr, MARGIN + TILESIZE * c, MARGIN + TILESIZE * r,
                          WIDTH, WIDTH, COL_GRID);
        draw_update(dr, 0, 0, bgw, bgh);

        snewa(ds->grid, wh);
        setmem(ds->grid, ~0, wh);

        sprintf(buf, "Region size: %d", state->shared->params.k);
        status_bar(dr, buf);
    }

    for (i = 0; i < wh; ++i) {
        if (black_border_dsf[i] == UNVISITED)
            dfs_dsf(i, w, state->borders, black_border_dsf, true);
        if (yellow_border_dsf[i] == UNVISITED)
            dfs_dsf(i, w, state->borders, yellow_border_dsf, false);
    }

    for (r = 0; r < h; ++r)
        for (c = 0; c < w; ++c) {
            int i = r * w + c, clue = state->shared->clues[i], flags, dir;
            int on = bitcount[state->borders[i] & BORDER_MASK];
            int off = bitcount[(state->borders[i] >> 4) & BORDER_MASK];

            flags = state->borders[i];

            if (flash) flags |= F_FLASH;

            if (clue != EMPTY && (on > clue || clue > 4 - off))
                flags |= F_ERROR_CLUE;

            if (ui->show && ui->x == c && ui->y == r)
                flags |= F_CURSOR;

            /* border errors */
            for (dir = 0; dir < 4; ++dir) {
                int rr = r + dy[dir], cc = c + dx[dir], ii = rr * w + cc;

                if (OUT_OF_BOUNDS(cc, rr, w, h)) continue;

                /* we draw each border twice, except the outermost
                 * big border, so we have to check for errors on
                 * both sides of each border.*/
                if (/* region too large */
                    ((dsf_size(yellow_border_dsf, i) > k ||
                      dsf_size(yellow_border_dsf, ii) > k) &&
                     (dsf_canonify(yellow_border_dsf, i) !=
                      dsf_canonify(yellow_border_dsf, ii)))

                    ||
                    /* region too small */
                    ((dsf_size(black_border_dsf, i) < k ||
                      dsf_size(black_border_dsf, ii) < k) &&
                     dsf_canonify(black_border_dsf, i) !=
                     dsf_canonify(black_border_dsf, ii))

                    ||
                    /* dangling borders within a single region */
                    ((state->borders[i] & BORDER(dir)) &&
                     /* we know it's a single region because there's a
                      * path crossing no border from i to ii... */
                     (dsf_canonify(yellow_border_dsf, i) ==
                      dsf_canonify(yellow_border_dsf, ii) ||
                      /* or because any such border would be an error */
                      (dsf_size(black_border_dsf, i) <= k &&
                       dsf_canonify(black_border_dsf, i) ==
                       dsf_canonify(black_border_dsf, ii)))))

                    flags |= BORDER_ERROR(BORDER(dir));
            }

            if (flags == ds->grid[i]) continue;
            ds->grid[i] = flags;
            draw_tile(dr, ds, r, c, ds->grid[i], clue);
        }

    sfree(black_border_dsf);
    sfree(yellow_border_dsf);
}

static float game_anim_length(const game_state *oldstate,
                              const game_state *newstate,
                              int dir, game_ui *ui)
{
    return 0.0F;
}

static float game_flash_length(const game_state *oldstate,
                               const game_state *newstate,
                               int dir, game_ui *ui)
{
    if (newstate->completed && !newstate->cheated && !oldstate->completed)
        return FLASH_TIME;
    return 0.0F;
}

static int game_status(const game_state *state)
{
    return state->completed ? +1 : 0;
}

static bool game_timing_state(const game_state *state, game_ui *ui)
{
    assert (!"this shouldn't get called");
    return false;                      /* placate optimiser */
}

static void game_print_size(const game_params *params, float *x, float *y)
{
    int pw, ph;

    game_compute_size(params, 700, &pw, &ph); /* 7mm, like loopy */

    *x = pw / 100.0F;
    *y = ph / 100.0F;
}

static void print_line(drawing *dr, int x1, int y1, int x2, int y2,
                       int colour, bool full)
{
    if (!full) {
        int i, subdivisions = 8;
        for (i = 1; i < subdivisions; ++i) {
            int x = (x1 * (subdivisions - i) + x2 * i) / subdivisions;
            int y = (y1 * (subdivisions - i) + y2 * i) / subdivisions;
            draw_circle(dr, x, y, 3, colour, colour);
        }
    } else draw_line(dr, x1, y1, x2, y2, colour);
}

static void game_print(drawing *dr, const game_state *state, int tilesize)
{
    int w = state->shared->params.w, h = state->shared->params.h;
    int ink = print_mono_colour(dr, 0);
    game_drawstate for_tilesize_macros, *ds = &for_tilesize_macros;
    int r, c;

    ds->tilesize = tilesize;

    for (r = 0; r < h; ++r)
        for (c = 0; c < w; ++c) {
            int x = MARGIN + TILESIZE * c, y = MARGIN + TILESIZE * r;
            int i = r * w + c, clue = state->shared->clues[i];

            if (clue != EMPTY) {
                char buf[2];
                buf[0] = '0' + clue;
                buf[1] = '\0';
                draw_text(dr, x + CENTER, y + CENTER, FONT_VARIABLE,
                          TILESIZE / 2, ALIGN_VCENTRE | ALIGN_HCENTRE,
                          ink, buf);
            }

#define ts TILESIZE
#define FULL(DIR) (state->borders[i] & (BORDER_ ## DIR))
            print_line(dr, x,      y,      x + ts, y,      ink, FULL(U));
            print_line(dr, x + ts, y,      x + ts, y + ts, ink, FULL(R));
            print_line(dr, x,      y + ts, x + ts, y + ts, ink, FULL(D));
            print_line(dr, x,      y,      x,      y + ts, ink, FULL(L));
#undef ts
#undef FULL
        }

    for (r = 1; r < h; ++r)
        for (c = 1; c < w; ++c) {
            int j = r * w + c, i = j - 1 - w;
            int x = MARGIN + TILESIZE * c, y = MARGIN + TILESIZE * r;
            if (state->borders[i] & (BORDER_D|BORDER_R)) continue;
            if (state->borders[j] & (BORDER_U|BORDER_L)) continue;
            draw_circle(dr, x, y, 3, ink, ink);
        }
}

#ifdef COMBINED
#define thegame palisade
#endif

const struct game thegame = {
    "Palisade", "games.palisade", "palisade",
    default_params,
    game_fetch_preset, NULL,
    decode_params,
    encode_params,
    free_params,
    dup_params,
    true, game_configure, custom_params,
    validate_params,
    new_game_desc,
    validate_desc,
    new_game,
    dup_game,
    free_game,
    true, solve_game,
    true, game_can_format_as_text_now, game_text_format,
    new_ui,
    free_ui,
    encode_ui,
    decode_ui,
    NULL, /* game_request_keys */
    game_changed_state,
    interpret_move,
    execute_move,
    48, game_compute_size, game_set_size,
    game_colours,
    game_new_drawstate,
    game_free_drawstate,
    game_redraw,
    game_anim_length,
    game_flash_length,
    game_status,
    true, false, game_print_size, game_print,
    true,                                     /* wants_statusbar */
    false, game_timing_state,
    0,                                         /* flags */
};