chiark / gitweb /
midend_deserialise: accept an extra validation function.
[sgt-puzzles.git] / net.c
diff --git a/net.c b/net.c
index 3e49b38651e7213c2815966ef7a33c908d9e1717..f4a1e39b451981844c66ab365e83aff90cdb4fb3 100644 (file)
--- a/net.c
+++ b/net.c
 #include "puzzles.h"
 #include "tree234.h"
 
-#define MATMUL(xr,yr,m,x,y) do { \
-    float rx, ry, xx = (x), yy = (y), *mat = (m); \
-    rx = mat[0] * xx + mat[2] * yy; \
-    ry = mat[1] * xx + mat[3] * yy; \
-    (xr) = rx; (yr) = ry; \
-} while (0)
+/*
+ * The standard user interface for Net simply has left- and
+ * right-button mouse clicks in a square rotate it one way or the
+ * other. We also provide, by #ifdef, a separate interface based on
+ * rotational dragging motions. I initially developed this for the
+ * Mac on the basis that it might work better than the click
+ * interface with only one mouse button available, but in fact
+ * found it to be quite strange and unintuitive. Apparently it
+ * works better on stylus-driven platforms such as Palm and
+ * PocketPC, though, so we enable it by default there.
+ */
+#ifdef STYLUS_BASED
+#define USE_DRAGGING
+#endif
 
 /* Direction and other bitfields */
 #define R 0x01
 #define D 0x08
 #define LOCKED 0x10
 #define ACTIVE 0x20
+#define RLOOP (R << 6)
+#define ULOOP (U << 6)
+#define LLOOP (L << 6)
+#define DLOOP (D << 6)
+#define LOOP(dir) ((dir) << 6)
 
 /* Rotations: Anticlockwise, Clockwise, Flip, general rotate */
 #define A(x) ( (((x) & 0x07) << 1) | (((x) & 0x08) >> 3) )
 
 #define PREFERRED_TILE_SIZE 32
 #define TILE_SIZE (ds->tilesize)
-#define TILE_BORDER 1
+#define LINE_THICK ((TILE_SIZE+47)/48)
+#ifdef SMALL_SCREEN
+#define WINDOW_OFFSET 4
+#else
 #define WINDOW_OFFSET 16
+#endif
 
 #define ROTATE_TIME 0.13F
 #define FLASH_FRAME 0.07F
 
-/* Transform physical coords to game coords using game_drawstate ds */
-#define GX(x) (((x) + ds->org_x) % ds->width)
-#define GY(y) (((y) + ds->org_y) % ds->height)
-/* ...and game coords to physical coords */
-#define RX(x) (((x) + ds->width - ds->org_x) % ds->width)
-#define RY(y) (((y) + ds->height - ds->org_y) % ds->height)
-
 enum {
     COL_BACKGROUND,
     COL_LOCKED,
@@ -66,6 +76,7 @@ enum {
     COL_ENDPOINT,
     COL_POWERED,
     COL_BARRIER,
+    COL_LOOP,
     NCOLOURS
 };
 
@@ -77,12 +88,17 @@ struct game_params {
     float barrier_probability;
 };
 
+typedef struct game_immutable_state {
+    int refcount;
+    unsigned char *barriers;
+} game_immutable_state;
+
 struct game_state {
     int width, height, wrapping, completed;
     int last_rotate_x, last_rotate_y, last_rotate_dir;
-    int used_solve, just_used_solve;
+    int used_solve;
     unsigned char *tiles;
-    unsigned char *barriers;
+    struct game_immutable_state *imm;
 };
 
 #define OFFSETWH(x2,y2,x1,y1,dir,width,height) \
@@ -94,7 +110,7 @@ struct game_state {
 
 #define index(state, a, x, y) ( a[(y) * (state)->width + (x)] )
 #define tile(state, x, y)     index(state, (state)->tiles, x, y)
-#define barrier(state, x, y)  index(state, (state)->barriers, x, y)
+#define barrier(state, x, y)  index(state, (state)->imm->barriers, x, y)
 
 struct xyd {
     int x, y, direction;
@@ -150,12 +166,16 @@ static const struct game_params net_presets[] = {
     {7, 7, FALSE, TRUE, 0.0},
     {9, 9, FALSE, TRUE, 0.0},
     {11, 11, FALSE, TRUE, 0.0},
+#ifndef SMALL_SCREEN
     {13, 11, FALSE, TRUE, 0.0},
+#endif
     {5, 5, TRUE, TRUE, 0.0},
     {7, 7, TRUE, TRUE, 0.0},
     {9, 9, TRUE, TRUE, 0.0},
     {11, 11, TRUE, TRUE, 0.0},
+#ifndef SMALL_SCREEN
     {13, 11, TRUE, TRUE, 0.0},
+#endif
 };
 
 static int game_fetch_preset(int i, char **name, game_params **params)
@@ -182,7 +202,7 @@ static void free_params(game_params *params)
     sfree(params);
 }
 
-static game_params *dup_params(game_params *params)
+static game_params *dup_params(const game_params *params)
 {
     game_params *ret = snew(game_params);
     *ret = *params;                   /* structure copy */
@@ -209,7 +229,7 @@ static void decode_params(game_params *ret, char const *string)
            ret->wrapping = TRUE;
        } else if (*p == 'b') {
            p++;
-            ret->barrier_probability = atof(p);
+            ret->barrier_probability = (float)atof(p);
            while (*p && (*p == '.' || isdigit((unsigned char)*p))) p++;
        } else if (*p == 'a') {
             p++;
@@ -219,7 +239,7 @@ static void decode_params(game_params *ret, char const *string)
     }
 }
 
-static char *encode_params(game_params *params, int full)
+static char *encode_params(const game_params *params, int full)
 {
     char ret[400];
     int len;
@@ -237,7 +257,7 @@ static char *encode_params(game_params *params, int full)
     return dupstr(ret);
 }
 
-static config_item *game_configure(game_params *params)
+static config_item *game_configure(const game_params *params)
 {
     config_item *ret;
     char buf[80];
@@ -280,7 +300,7 @@ static config_item *game_configure(game_params *params)
     return ret;
 }
 
-static game_params *custom_params(config_item *cfg)
+static game_params *custom_params(const config_item *cfg)
 {
     game_params *ret = snew(game_params);
 
@@ -293,7 +313,7 @@ static game_params *custom_params(config_item *cfg)
     return ret;
 }
 
-static char *validate_params(game_params *params, int full)
+static char *validate_params(const game_params *params, int full)
 {
     if (params->width <= 0 || params->height <= 0)
        return "Width and height must both be greater than zero";
@@ -430,6 +450,11 @@ static int todo_get(struct todo *todo) {
     return ret;
 }
 
+/*
+ * Return values: -1 means puzzle was proved inconsistent, 0 means we
+ * failed to narrow down to a unique solution, +1 means we solved it
+ * fully.
+ */
 static int net_solver(int w, int h, unsigned char *tiles,
                      unsigned char *barriers, int wrapping)
 {
@@ -521,9 +546,7 @@ static int net_solver(int w, int h, unsigned char *tiles,
      * classes) by finding the representative of each tile and
      * setting equivalence[one]=the_other.
      */
-    equivalence = snewn(w * h, int);
-    for (i = 0; i < w*h; i++)
-       equivalence[i] = i;            /* initially all distinct */
+    equivalence = snew_dsf(w * h);
 
     /*
      * On a non-wrapping grid, we instantly know that all the edges
@@ -706,7 +729,11 @@ static int net_solver(int w, int h, unsigned char *tiles,
 #endif
            }
 
-           assert(j > 0);             /* we can't lose _all_ possibilities! */
+           if (j == 0) {
+                /* If we've ruled out all possible orientations for a
+                 * tile, then our puzzle has no solution at all. */
+                return -1;
+            }
 
            if (j < i) {
                done_something = TRUE;
@@ -786,14 +813,14 @@ static int net_solver(int w, int h, unsigned char *tiles,
     /*
      * Mark all completely determined tiles as locked.
      */
-    j = TRUE;
+    j = +1;
     for (i = 0; i < w*h; i++) {
        if (tilestate[i * 4 + 1] == 255) {
            assert(tilestate[i * 4 + 0] != 255);
            tiles[i] = tilestate[i * 4] | LOCKED;
        } else {
            tiles[i] &= ~LOCKED;
-           j = FALSE;
+           j = 0;
        }
     }
 
@@ -929,8 +956,10 @@ static void perturb(int w, int h, unsigned char *tiles, int wrapping,
     }
     sfree(perim2);
 
-    if (i == nperim)
+    if (i == nperim) {
+        sfree(perimeter);
        return;                        /* nothing we can do! */
+    }
 
     /*
      * Now we've constructed a new link, we need to find the entire
@@ -1103,7 +1132,11 @@ static void perturb(int w, int h, unsigned char *tiles, int wrapping,
     sfree(perimeter);
 }
 
-static char *new_game_desc(game_params *params, random_state *rs,
+static int *compute_loops_inner(int w, int h, int wrapping,
+                                const unsigned char *tiles,
+                                const unsigned char *barriers);
+
+static char *new_game_desc(const game_params *params, random_state *rs,
                           char **aux, int interactive)
 {
     tree234 *possibilities, *barriertree;
@@ -1301,7 +1334,7 @@ static char *new_game_desc(game_params *params, random_state *rs,
        /*
         * Run the solver to check unique solubility.
         */
-       while (!net_solver(w, h, tiles, NULL, params->wrapping)) {
+       while (net_solver(w, h, tiles, NULL, params->wrapping) != 1) {
            int n = 0;
 
            /*
@@ -1382,13 +1415,98 @@ static char *new_game_desc(game_params *params, random_state *rs,
 
     /*
      * Now shuffle the grid.
+     * 
+     * In order to avoid accidentally generating an already-solved
+     * grid, we will reshuffle as necessary to ensure that at least
+     * one edge has a mismatched connection.
+     *
+     * This can always be done, since validate_params() enforces a
+     * grid area of at least 2 and our generator never creates
+     * either type of rotationally invariant tile (cross and
+     * blank). Hence there must be at least one edge separating
+     * distinct tiles, and it must be possible to find orientations
+     * of those tiles such that one tile is trying to connect
+     * through that edge and the other is not.
+     * 
+     * (We could be more subtle, and allow the shuffle to generate
+     * a grid in which all tiles match up locally and the only
+     * criterion preventing the grid from being already solved is
+     * connectedness. However, that would take more effort, and
+     * it's easier to simply make sure every grid is _obviously_
+     * not solved.)
+     *
+     * We also require that our shuffle produces no loops in the
+     * initial grid state, because it's a bit rude to light up a 'HEY,
+     * YOU DID SOMETHING WRONG!' indicator when the user hasn't even
+     * had a chance to do _anything_ yet. This also is possible just
+     * by retrying the whole shuffle on failure, because it's clear
+     * that at least one non-solved shuffle with no loops must exist.
+     * (Proof: take the _solved_ state of the puzzle, and rotate one
+     * endpoint.)
      */
-    for (y = 0; y < h; y++) {
-       for (x = 0; x < w; x++) {
-           int orig = index(params, tiles, x, y);
-           int rot = random_upto(rs, 4);
-           index(params, tiles, x, y) = ROT(orig, rot);
-       }
+    while (1) {
+        int mismatches, prev_loopsquares, this_loopsquares, i;
+        int *loops;
+
+      shuffle:
+        for (y = 0; y < h; y++) {
+            for (x = 0; x < w; x++) {
+                int orig = index(params, tiles, x, y);
+                int rot = random_upto(rs, 4);
+                index(params, tiles, x, y) = ROT(orig, rot);
+            }
+        }
+
+        /*
+         * Check for loops, and try to fix them by reshuffling just
+         * the squares involved.
+         */
+        prev_loopsquares = w*h+1;
+        while (1) {
+            loops = compute_loops_inner(w, h, params->wrapping, tiles, NULL);
+            this_loopsquares = 0;
+            for (i = 0; i < w*h; i++) {
+                if (loops[i]) {
+                    int orig = tiles[i];
+                    int rot = random_upto(rs, 4);
+                    tiles[i] = ROT(orig, rot);
+                    this_loopsquares++;
+                }
+            }
+            sfree(loops);
+            if (this_loopsquares > prev_loopsquares) {
+                /*
+                 * We're increasing rather than reducing the number of
+                 * loops. Give up and go back to the full shuffle.
+                 */
+                goto shuffle;
+            }
+            if (this_loopsquares == 0)
+                break;
+            prev_loopsquares = this_loopsquares;
+        }
+
+        mismatches = 0;
+        /*
+         * I can't even be bothered to check for mismatches across
+         * a wrapping edge, so I'm just going to enforce that there
+         * must be a mismatch across a non-wrapping edge, which is
+         * still always possible.
+         */
+        for (y = 0; y < h; y++) for (x = 0; x < w; x++) {
+            if (x+1 < w && ((ROT(index(params, tiles, x, y), 2) ^ 
+                             index(params, tiles, x+1, y)) & L))
+                mismatches++;
+            if (y+1 < h && ((ROT(index(params, tiles, x, y), 2) ^ 
+                             index(params, tiles, x, y+1)) & U))
+                mismatches++;
+        }
+
+        if (mismatches == 0)
+            continue;
+
+        /* OK. */
+        break;
     }
 
     /*
@@ -1480,7 +1598,7 @@ static char *new_game_desc(game_params *params, random_state *rs,
     return desc;
 }
 
-static char *validate_desc(game_params *params, char *desc)
+static char *validate_desc(const game_params *params, const char *desc)
 {
     int w = params->width, h = params->height;
     int i;
@@ -1510,7 +1628,8 @@ static char *validate_desc(game_params *params, char *desc)
  * Construct an initial game state, given a description and parameters.
  */
 
-static game_state *new_game(midend *me, game_params *params, char *desc)
+static game_state *new_game(midend *me, const game_params *params,
+                            const char *desc)
 {
     game_state *state;
     int w, h, x, y;
@@ -1525,12 +1644,14 @@ static game_state *new_game(midend *me, game_params *params, char *desc)
     w = state->width = params->width;
     h = state->height = params->height;
     state->wrapping = params->wrapping;
+    state->imm = snew(game_immutable_state);
+    state->imm->refcount = 1;
     state->last_rotate_dir = state->last_rotate_x = state->last_rotate_y = 0;
-    state->completed = state->used_solve = state->just_used_solve = FALSE;
+    state->completed = state->used_solve = FALSE;
     state->tiles = snewn(state->width * state->height, unsigned char);
     memset(state->tiles, 0, state->width * state->height);
-    state->barriers = snewn(state->width * state->height, unsigned char);
-    memset(state->barriers, 0, state->width * state->height);
+    state->imm->barriers = snewn(state->width * state->height, unsigned char);
+    memset(state->imm->barriers, 0, state->width * state->height);
 
     /*
      * Parse the game description into the grid.
@@ -1587,7 +1708,7 @@ static game_state *new_game(midend *me, game_params *params, char *desc)
             if (!(barrier(state, x, 0) & U) ||
                 !(barrier(state, x, state->height-1) & D))
                 state->wrapping = TRUE;
-        for (y = 0; y < state->width; y++)
+        for (y = 0; y < state->height; y++)
             if (!(barrier(state, 0, y) & L) ||
                 !(barrier(state, state->width-1, y) & R))
                 state->wrapping = TRUE;
@@ -1596,37 +1717,39 @@ static game_state *new_game(midend *me, game_params *params, char *desc)
     return state;
 }
 
-static game_state *dup_game(game_state *state)
+static game_state *dup_game(const game_state *state)
 {
     game_state *ret;
 
     ret = snew(game_state);
+    ret->imm = state->imm;
+    ret->imm->refcount++;
     ret->width = state->width;
     ret->height = state->height;
     ret->wrapping = state->wrapping;
     ret->completed = state->completed;
     ret->used_solve = state->used_solve;
-    ret->just_used_solve = state->just_used_solve;
     ret->last_rotate_dir = state->last_rotate_dir;
     ret->last_rotate_x = state->last_rotate_x;
     ret->last_rotate_y = state->last_rotate_y;
     ret->tiles = snewn(state->width * state->height, unsigned char);
     memcpy(ret->tiles, state->tiles, state->width * state->height);
-    ret->barriers = snewn(state->width * state->height, unsigned char);
-    memcpy(ret->barriers, state->barriers, state->width * state->height);
 
     return ret;
 }
 
 static void free_game(game_state *state)
 {
+    if (--state->imm->refcount == 0) {
+        sfree(state->imm->barriers);
+        sfree(state->imm);
+    }
     sfree(state->tiles);
-    sfree(state->barriers);
     sfree(state);
 }
 
-static char *solve_game(game_state *state, game_state *currstate,
-                       char *aux, char **error)
+static char *solve_game(const game_state *state, const game_state *currstate,
+                        const char *aux, char **error)
 {
     unsigned char *tiles;
     char *ret;
@@ -1640,9 +1763,17 @@ static char *solve_game(game_state *state, game_state *currstate,
         * Run the internal solver on the provided grid. This might
         * not yield a complete solution.
         */
+        int solver_result;
+
        memcpy(tiles, state->tiles, state->width * state->height);
-       net_solver(state->width, state->height, tiles,
-                  state->barriers, state->wrapping);
+       solver_result = net_solver(state->width, state->height, tiles,
+                                   state->imm->barriers, state->wrapping);
+
+        if (solver_result < 0) {
+            *error = "No solution exists for this puzzle";
+            sfree(tiles);
+            return NULL;
+        }
     } else {
         for (i = 0; i < state->width * state->height; i++) {
             int c = aux[i];
@@ -1720,7 +1851,12 @@ static char *solve_game(game_state *state, game_state *currstate,
     return ret;
 }
 
-static char *game_text_format(game_state *state)
+static int game_can_format_as_text_now(const game_params *params)
+{
+    return TRUE;
+}
+
+static char *game_text_format(const game_state *state)
 {
     return NULL;
 }
@@ -1736,7 +1872,7 @@ static char *game_text_format(game_state *state)
  * completed - just call this function and see whether every square
  * is marked active.
  */
-static unsigned char *compute_active(game_state *state, int cx, int cy)
+static unsigned char *compute_active(const game_state *state, int cx, int cy)
 {
     unsigned char *active;
     tree234 *todo;
@@ -1786,15 +1922,99 @@ static unsigned char *compute_active(game_state *state, int cx, int cy)
     return active;
 }
 
+struct net_neighbour_ctx {
+    int w, h;
+    const unsigned char *tiles, *barriers;
+    int i, n, neighbours[4];
+};
+static int net_neighbour(int vertex, void *vctx)
+{
+    struct net_neighbour_ctx *ctx = (struct net_neighbour_ctx *)vctx;
+
+    if (vertex >= 0) {
+        int x = vertex % ctx->w, y = vertex / ctx->w;
+        int tile, dir, x1, y1, v1;
+
+        ctx->i = ctx->n = 0;
+
+        tile = ctx->tiles[vertex];
+        if (ctx->barriers)
+            tile &= ~ctx->barriers[vertex];
+
+        for (dir = 1; dir < 0x10; dir <<= 1) {
+            if (!(tile & dir))
+                continue;
+            OFFSETWH(x1, y1, x, y, dir, ctx->w, ctx->h);
+            v1 = y1 * ctx->w + x1;
+            if (ctx->tiles[v1] & F(dir))
+                ctx->neighbours[ctx->n++] = v1;
+        }
+    }
+
+    if (ctx->i < ctx->n)
+        return ctx->neighbours[ctx->i++];
+    else
+        return -1;
+}
+
+static int *compute_loops_inner(int w, int h, int wrapping,
+                                const unsigned char *tiles,
+                                const unsigned char *barriers)
+{
+    struct net_neighbour_ctx ctx;
+    struct findloopstate *fls;
+    int *loops;
+    int x, y;
+
+    fls = findloop_new_state(w*h);
+    ctx.w = w;
+    ctx.h = h;
+    ctx.tiles = tiles;
+    ctx.barriers = barriers;
+    findloop_run(fls, w*h, net_neighbour, &ctx);
+
+    loops = snewn(w*h, int);
+
+    for (y = 0; y < h; y++) {
+        for (x = 0; x < w; x++) {
+            int x1, y1, dir;
+            int flags = 0;
+
+            for (dir = 1; dir < 0x10; dir <<= 1) {
+                if ((tiles[y*w+x] & dir) &&
+                    !(barriers && (barriers[y*w+x] & dir))) {
+                    OFFSETWH(x1, y1, x, y, dir, w, h);
+                    if ((tiles[y1*w+x1] & F(dir)) &&
+                        findloop_is_loop_edge(fls, y*w+x, y1*w+x1))
+                        flags |= LOOP(dir);
+                }
+            }
+            loops[y*w+x] = flags;
+        }
+    }
+
+    findloop_free_state(fls);
+    return loops;
+}
+
+static int *compute_loops(const game_state *state)
+{
+    return compute_loops_inner(state->width, state->height, state->wrapping,
+                               state->tiles, state->imm->barriers);
+}
+
 struct game_ui {
     int org_x, org_y; /* origin */
     int cx, cy;       /* source tile (game coordinates) */
     int cur_x, cur_y;
     int cur_visible;
     random_state *rs; /* used for jumbling */
+#ifdef USE_DRAGGING
+    int dragtilex, dragtiley, dragstartx, dragstarty, dragged;
+#endif
 };
 
-static game_ui *new_ui(game_state *state)
+static game_ui *new_ui(const game_state *state)
 {
     void *seed;
     int seedsize;
@@ -1816,7 +2036,7 @@ static void free_ui(game_ui *ui)
     sfree(ui);
 }
 
-static char *encode_ui(game_ui *ui)
+static char *encode_ui(const game_ui *ui)
 {
     char buf[120];
     /*
@@ -1827,30 +2047,30 @@ static char *encode_ui(game_ui *ui)
     return dupstr(buf);
 }
 
-static void decode_ui(game_ui *ui, char *encoding)
+static void decode_ui(game_ui *ui, const char *encoding)
 {
     sscanf(encoding, "O%d,%d;C%d,%d",
           &ui->org_x, &ui->org_y, &ui->cx, &ui->cy);
 }
 
-static void game_changed_state(game_ui *ui, game_state *oldstate,
-                               game_state *newstate)
+static void game_changed_state(game_ui *ui, const game_state *oldstate,
+                               const game_state *newstate)
 {
 }
 
 struct game_drawstate {
     int started;
     int width, height;
-    int org_x, org_y;
     int tilesize;
-    unsigned char *visible;
+    unsigned long *visible, *to_draw;
 };
 
 /* ----------------------------------------------------------------------
  * Process a move.
  */
-static char *interpret_move(game_state *state, game_ui *ui,
-                           game_drawstate *ds, int x, int y, int button)
+static char *interpret_move(const game_state *state, game_ui *ui,
+                            const game_drawstate *ds,
+                            int x, int y, int button)
 {
     char *nullret;
     int tx = -1, ty = -1, dir = 0;
@@ -1866,6 +2086,12 @@ static char *interpret_move(game_state *state, game_ui *ui,
 
     if (button == LEFT_BUTTON ||
        button == MIDDLE_BUTTON ||
+#ifdef USE_DRAGGING
+       button == LEFT_DRAG ||
+       button == LEFT_RELEASE ||
+       button == RIGHT_DRAG ||
+       button == RIGHT_RELEASE ||
+#endif
        button == RIGHT_BUTTON) {
 
        if (ui->cur_visible) {
@@ -1876,8 +2102,8 @@ static char *interpret_move(game_state *state, game_ui *ui,
        /*
         * The button must have been clicked on a valid tile.
         */
-       x -= WINDOW_OFFSET + TILE_BORDER;
-       y -= WINDOW_OFFSET + TILE_BORDER;
+       x -= WINDOW_OFFSET + LINE_THICK;
+       y -= WINDOW_OFFSET + LINE_THICK;
        if (x < 0 || y < 0)
            return nullret;
        tx = x / TILE_SIZE;
@@ -1887,14 +2113,117 @@ static char *interpret_move(game_state *state, game_ui *ui,
         /* Transform from physical to game coords */
         tx = (tx + ui->org_x) % state->width;
         ty = (ty + ui->org_y) % state->height;
-       if (x % TILE_SIZE >= TILE_SIZE - TILE_BORDER ||
-           y % TILE_SIZE >= TILE_SIZE - TILE_BORDER)
+       if (x % TILE_SIZE >= TILE_SIZE - LINE_THICK ||
+           y % TILE_SIZE >= TILE_SIZE - LINE_THICK)
            return nullret;
 
-        action = button == LEFT_BUTTON ? ROTATE_LEFT :
-                 button == RIGHT_BUTTON ? ROTATE_RIGHT : TOGGLE_LOCK;
-    } else if (button == CURSOR_UP || button == CURSOR_DOWN ||
-              button == CURSOR_RIGHT || button == CURSOR_LEFT) {
+#ifdef USE_DRAGGING
+
+        if (button == MIDDLE_BUTTON
+#ifdef STYLUS_BASED
+           || button == RIGHT_BUTTON  /* with a stylus, `right-click' locks */
+#endif
+           ) {
+            /*
+             * Middle button never drags: it only toggles the lock.
+             */
+            action = TOGGLE_LOCK;
+        } else if (button == LEFT_BUTTON
+#ifndef STYLUS_BASED
+                   || button == RIGHT_BUTTON /* (see above) */
+#endif
+                  ) {
+            /*
+             * Otherwise, we note down the start point for a drag.
+             */
+            ui->dragtilex = tx;
+            ui->dragtiley = ty;
+            ui->dragstartx = x % TILE_SIZE;
+            ui->dragstarty = y % TILE_SIZE;
+            ui->dragged = FALSE;
+            return nullret;            /* no actual action */
+        } else if (button == LEFT_DRAG
+#ifndef STYLUS_BASED
+                   || button == RIGHT_DRAG
+#endif
+                  ) {
+            /*
+             * Find the new drag point and see if it necessitates a
+             * rotation.
+             */
+            int x0,y0, xA,yA, xC,yC, xF,yF;
+            int mx, my;
+            int d0, dA, dC, dF, dmin;
+
+            tx = ui->dragtilex;
+            ty = ui->dragtiley;
+
+            mx = x - (ui->dragtilex * TILE_SIZE);
+            my = y - (ui->dragtiley * TILE_SIZE);
+
+            x0 = ui->dragstartx;
+            y0 = ui->dragstarty;
+            xA = ui->dragstarty;
+            yA = TILE_SIZE-1 - ui->dragstartx;
+            xF = TILE_SIZE-1 - ui->dragstartx;
+            yF = TILE_SIZE-1 - ui->dragstarty;
+            xC = TILE_SIZE-1 - ui->dragstarty;
+            yC = ui->dragstartx;
+
+            d0 = (mx-x0)*(mx-x0) + (my-y0)*(my-y0);
+            dA = (mx-xA)*(mx-xA) + (my-yA)*(my-yA);
+            dF = (mx-xF)*(mx-xF) + (my-yF)*(my-yF);
+            dC = (mx-xC)*(mx-xC) + (my-yC)*(my-yC);
+
+            dmin = min(min(d0,dA),min(dF,dC));
+
+            if (d0 == dmin) {
+                return nullret;
+            } else if (dF == dmin) {
+                action = ROTATE_180;
+                ui->dragstartx = xF;
+                ui->dragstarty = yF;
+                ui->dragged = TRUE;
+            } else if (dA == dmin) {
+                action = ROTATE_LEFT;
+                ui->dragstartx = xA;
+                ui->dragstarty = yA;
+                ui->dragged = TRUE;
+            } else /* dC == dmin */ {
+                action = ROTATE_RIGHT;
+                ui->dragstartx = xC;
+                ui->dragstarty = yC;
+                ui->dragged = TRUE;
+            }
+        } else if (button == LEFT_RELEASE
+#ifndef STYLUS_BASED
+                   || button == RIGHT_RELEASE
+#endif
+                  ) {
+            if (!ui->dragged) {
+                /*
+                 * There was a click but no perceptible drag:
+                 * revert to single-click behaviour.
+                 */
+                tx = ui->dragtilex;
+                ty = ui->dragtiley;
+
+                if (button == LEFT_RELEASE)
+                    action = ROTATE_LEFT;
+                else
+                    action = ROTATE_RIGHT;
+            } else
+                return nullret;        /* no action */
+        }
+
+#else /* USE_DRAGGING */
+
+       action = (button == LEFT_BUTTON ? ROTATE_LEFT :
+                 button == RIGHT_BUTTON ? ROTATE_RIGHT : TOGGLE_LOCK);
+
+#endif /* USE_DRAGGING */
+
+    } else if (IS_CURSOR_MOVE(button)) {
         switch (button) {
           case CURSOR_UP:       dir = U; break;
           case CURSOR_DOWN:     dir = D; break;
@@ -1909,12 +2238,12 @@ static char *interpret_move(game_state *state, game_ui *ui,
     } else if (button == 'a' || button == 's' || button == 'd' ||
               button == 'A' || button == 'S' || button == 'D' ||
                button == 'f' || button == 'F' ||
-              button == CURSOR_SELECT) {
+               IS_CURSOR_SELECT(button)) {
        tx = ui->cur_x;
        ty = ui->cur_y;
        if (button == 'a' || button == 'A' || button == CURSOR_SELECT)
            action = ROTATE_LEFT;
-       else if (button == 's' || button == 'S')
+       else if (button == 's' || button == 'S' || button == CURSOR_SELECT2)
            action = TOGGLE_LOCK;
        else if (button == 'd' || button == 'D')
            action = ROTATE_RIGHT;
@@ -2013,17 +2342,16 @@ static char *interpret_move(game_state *state, game_ui *ui,
     }
 }
 
-static game_state *execute_move(game_state *from, char *move)
+static game_state *execute_move(const game_state *from, const char *move)
 {
     game_state *ret;
-    int tx, ty, n, noanim, orig;
+    int tx = -1, ty = -1, n, noanim, orig;
 
     ret = dup_game(from);
-    ret->just_used_solve = FALSE;
 
     if (move[0] == 'J' || move[0] == 'S') {
        if (move[0] == 'S')
-           ret->just_used_solve = ret->used_solve = TRUE;
+           ret->used_solve = TRUE;
 
        move++;
        if (*move == ';')
@@ -2066,6 +2394,7 @@ static game_state *execute_move(game_state *from, char *move)
        }
     }
     if (!noanim) {
+        if (tx == -1 || ty == -1) { free_game(ret); return NULL; }
        ret->last_rotate_x = tx;
        ret->last_rotate_y = ty;
     }
@@ -2073,24 +2402,31 @@ static game_state *execute_move(game_state *from, char *move)
     /*
      * Check whether the game has been completed.
      * 
-     * For this purpose it doesn't matter where the source square
-     * is, because we can start from anywhere and correctly
-     * determine whether the game is completed.
+     * For this purpose it doesn't matter where the source square is,
+     * because we can start from anywhere (or, at least, any square
+     * that's non-empty!), and correctly determine whether the game is
+     * completed.
      */
     {
-       unsigned char *active = compute_active(ret, 0, 0);
-       int x1, y1;
+       unsigned char *active;
+       int pos;
        int complete = TRUE;
 
-       for (x1 = 0; x1 < ret->width; x1++)
-           for (y1 = 0; y1 < ret->height; y1++)
-               if ((tile(ret, x1, y1) & 0xF) && !index(ret, active, x1, y1)) {
+       for (pos = 0; pos < ret->width * ret->height; pos++)
+            if (ret->tiles[pos] & 0xF)
+                break;
+
+        if (pos < ret->width * ret->height) {
+            active = compute_active(ret, pos % ret->width, pos / ret->width);
+
+            for (pos = 0; pos < ret->width * ret->height; pos++)
+                if ((ret->tiles[pos] & 0xF) && !active[pos]) {
                    complete = FALSE;
-                   goto break_label;  /* break out of two loops at once */
-               }
-       break_label:
+                    break;
+                }
 
-       sfree(active);
+            sfree(active);
+        }
 
        if (complete)
            ret->completed = TRUE;
@@ -2104,41 +2440,52 @@ static game_state *execute_move(game_state *from, char *move)
  * Routines for drawing the game position on the screen.
  */
 
-static game_drawstate *game_new_drawstate(drawing *dr, game_state *state)
+static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
 {
     game_drawstate *ds = snew(game_drawstate);
+    int i, ncells;
 
     ds->started = FALSE;
     ds->width = state->width;
     ds->height = state->height;
-    ds->org_x = ds->org_y = -1;
-    ds->visible = snewn(state->width * state->height, unsigned char);
+    ncells = (state->width+2) * (state->height+2);
+    ds->visible = snewn(ncells, unsigned long);
+    ds->to_draw = snewn(ncells, unsigned long);
     ds->tilesize = 0;                  /* undecided yet */
-    memset(ds->visible, 0xFF, state->width * state->height);
+    for (i = 0; i < ncells; i++)
+        ds->visible[i] = -1;
 
     return ds;
 }
 
+#define dsindex(ds, field, x, y) ((ds)->field[((y)+1)*((ds)->width+2)+((x)+1)])
+#define visible(ds, x, y) dsindex(ds, visible, x, y)
+#define todraw(ds, x, y) dsindex(ds, to_draw, x, y)
+
 static void game_free_drawstate(drawing *dr, game_drawstate *ds)
 {
     sfree(ds->visible);
     sfree(ds);
 }
 
-static void game_compute_size(game_params *params, int tilesize,
-                             int *x, int *y)
+static void game_compute_size(const game_params *params, int tilesize,
+                              int *x, int *y)
 {
-    *x = WINDOW_OFFSET * 2 + tilesize * params->width + TILE_BORDER;
-    *y = WINDOW_OFFSET * 2 + tilesize * params->height + TILE_BORDER;
+    /* Ick: fake up `ds->tilesize' for macro expansion purposes */
+    struct { int tilesize; } ads, *ds = &ads;
+    ads.tilesize = tilesize;
+
+    *x = WINDOW_OFFSET * 2 + TILE_SIZE * params->width + LINE_THICK;
+    *y = WINDOW_OFFSET * 2 + TILE_SIZE * params->height + LINE_THICK;
 }
 
 static void game_set_size(drawing *dr, game_drawstate *ds,
-                         game_params *params, int tilesize)
+                          const game_params *params, int tilesize)
 {
     ds->tilesize = tilesize;
 }
 
-static float *game_colours(frontend *fe, game_state *state, int *ncolours)
+static float *game_colours(frontend *fe, int *ncolours)
 {
     float *ret;
 
@@ -2172,6 +2519,13 @@ static float *game_colours(frontend *fe, game_state *state, int *ncolours)
     ret[COL_BARRIER * 3 + 1] = 0.0F;
     ret[COL_BARRIER * 3 + 2] = 0.0F;
 
+    /*
+     * Highlighted loops are red as well.
+     */
+    ret[COL_LOOP * 3 + 0] = 1.0F;
+    ret[COL_LOOP * 3 + 1] = 0.0F;
+    ret[COL_LOOP * 3 + 2] = 0.0F;
+
     /*
      * Unpowered endpoints are blue.
      */
@@ -2196,361 +2550,313 @@ static float *game_colours(frontend *fe, game_state *state, int *ncolours)
     return ret;
 }
 
-static void draw_thick_line(drawing *dr, int x1, int y1, int x2, int y2,
-                            int colour)
+static void rotated_coords(float *ox, float *oy, const float matrix[4],
+                           float cx, float cy, float ix, float iy)
 {
-    draw_line(dr, x1-1, y1, x2-1, y2, COL_WIRE);
-    draw_line(dr, x1+1, y1, x2+1, y2, COL_WIRE);
-    draw_line(dr, x1, y1-1, x2, y2-1, COL_WIRE);
-    draw_line(dr, x1, y1+1, x2, y2+1, COL_WIRE);
-    draw_line(dr, x1, y1, x2, y2, colour);
+    *ox = matrix[0] * ix + matrix[2] * iy + cx;
+    *oy = matrix[1] * ix + matrix[3] * iy + cy;
 }
 
-static void draw_rect_coords(drawing *dr, int x1, int y1, int x2, int y2,
-                             int colour)
+/* Flags describing the visible features of a tile. */
+#define TILE_BARRIER_SHIFT            0  /* 4 bits: R U L D */
+#define TILE_BARRIER_CORNER_SHIFT     4  /* 4 bits: RU UL LD DR */
+#define TILE_KEYBOARD_CURSOR      (1<<8) /* 1 bit if cursor is here */
+#define TILE_WIRE_SHIFT               9  /* 8 bits: RR UU LL DD
+                                          * Each pair: 0=no wire, 1=unpowered,
+                                          * 2=powered, 3=loop err highlight */
+#define TILE_ENDPOINT_SHIFT          17  /* 2 bits: 0=no endpoint, 1=unpowered,
+                                          * 2=powered, 3=power-source square */
+#define TILE_WIRE_ON_EDGE_SHIFT      19  /* 8 bits: RR UU LL DD,
+                                          * same encoding as TILE_WIRE_SHIFT */
+#define TILE_ROTATING          (1UL<<27) /* 1 bit if tile is rotating */
+#define TILE_LOCKED            (1UL<<28) /* 1 bit if tile is locked */
+
+static void draw_wires(drawing *dr, int cx, int cy, int radius,
+                       unsigned long tile, int bitmap,
+                       int colour, int halfwidth, const float matrix[4])
 {
-    int mx = (x1 < x2 ? x1 : x2);
-    int my = (y1 < y2 ? y1 : y2);
-    int dx = (x2 + x1 - 2*mx + 1);
-    int dy = (y2 + y1 - 2*my + 1);
-
-    draw_rect(dr, mx, my, dx, dy, colour);
-}
-
-/*
- * draw_barrier_corner() and draw_barrier() are passed physical coords
- */
-static void draw_barrier_corner(drawing *dr, game_drawstate *ds,
-                                int x, int y, int dx, int dy, int phase)
-{
-    int bx = WINDOW_OFFSET + TILE_SIZE * x;
-    int by = WINDOW_OFFSET + TILE_SIZE * y;
-    int x1, y1;
-
-    x1 = (dx > 0 ? TILE_SIZE+TILE_BORDER-1 : 0);
-    y1 = (dy > 0 ? TILE_SIZE+TILE_BORDER-1 : 0);
-
-    if (phase == 0) {
-        draw_rect_coords(dr, bx+x1+dx, by+y1,
-                         bx+x1-TILE_BORDER*dx, by+y1-(TILE_BORDER-1)*dy,
-                         COL_WIRE);
-        draw_rect_coords(dr, bx+x1, by+y1+dy,
-                         bx+x1-(TILE_BORDER-1)*dx, by+y1-TILE_BORDER*dy,
-                         COL_WIRE);
-    } else {
-        draw_rect_coords(dr, bx+x1, by+y1,
-                         bx+x1-(TILE_BORDER-1)*dx, by+y1-(TILE_BORDER-1)*dy,
-                         COL_BARRIER);
+    float fpoints[12*2];
+    int points[12*2];
+    int npoints, d, dsh, i;
+    int any_wire_this_colour = FALSE;
+    float xf, yf;
+
+    npoints = 0;
+    for (d = 1, dsh = 0; d < 16; d *= 2, dsh++) {
+        int wiretype = (tile >> (TILE_WIRE_SHIFT + 2*dsh)) & 3;
+
+        fpoints[2*npoints+0] = halfwidth * (X(d) + X(C(d)));
+        fpoints[2*npoints+1] = halfwidth * (Y(d) + Y(C(d)));
+        npoints++;
+
+        if (bitmap & (1 << wiretype)) {
+            fpoints[2*npoints+0] = radius * X(d) + halfwidth * X(C(d));
+            fpoints[2*npoints+1] = radius * Y(d) + halfwidth * Y(C(d));
+            npoints++;
+            fpoints[2*npoints+0] = radius * X(d) + halfwidth * X(A(d));
+            fpoints[2*npoints+1] = radius * Y(d) + halfwidth * Y(A(d));
+            npoints++;
+
+            any_wire_this_colour = TRUE;
+        }
     }
-}
-
-static void draw_barrier(drawing *dr, game_drawstate *ds,
-                         int x, int y, int dir, int phase)
-{
-    int bx = WINDOW_OFFSET + TILE_SIZE * x;
-    int by = WINDOW_OFFSET + TILE_SIZE * y;
-    int x1, y1, w, h;
 
-    x1 = (X(dir) > 0 ? TILE_SIZE : X(dir) == 0 ? TILE_BORDER : 0);
-    y1 = (Y(dir) > 0 ? TILE_SIZE : Y(dir) == 0 ? TILE_BORDER : 0);
-    w = (X(dir) ? TILE_BORDER : TILE_SIZE - TILE_BORDER);
-    h = (Y(dir) ? TILE_BORDER : TILE_SIZE - TILE_BORDER);
+    if (!any_wire_this_colour)
+        return;
 
-    if (phase == 0) {
-        draw_rect(dr, bx+x1-X(dir), by+y1-Y(dir), w, h, COL_WIRE);
-    } else {
-        draw_rect(dr, bx+x1, by+y1, w, h, COL_BARRIER);
+    for (i = 0; i < npoints; i++) {
+        rotated_coords(&xf, &yf, matrix, cx, cy, fpoints[2*i], fpoints[2*i+1]);
+        points[2*i] = 0.5 + xf;
+        points[2*i+1] = 0.5 + yf;
     }
+
+    draw_polygon(dr, points, npoints, colour, colour);
 }
 
-/*
- * draw_tile() is passed physical coordinates
- */
-static void draw_tile(drawing *dr, game_state *state, game_drawstate *ds,
-                      int x, int y, int tile, int src, float angle, int cursor)
+static void draw_tile(drawing *dr, game_drawstate *ds, int x, int y,
+                      unsigned long tile, float angle)
 {
-    int bx = WINDOW_OFFSET + TILE_SIZE * x;
-    int by = WINDOW_OFFSET + TILE_SIZE * y;
+    int tx, ty;
+    int clipx, clipy, clipX, clipY, clipw, cliph;
+    int border_br = LINE_THICK/2, border_tl = LINE_THICK - border_br;
+    int barrier_outline_thick = (LINE_THICK+1)/2;
+    int bg, d, dsh, pass;
+    int cx, cy, radius;
     float matrix[4];
-    float cx, cy, ex, ey, tx, ty;
-    int dir, col, phase;
+
+    tx = WINDOW_OFFSET + TILE_SIZE * x + border_br;
+    ty = WINDOW_OFFSET + TILE_SIZE * y + border_br;
 
     /*
-     * When we draw a single tile, we must draw everything up to
-     * and including the borders around the tile. This means that
-     * if the neighbouring tiles have connections to those borders,
-     * we must draw those connections on the borders themselves.
+     * Clip to the tile boundary, with adjustments if we're drawing
+     * just outside the grid.
      */
-
-    clip(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER);
+    clipx = tx; clipX = tx + TILE_SIZE;
+    clipy = ty; clipY = ty + TILE_SIZE;
+    if (x == -1) {
+        clipx = clipX - border_br - barrier_outline_thick;
+    } else if (x == ds->width) {
+        clipX = clipx + border_tl + barrier_outline_thick;
+    }
+    if (y == -1) {
+        clipy = clipY - border_br - barrier_outline_thick;
+    } else if (y == ds->height) {
+        clipY = clipy + border_tl + barrier_outline_thick;
+    }
+    clipw = clipX - clipx;
+    cliph = clipY - clipy;
+    clip(dr, clipx, clipy, clipw, cliph);
 
     /*
-     * So. First blank the tile out completely: draw a big
-     * rectangle in border colour, and a smaller rectangle in
-     * background colour to fill it in.
+     * Clear the clip region.
      */
-    draw_rect(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER,
-              COL_BORDER);
-    draw_rect(dr, bx+TILE_BORDER, by+TILE_BORDER,
-              TILE_SIZE-TILE_BORDER, TILE_SIZE-TILE_BORDER,
-              tile & LOCKED ? COL_LOCKED : COL_BACKGROUND);
+    bg = (tile & TILE_LOCKED) ? COL_LOCKED : COL_BACKGROUND;
+    draw_rect(dr, clipx, clipy, clipw, cliph, bg);
 
     /*
-     * Draw an inset outline rectangle as a cursor, in whichever of
-     * COL_LOCKED and COL_BACKGROUND we aren't currently drawing
-     * in.
+     * Draw the grid lines.
      */
-    if (cursor) {
-       draw_line(dr, bx+TILE_SIZE/8, by+TILE_SIZE/8,
-                 bx+TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8,
-                 tile & LOCKED ? COL_BACKGROUND : COL_LOCKED);
-       draw_line(dr, bx+TILE_SIZE/8, by+TILE_SIZE/8,
-                 bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE/8,
-                 tile & LOCKED ? COL_BACKGROUND : COL_LOCKED);
-       draw_line(dr, bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE/8,
-                 bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8,
-                 tile & LOCKED ? COL_BACKGROUND : COL_LOCKED);
-       draw_line(dr, bx+TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8,
-                 bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8,
-                 tile & LOCKED ? COL_BACKGROUND : COL_LOCKED);
+    {
+        int gridl = (x == -1 ? tx+TILE_SIZE-border_br : tx);
+        int gridr = (x == ds->width ? tx+border_tl : tx+TILE_SIZE);
+        int gridu = (y == -1 ? ty+TILE_SIZE-border_br : ty);
+        int gridd = (y == ds->height ? ty+border_tl : ty+TILE_SIZE);
+        if (x >= 0)
+            draw_rect(dr, tx, gridu, border_tl, gridd-gridu, COL_BORDER);
+        if (y >= 0)
+            draw_rect(dr, gridl, ty, gridr-gridl, border_tl, COL_BORDER);
+        if (x < ds->width)
+            draw_rect(dr, tx+TILE_SIZE-border_br, gridu,
+                      border_br, gridd-gridu, COL_BORDER);
+        if (y < ds->height)
+            draw_rect(dr, gridl, ty+TILE_SIZE-border_br,
+                      gridr-gridl, border_br, COL_BORDER);
     }
 
     /*
-     * Set up the rotation matrix.
+     * Draw the keyboard cursor.
      */
-    matrix[0] = (float)cos(angle * PI / 180.0);
-    matrix[1] = (float)-sin(angle * PI / 180.0);
-    matrix[2] = (float)sin(angle * PI / 180.0);
-    matrix[3] = (float)cos(angle * PI / 180.0);
+    if (tile & TILE_KEYBOARD_CURSOR) {
+        int cursorcol = (tile & TILE_LOCKED) ? COL_BACKGROUND : COL_LOCKED;
+        int inset_outer = TILE_SIZE/8, inset_inner = inset_outer + LINE_THICK;
+        draw_rect(dr, tx + inset_outer, ty + inset_outer,
+                  TILE_SIZE - 2*inset_outer, TILE_SIZE - 2*inset_outer,
+                  cursorcol);
+        draw_rect(dr, tx + inset_inner, ty + inset_inner,
+                  TILE_SIZE - 2*inset_inner, TILE_SIZE - 2*inset_inner,
+                  bg);
+    }
+
+    radius = (TILE_SIZE+1)/2;
+    cx = tx + radius;
+    cy = ty + radius;
+    radius++;
 
     /*
-     * Draw the wires.
+     * Draw protrusions into this cell's edges of wires in
+     * neighbouring cells, as given by the TILE_WIRE_ON_EDGE_SHIFT
+     * flags. We only draw each of these if there _isn't_ a wire of
+     * our own that's going to overlap it, which means either the
+     * corresponding TILE_WIRE_SHIFT flag is zero, or else the
+     * TILE_ROTATING flag is set (so that our main wire won't be drawn
+     * in quite that place anyway).
      */
-    cx = cy = TILE_BORDER + (TILE_SIZE-TILE_BORDER) / 2.0F - 0.5F;
-    col = (tile & ACTIVE ? COL_POWERED : COL_WIRE);
-    for (dir = 1; dir < 0x10; dir <<= 1) {
-        if (tile & dir) {
-            ex = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * X(dir);
-            ey = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * Y(dir);
-            MATMUL(tx, ty, matrix, ex, ey);
-            draw_thick_line(dr, bx+(int)cx, by+(int)cy,
-                           bx+(int)(cx+tx), by+(int)(cy+ty),
-                            COL_WIRE);
-        }
-    }
-    for (dir = 1; dir < 0x10; dir <<= 1) {
-        if (tile & dir) {
-            ex = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * X(dir);
-            ey = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * Y(dir);
-            MATMUL(tx, ty, matrix, ex, ey);
-            draw_line(dr, bx+(int)cx, by+(int)cy,
-                     bx+(int)(cx+tx), by+(int)(cy+ty), col);
+    for (d = 1, dsh = 0; d < 16; d *= 2, dsh++) {
+        int edgetype = ((tile >> (TILE_WIRE_ON_EDGE_SHIFT + 2*dsh)) & 3);
+        if (edgetype == 0)
+            continue;             /* there isn't a wire on the edge */
+        if (!(tile & TILE_ROTATING) &&
+            ((tile >> (TILE_WIRE_SHIFT + 2*dsh)) & 3) != 0)
+            continue;     /* wire on edge would be overdrawn anyway */
+
+        for (pass = 0; pass < 2; pass++) {
+            int x, y, w, h;
+            int col = (pass == 0 || edgetype == 1 ? COL_WIRE :
+                       edgetype == 2 ? COL_POWERED : COL_LOOP);
+            int halfwidth = pass == 0 ? 2*LINE_THICK-1 : LINE_THICK-1;
+
+            if (X(d) < 0) {
+                x = tx;
+                w = border_tl;
+            } else if (X(d) > 0) {
+                x = tx + TILE_SIZE - border_br;
+                w = border_br;
+            } else {
+                x = cx - halfwidth;
+                w = 2 * halfwidth + 1;
+            }
+
+            if (Y(d) < 0) {
+                y = ty;
+                h = border_tl;
+            } else if (Y(d) > 0) {
+                y = ty + TILE_SIZE - border_br;
+                h = border_br;
+            } else {
+                y = cy - halfwidth;
+                h = 2 * halfwidth + 1;
+            }
+
+            draw_rect(dr, x, y, w, h, col);
         }
     }
 
     /*
-     * Draw the box in the middle. We do this in blue if the tile
-     * is an unpowered endpoint, in cyan if the tile is a powered
-     * endpoint, in black if the tile is the centrepiece, and
-     * otherwise not at all.
+     * Set up the rotation matrix for the main cell contents, i.e.
+     * everything that is centred in the grid square and optionally
+     * rotated by an arbitrary angle about that centre point.
      */
-    col = -1;
-    if (src)
-        col = COL_WIRE;
-    else if (COUNT(tile) == 1) {
-        col = (tile & ACTIVE ? COL_POWERED : COL_ENDPOINT);
-    }
-    if (col >= 0) {
-        int i, points[8];
-
-        points[0] = +1; points[1] = +1;
-        points[2] = +1; points[3] = -1;
-        points[4] = -1; points[5] = -1;
-        points[6] = -1; points[7] = +1;
-
-        for (i = 0; i < 8; i += 2) {
-            ex = (TILE_SIZE * 0.24F) * points[i];
-            ey = (TILE_SIZE * 0.24F) * points[i+1];
-            MATMUL(tx, ty, matrix, ex, ey);
-            points[i] = bx+(int)(cx+tx);
-            points[i+1] = by+(int)(cy+ty);
-        }
-
-        draw_polygon(dr, points, 4, col, COL_WIRE);
+    if (tile & TILE_ROTATING) {
+        matrix[0] = (float)cos(angle * PI / 180.0);
+        matrix[2] = (float)sin(angle * PI / 180.0);
+    } else {
+        matrix[0] = 1.0F;
+        matrix[2] = 0.0F;
     }
+    matrix[3] = matrix[0];
+    matrix[1] = -matrix[2];
 
     /*
-     * Draw the points on the border if other tiles are connected
-     * to us.
+     * Draw the wires.
      */
-    for (dir = 1; dir < 0x10; dir <<= 1) {
-        int dx, dy, px, py, lx, ly, vx, vy, ox, oy;
-
-        dx = X(dir);
-        dy = Y(dir);
-
-        ox = x + dx;
-        oy = y + dy;
-
-        if (ox < 0 || ox >= state->width || oy < 0 || oy >= state->height)
-            continue;
-
-        if (!(tile(state, GX(ox), GY(oy)) & F(dir)))
-            continue;
+    draw_wires(dr, cx, cy, radius, tile,
+               0xE, COL_WIRE, 2*LINE_THICK-1, matrix);
+    draw_wires(dr, cx, cy, radius, tile,
+               0x4, COL_POWERED, LINE_THICK-1, matrix);
+    draw_wires(dr, cx, cy, radius, tile,
+               0x8, COL_LOOP, LINE_THICK-1, matrix);
 
-        px = bx + (int)(dx>0 ? TILE_SIZE + TILE_BORDER - 1 : dx<0 ? 0 : cx);
-        py = by + (int)(dy>0 ? TILE_SIZE + TILE_BORDER - 1 : dy<0 ? 0 : cy);
-        lx = dx * (TILE_BORDER-1);
-        ly = dy * (TILE_BORDER-1);
-        vx = (dy ? 1 : 0);
-        vy = (dx ? 1 : 0);
+    /*
+     * Draw the central box.
+     */
+    for (pass = 0; pass < 2; pass++) {
+        int endtype = (tile >> TILE_ENDPOINT_SHIFT) & 3;
+        if (endtype) {
+            int i, points[8], col;
+            float boxr = TILE_SIZE * 0.24F + (pass == 0 ? LINE_THICK-1 : 0);
+
+            col = (pass == 0 || endtype == 3 ? COL_WIRE :
+                   endtype == 2 ? COL_POWERED : COL_ENDPOINT);
+
+            points[0] = +1; points[1] = +1;
+            points[2] = +1; points[3] = -1;
+            points[4] = -1; points[5] = -1;
+            points[6] = -1; points[7] = +1;
+
+            for (i = 0; i < 8; i += 2) {
+                float x, y;
+                rotated_coords(&x, &y, matrix, cx, cy,
+                               boxr * points[i], boxr * points[i+1]);
+                points[i] = x + 0.5;
+                points[i+1] = y + 0.5;
+            }
 
-        if (angle == 0.0 && (tile & dir)) {
-            /*
-             * If we are fully connected to the other tile, we must
-             * draw right across the tile border. (We can use our
-             * own ACTIVE state to determine what colour to do this
-             * in: if we are fully connected to the other tile then
-             * the two ACTIVE states will be the same.)
-             */
-            draw_rect_coords(dr, px-vx, py-vy, px+lx+vx, py+ly+vy, COL_WIRE);
-            draw_rect_coords(dr, px, py, px+lx, py+ly,
-                             (tile & ACTIVE) ? COL_POWERED : COL_WIRE);
-        } else {
-            /*
-             * The other tile extends into our border, but isn't
-             * actually connected to us. Just draw a single black
-             * dot.
-             */
-            draw_rect_coords(dr, px, py, px, py, COL_WIRE);
+            draw_polygon(dr, points, 4, col, COL_WIRE);
         }
     }
 
     /*
-     * Draw barrier corners, and then barriers.
+     * Draw barriers along grid edges.
      */
-    for (phase = 0; phase < 2; phase++) {
-        for (dir = 1; dir < 0x10; dir <<= 1) {
-            int x1, y1, corner = FALSE;
-            /*
-             * If at least one barrier terminates at the corner
-             * between dir and A(dir), draw a barrier corner.
-             */
-            if (barrier(state, GX(x), GY(y)) & (dir | A(dir))) {
-                corner = TRUE;
-            } else {
-                /*
-                 * Only count barriers terminating at this corner
-                 * if they're physically next to the corner. (That
-                 * is, if they've wrapped round from the far side
-                 * of the screen, they don't count.)
-                 */
-                x1 = x + X(dir);
-                y1 = y + Y(dir);
-                if (x1 >= 0 && x1 < state->width &&
-                    y1 >= 0 && y1 < state->height &&
-                    (barrier(state, GX(x1), GY(y1)) & A(dir))) {
-                    corner = TRUE;
-                } else {
-                    x1 = x + X(A(dir));
-                    y1 = y + Y(A(dir));
-                    if (x1 >= 0 && x1 < state->width &&
-                        y1 >= 0 && y1 < state->height &&
-                        (barrier(state, GX(x1), GY(y1)) & dir))
-                        corner = TRUE;
-                }
-            }
-
-            if (corner) {
-                /*
-                 * At least one barrier terminates here. Draw a
-                 * corner.
-                 */
-                draw_barrier_corner(dr, ds, x, y,
-                                    X(dir)+X(A(dir)), Y(dir)+Y(A(dir)),
-                                    phase);
-            }
+    for (pass = 0; pass < 2; pass++) {
+        int btl = border_tl, bbr = border_br, col = COL_BARRIER;
+        if (pass == 0) {
+            btl += barrier_outline_thick;
+            bbr += barrier_outline_thick;
+            col = COL_WIRE;
         }
 
-        for (dir = 1; dir < 0x10; dir <<= 1)
-            if (barrier(state, GX(x), GY(y)) & dir)
-                draw_barrier(dr, ds, x, y, dir, phase);
+        if (tile & (L << TILE_BARRIER_SHIFT))
+            draw_rect(dr, tx, ty, btl, TILE_SIZE, col);
+        if (tile & (R << TILE_BARRIER_SHIFT))
+            draw_rect(dr, tx+TILE_SIZE-bbr, ty, bbr, TILE_SIZE, col);
+        if (tile & (U << TILE_BARRIER_SHIFT))
+            draw_rect(dr, tx, ty, TILE_SIZE, btl, col);
+        if (tile & (D << TILE_BARRIER_SHIFT))
+            draw_rect(dr, tx, ty+TILE_SIZE-bbr, TILE_SIZE, bbr, col);
+
+        if (tile & (R << TILE_BARRIER_CORNER_SHIFT))
+            draw_rect(dr, tx+TILE_SIZE-bbr, ty, bbr, btl, col);
+        if (tile & (U << TILE_BARRIER_CORNER_SHIFT))
+            draw_rect(dr, tx, ty, btl, btl, col);
+        if (tile & (L << TILE_BARRIER_CORNER_SHIFT))
+            draw_rect(dr, tx, ty+TILE_SIZE-bbr, btl, bbr, col);
+        if (tile & (D << TILE_BARRIER_CORNER_SHIFT))
+            draw_rect(dr, tx+TILE_SIZE-bbr, ty+TILE_SIZE-bbr, bbr, bbr, col);
     }
 
+    /*
+     * Unclip and draw update, to finish.
+     */
     unclip(dr);
-
-    draw_update(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER);
+    draw_update(dr, clipx, clipy, clipw, cliph);
 }
 
-static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
-                 game_state *state, int dir, game_ui *ui, float t, float ft)
+static void game_redraw(drawing *dr, game_drawstate *ds,
+                        const game_state *oldstate, const game_state *state,
+                        int dir, const game_ui *ui,
+                        float t, float ft)
 {
-    int x, y, tx, ty, frame, last_rotate_dir, moved_origin = FALSE;
+    int tx, ty, dx, dy, d, dsh, last_rotate_dir, frame;
     unsigned char *active;
+    int *loops;
     float angle = 0.0;
 
     /*
-     * Clear the screen, and draw the exterior barrier lines, if
-     * this is our first call or if the origin has changed.
+     * Clear the screen on our first call.
      */
-    if (!ds->started || ui->org_x != ds->org_x || ui->org_y != ds->org_y) {
-        int phase;
+    if (!ds->started) {
+        int w, h;
+        game_params params;
 
         ds->started = TRUE;
 
-        draw_rect(dr, 0, 0, 
-                  WINDOW_OFFSET * 2 + TILE_SIZE * state->width + TILE_BORDER,
-                  WINDOW_OFFSET * 2 + TILE_SIZE * state->height + TILE_BORDER,
-                  COL_BACKGROUND);
-
-        ds->org_x = ui->org_x;
-        ds->org_y = ui->org_y;
-        moved_origin = TRUE;
-
-        draw_update(dr, 0, 0, 
-                    WINDOW_OFFSET*2 + TILE_SIZE*state->width + TILE_BORDER,
-                    WINDOW_OFFSET*2 + TILE_SIZE*state->height + TILE_BORDER);
-
-        for (phase = 0; phase < 2; phase++) {
-
-            for (x = 0; x < ds->width; x++) {
-                if (x+1 < ds->width) {
-                    if (barrier(state, GX(x), GY(0)) & R)
-                        draw_barrier_corner(dr, ds, x, -1, +1, +1, phase);
-                    if (barrier(state, GX(x), GY(ds->height-1)) & R)
-                        draw_barrier_corner(dr, ds, x, ds->height, +1, -1, phase);
-                }
-                if (barrier(state, GX(x), GY(0)) & U) {
-                    draw_barrier_corner(dr, ds, x, -1, -1, +1, phase);
-                    draw_barrier_corner(dr, ds, x, -1, +1, +1, phase);
-                    draw_barrier(dr, ds, x, -1, D, phase);
-                }
-                if (barrier(state, GX(x), GY(ds->height-1)) & D) {
-                    draw_barrier_corner(dr, ds, x, ds->height, -1, -1, phase);
-                    draw_barrier_corner(dr, ds, x, ds->height, +1, -1, phase);
-                    draw_barrier(dr, ds, x, ds->height, U, phase);
-                }
-            }
+        params.width = ds->width;
+        params.height = ds->height;
+        game_compute_size(&params, TILE_SIZE, &w, &h);
 
-            for (y = 0; y < ds->height; y++) {
-                if (y+1 < ds->height) {
-                    if (barrier(state, GX(0), GY(y)) & D)
-                        draw_barrier_corner(dr, ds, -1, y, +1, +1, phase);
-                    if (barrier(state, GX(ds->width-1), GY(y)) & D)
-                        draw_barrier_corner(dr, ds, ds->width, y, -1, +1, phase);
-                }
-                if (barrier(state, GX(0), GY(y)) & L) {
-                    draw_barrier_corner(dr, ds, -1, y, +1, -1, phase);
-                    draw_barrier_corner(dr, ds, -1, y, +1, +1, phase);
-                    draw_barrier(dr, ds, -1, y, R, phase);
-                }
-                if (barrier(state, GX(ds->width-1), GY(y)) & R) {
-                    draw_barrier_corner(dr, ds, ds->width, y, -1, -1, phase);
-                    draw_barrier_corner(dr, ds, ds->width, y, -1, +1, phase);
-                    draw_barrier(dr, ds, ds->width, y, L, phase);
-                }
-            }
-        }
+        draw_rect(dr, 0, 0, w, h, COL_BACKGROUND);
+        draw_update(dr, 0, 0, w, h);
     }
 
     tx = ty = -1;
@@ -2567,28 +2873,83 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
         state = oldstate;
     }
 
-    frame = -1;
     if (ft > 0) {
         /*
          * We're animating a completion flash. Find which frame
          * we're at.
          */
         frame = (int)(ft / FLASH_FRAME);
+    } else {
+        frame = 0;
     }
 
     /*
-     * Draw any tile which differs from the way it was last drawn.
+     * Build up a map of what we want every tile to look like. We
+     * include tiles one square outside the grid, for the outer edges
+     * of barriers.
      */
     active = compute_active(state, ui->cx, ui->cy);
+    loops = compute_loops(state);
 
-    for (x = 0; x < ds->width; x++)
-        for (y = 0; y < ds->height; y++) {
-            unsigned char c = tile(state, GX(x), GY(y)) |
-                              index(state, active, GX(x), GY(y));
-            int is_src = GX(x) == ui->cx && GY(y) == ui->cy;
-            int is_anim = GX(x) == tx && GY(y) == ty;
-            int is_cursor = ui->cur_visible &&
-                            GX(x) == ui->cur_x && GY(y) == ui->cur_y;
+    for (dy = -1; dy < ds->height+1; dy++) {
+        for (dx = -1; dx < ds->width+1; dx++) {
+            todraw(ds, dx, dy) = 0;
+        }
+    }
+
+    for (dy = 0; dy < ds->height; dy++) {
+        int gy = (dy + ui->org_y) % ds->height;
+        for (dx = 0; dx < ds->width; dx++) {
+            int gx = (dx + ui->org_x) % ds->width;
+            int t = (tile(state, gx, gy) |
+                     index(state, loops, gx, gy) |
+                     index(state, active, gx, gy));
+
+            for (d = 1, dsh = 0; d < 16; d *= 2, dsh++) {
+                if (barrier(state, gx, gy) & d) {
+                    todraw(ds, dx, dy) |=
+                        d << TILE_BARRIER_SHIFT;
+                    todraw(ds, dx + X(d), dy + Y(d)) |=
+                        F(d) << TILE_BARRIER_SHIFT;
+                    todraw(ds, dx + X(A(d)), dy + Y(A(d))) |=
+                        C(d) << TILE_BARRIER_CORNER_SHIFT;
+                    todraw(ds, dx + X(A(d)) + X(d), dy + Y(A(d)) + Y(d)) |=
+                        F(d) << TILE_BARRIER_CORNER_SHIFT;
+                    todraw(ds, dx + X(C(d)), dy + Y(C(d))) |=
+                        d << TILE_BARRIER_CORNER_SHIFT;
+                    todraw(ds, dx + X(C(d)) + X(d), dy + Y(C(d)) + Y(d)) |=
+                        A(d) << TILE_BARRIER_CORNER_SHIFT;
+                }
+
+                if (t & d) {
+                    int edgeval = (t & LOOP(d) ? 3 : t & ACTIVE ? 2 : 1);
+                    todraw(ds, dx, dy) |= edgeval << (TILE_WIRE_SHIFT + dsh*2);
+                    if (!(gx == tx && gy == ty)) {
+                        todraw(ds, dx + X(d), dy + Y(d)) |=
+                            edgeval << (TILE_WIRE_ON_EDGE_SHIFT + (dsh ^ 2)*2);
+                    }
+                }
+            }
+
+            if (ui->cur_visible && gx == ui->cur_x && gy == ui->cur_y)
+                todraw(ds, dx, dy) |= TILE_KEYBOARD_CURSOR;
+
+            if (gx == tx && gy == ty)
+                todraw(ds, dx, dy) |= TILE_ROTATING;
+
+            if (gx == ui->cx && gy == ui->cy) {
+                todraw(ds, dx, dy) |= 3 << TILE_ENDPOINT_SHIFT;
+            } else if ((t & 0xF) != R && (t & 0xF) != U && 
+                       (t & 0xF) != L && (t & 0xF) != D) {
+                /* this is not an endpoint tile */
+            } else if (t & ACTIVE) {
+                todraw(ds, dx, dy) |= 2 << TILE_ENDPOINT_SHIFT;
+            } else {
+                todraw(ds, dx, dy) |= 1 << TILE_ENDPOINT_SHIFT;
+            }
+
+            if (t & LOCKED)
+                todraw(ds, dx, dy) |= TILE_LOCKED;
 
             /*
              * In a completion flash, we adjust the LOCKED bit
@@ -2596,69 +2957,91 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
              * the frame number.
              */
             if (frame >= 0) {
-                int rcx = RX(ui->cx), rcy = RY(ui->cy);
+                int rcx = (ui->cx + ds->width - ui->org_x) % ds->width;
+                int rcy = (ui->cy + ds->height - ui->org_y) % ds->height;
                 int xdist, ydist, dist;
-                xdist = (x < rcx ? rcx - x : x - rcx);
-                ydist = (y < rcy ? rcy - y : y - rcy);
+                xdist = (dx < rcx ? rcx - dx : dx - rcx);
+                ydist = (dy < rcy ? rcy - dy : dy - rcy);
                 dist = (xdist > ydist ? xdist : ydist);
 
-                if (frame >= dist && frame < dist+4) {
-                    int lock = (frame - dist) & 1;
-                    lock = lock ? LOCKED : 0;
-                    c = (c &~ LOCKED) | lock;
-                }
+                if (frame >= dist && frame < dist+4 &&
+                    ((frame - dist) & 1))
+                    todraw(ds, dx, dy) ^= TILE_LOCKED;
             }
+        }
+    }
 
-            if (moved_origin ||
-                index(state, ds->visible, x, y) != c ||
-                index(state, ds->visible, x, y) == 0xFF ||
-                is_src || is_anim || is_cursor) {
-                draw_tile(dr, state, ds, x, y, c,
-                          is_src, (is_anim ? angle : 0.0F), is_cursor);
-                if (is_src || is_anim || is_cursor)
-                    index(state, ds->visible, x, y) = 0xFF;
-                else
-                    index(state, ds->visible, x, y) = c;
+    /*
+     * Now draw any tile that differs from the way it was last drawn.
+     * An exception is that if either the previous _or_ current state
+     * has the TILE_ROTATING bit set, we must draw it regardless,
+     * because it will have rotated to a different angle.q
+     */
+    for (dy = -1; dy < ds->height+1; dy++) {
+        for (dx = -1; dx < ds->width+1; dx++) {
+            int prev = visible(ds, dx, dy);
+            int curr = todraw(ds, dx, dy);
+            if (prev != curr || ((prev | curr) & TILE_ROTATING) != 0) {
+                draw_tile(dr, ds, dx, dy, curr, angle);
+                visible(ds, dx, dy) = curr;
             }
         }
+    }
 
     /*
      * Update the status bar.
      */
     {
-       char statusbuf[256];
+       char statusbuf[256], *p;
        int i, n, n2, a;
+        int complete = FALSE;
+
+        p = statusbuf;
+        *p = '\0';     /* ensure even an empty status string is terminated */
 
-       n = state->width * state->height;
-       for (i = a = n2 = 0; i < n; i++) {
-           if (active[i])
-               a++;
-            if (state->tiles[i] & 0xF)
-                n2++;
+        if (state->used_solve) {
+            p += sprintf(p, "Auto-solved. ");
+            complete = TRUE;
+        } else if (state->completed) {
+            p += sprintf(p, "COMPLETED! ");
+            complete = TRUE;
         }
 
-       sprintf(statusbuf, "%sActive: %d/%d",
-               (state->used_solve ? "Auto-solved. " :
-                state->completed ? "COMPLETED! " : ""), a, n2);
+        /*
+         * Omit the 'Active: n/N' counter completely if the source
+         * tile is a completely empty one, because then the active
+         * count can't help but read '1'.
+         */
+        if (tile(state, ui->cx, ui->cy) & 0xF) {
+            n = state->width * state->height;
+            for (i = a = n2 = 0; i < n; i++) {
+                if (active[i])
+                    a++;
+                if (state->tiles[i] & 0xF)
+                    n2++;
+            }
+
+            /*
+             * Also, if we're displaying a completion indicator and
+             * the game is still in its completed state (i.e. every
+             * tile is active), we might as well omit this too.
+             */
+            if (!complete || a < n2)
+                p += sprintf(p, "Active: %d/%d", a, n2);
+        }
 
        status_bar(dr, statusbuf);
     }
 
     sfree(active);
+    sfree(loops);
 }
 
-static float game_anim_length(game_state *oldstate,
-                             game_state *newstate, int dir, game_ui *ui)
+static float game_anim_length(const game_state *oldstate,
+                              const game_state *newstate, int dir, game_ui *ui)
 {
     int last_rotate_dir;
 
-    /*
-     * Don't animate an auto-solve move.
-     */
-    if ((dir > 0 && newstate->just_used_solve) ||
-       (dir < 0 && oldstate->just_used_solve))
-       return 0.0F;
-
     /*
      * Don't animate if last_rotate_dir is zero.
      */
@@ -2670,8 +3053,8 @@ static float game_anim_length(game_state *oldstate,
     return 0.0F;
 }
 
-static float game_flash_length(game_state *oldstate,
-                              game_state *newstate, int dir, game_ui *ui)
+static float game_flash_length(const game_state *oldstate,
+                               const game_state *newstate, int dir, game_ui *ui)
 {
     /*
      * If the game has just been completed, we display a completion
@@ -2690,17 +3073,17 @@ static float game_flash_length(game_state *oldstate,
     return 0.0F;
 }
 
-static int game_wants_statusbar(void)
+static int game_status(const game_state *state)
 {
-    return TRUE;
+    return state->completed ? +1 : 0;
 }
 
-static int game_timing_state(game_state *state, game_ui *ui)
+static int game_timing_state(const game_state *state, game_ui *ui)
 {
     return TRUE;
 }
 
-static void game_print_size(game_params *params, float *x, float *y)
+static void game_print_size(const game_params *params, float *x, float *y)
 {
     int pw, ph;
 
@@ -2708,8 +3091,8 @@ static void game_print_size(game_params *params, float *x, float *y)
      * I'll use 8mm squares by default.
      */
     game_compute_size(params, 800, &pw, &ph);
-    *x = pw / 100.0;
-    *y = ph / 100.0;
+    *x = pw / 100.0F;
+    *y = ph / 100.0F;
 }
 
 static void draw_diagram(drawing *dr, game_drawstate *ds, int x, int y,
@@ -2758,7 +3141,7 @@ static void draw_diagram(drawing *dr, game_drawstate *ds, int x, int y,
     }
 }
 
-static void game_print(drawing *dr, game_state *state, int tilesize)
+static void game_print(drawing *dr, const game_state *state, int tilesize)
 {
     int w = state->width, h = state->height;
     int ink = print_mono_colour(dr, 0);
@@ -2840,9 +3223,9 @@ static void game_print(drawing *dr, game_state *state, int tilesize)
 #endif
 
 const struct game thegame = {
-    "Net", "games.net",
+    "Net", "games.net", "net",
     default_params,
-    game_fetch_preset,
+    game_fetch_preset, NULL,
     decode_params,
     encode_params,
     free_params,
@@ -2855,7 +3238,7 @@ const struct game thegame = {
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_text_format,
+    FALSE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,
@@ -2870,8 +3253,9 @@ const struct game thegame = {
     game_redraw,
     game_anim_length,
     game_flash_length,
+    game_status,
     TRUE, FALSE, game_print_size, game_print,
-    game_wants_statusbar,
+    TRUE,                             /* wants_statusbar */
     FALSE, game_timing_state,
-    0,                                /* mouse_priorities */
+    0,                                /* flags */
 };