chiark / gitweb /
Fix completion checking in Killer Solo.
[sgt-puzzles.git] / signpost.c
index 5aa738ead8f64872021dbef6296bd99e6c0cca78..aa2e13af9abfa65ce51c0b663aead1a3cb32825d 100644 (file)
@@ -92,7 +92,8 @@ static int whichdiri(game_state *state, int fromi, int toi)
     return whichdir(fromi%w, fromi/w, toi%w, toi/w);
 }
 
-static int ispointing(game_state *state, int fromx, int fromy, int tox, int toy)
+static int ispointing(const game_state *state, int fromx, int fromy,
+                      int tox, int toy)
 {
     int w = state->w, dir = state->dirs[fromy*w+fromx];
 
@@ -119,7 +120,7 @@ static int ispointingi(game_state *state, int fromi, int toi)
 /* Taking the number 'num', work out the gap between it and the next
  * available number up or down (depending on d). Return 1 if the region
  * at (x,y) will fit in that gap, or 0 otherwise. */
-static int move_couldfit(game_state *state, int num, int d, int x, int y)
+static int move_couldfit(const game_state *state, int num, int d, int x, int y)
 {
     int n, gap, i = y*state->w+x, sz;
 
@@ -144,7 +145,7 @@ static int move_couldfit(game_state *state, int num, int d, int x, int y)
     return (sz > gap) ? 0 : 1;
 }
 
-static int isvalidmove(game_state *state, int clever,
+static int isvalidmove(const game_state *state, int clever,
                        int fromx, int fromy, int tox, int toy)
 {
     int w = state->w, from = fromy*w+fromx, to = toy*w+tox;
@@ -159,8 +160,9 @@ static int isvalidmove(game_state *state, int clever,
 
     nfrom = state->nums[from]; nto = state->nums[to];
 
-    /* can't move _from_ the final number, or _to_ the 1. */
-    if (nfrom == state->n || nto == 1)
+    /* can't move _from_ the preset final number, or _to_ the preset 1. */
+    if (((nfrom == state->n) && (state->flags[from] & FLAG_IMMUTABLE)) ||
+        ((nto   == 1)        && (state->flags[to]   & FLAG_IMMUTABLE)))
         return 0;
 
     /* can't create a new connection between cells in the same region
@@ -195,13 +197,13 @@ static void makelink(game_state *state, int from, int to)
     state->prev[to] = from;
 }
 
-static int game_can_format_as_text_now(game_params *params)
+static int game_can_format_as_text_now(const game_params *params)
 {
     if (params->w * params->h >= 100) return 0;
     return 1;
 }
 
-static char *game_text_format(game_state *state)
+static char *game_text_format(const game_state *state)
 {
     int len = state->h * 2 * (4*state->w + 1) + state->h + 2;
     int x, y, i, num, n, set;
@@ -338,7 +340,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 */
@@ -362,7 +364,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 data[256];
 
@@ -375,7 +377,7 @@ static char *encode_params(game_params *params, int full)
     return dupstr(data);
 }
 
-static config_item *game_configure(game_params *params)
+static config_item *game_configure(const game_params *params)
 {
     config_item *ret;
     char buf[80];
@@ -407,7 +409,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);
 
@@ -418,11 +420,14 @@ 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->w < 2 || params->h < 2)
-       return "Width and height must both be at least two";
-
+    if (params->w < 1) return "Width must be at least one";
+    if (params->h < 1) return "Height must be at least one";
+    if (full && params->w == 1 && params->h == 1)
+       /* The UI doesn't let us move these from unsolved to solved,
+        * so we disallow generating (but not playing) them. */
+       return "Width and height cannot both be one";
     return NULL;
 }
 
@@ -460,7 +465,7 @@ static game_state *blank_game(int w, int h)
     return state;
 }
 
-static void dup_game_to(game_state *to, game_state *from)
+static void dup_game_to(game_state *to, const game_state *from)
 {
     to->completed = from->completed;
     to->used_solve = from->used_solve;
@@ -477,7 +482,7 @@ static void dup_game_to(game_state *to, game_state *from)
     memcpy(to->numsi, from->numsi, (to->n+1)*sizeof(int));
 }
 
-static game_state *dup_game(game_state *state)
+static game_state *dup_game(const game_state *state)
 {
     game_state *ret = blank_game(state->w, state->h);
     dup_game_to(ret, state);
@@ -496,7 +501,7 @@ static void free_game(game_state *state)
     sfree(state);
 }
 
-static void unpick_desc(game_params *params, char *desc,
+static void unpick_desc(const game_params *params, const char *desc,
                         game_state **sout, char **mout)
 {
     game_state *state = blank_game(params->w, params->h);
@@ -510,7 +515,7 @@ static void unpick_desc(game_params *params, char *desc,
         }
 
         c = *desc;
-        if (isdigit(c)) {
+        if (isdigit((unsigned char)c)) {
             num = (num*10) + (int)(c-'0');
             if (num > state->n) {
                 msg = "Number too large";
@@ -619,6 +624,7 @@ static int new_game_fill(game_state *state, random_state *rs,
 
     state->dirs[taili] = 0;
     nfilled = 2;
+    assert(state->n > 1);
 
     while (nfilled < state->n) {
         /* Try and expand _from_ headi; keep going if there's only one
@@ -634,6 +640,8 @@ static int new_game_fill(game_state *state, random_state *rs,
             an = cell_adj(state, headi, aidx, adir);
         } while (an == 1);
 
+       if (nfilled == state->n) break;
+
         /* Try and expand _to_ taili; keep going if there's only one
          * place to go to. */
         an = cell_adj(state, taili, aidx, adir);
@@ -749,6 +757,7 @@ static int new_game_strip(game_state *state, random_state *rs)
         copy->flags[j] |= FLAG_IMMUTABLE;
         state->flags[j] |= FLAG_IMMUTABLE;
         debug_state("Copy of state: ", copy);
+        strip_nums(copy);
         if (solve_state(copy) > 0) goto solved;
         assert(check_nums(state, copy, 1));
     }
@@ -789,13 +798,16 @@ done:
     return ret;
 }
 
-static char *new_game_desc(game_params *params, random_state *rs,
+static char *new_game_desc(const game_params *params, random_state *rs,
                           char **aux, int interactive)
 {
     game_state *state = blank_game(params->w, params->h);
     char *ret;
     int headi, taili;
 
+    /* this shouldn't happen (validate_params), but let's play it safe */
+    if (params->w == 1 && params->h == 1) return dupstr("1a");
+
 generate:
     blank_game_into(state);
 
@@ -836,7 +848,7 @@ generate:
     return ret;
 }
 
-static char *validate_desc(game_params *params, char *desc)
+static char *validate_desc(const game_params *params, const char *desc)
 {
     char *ret = NULL;
 
@@ -847,7 +859,7 @@ static char *validate_desc(game_params *params, char *desc)
 /* --- Linked-list and numbers array --- */
 
 /* Assuming numbers are always up-to-date, there are only four possibilities
- * for regions changing:
+ * for regions changing after a single valid move:
  *
  * 1) two differently-coloured regions being combined (the resulting colouring
  *     should be based on the larger of the two regions)
@@ -861,76 +873,46 @@ static char *validate_desc(game_params *params, char *desc)
  * There should never be any complications with regions containing 3 colours
  * being combined, since two of those colours should have been merged on a
  * previous move.
- */
-
-/* New algorithm for working out numbering:
  *
- * At start, only remove numbers from cells with neither prev nor next.
- * Search for all cells with !prev && next (head of chain); for each one:
-   * Search the group for a 'real' number: if we find one the num. for
-      the head of the chain is trivial.
-   * Otherwise, if we _don't_ have a number already:
-     * If head->next has a number, that number is the one we should use
-     * Otherwise pick the smallest unused colour set.
-   * and if we _do_ have a number already:
-     * Work out the size of this group (the dsf must already have been set up)
-     * Start enumerating through the group counting squares that have the
-        same colouring as us
-     * If we reach a square with a different colour, work out which set is
-        bigger (ncol1 vs ncol2 == sz-ncol1), and use that colour
-     * If we reached a square with no colour (or the end of the group, which
-        would be weird under the circumstances) just keep the existing colour.
+ * Most of the complications are in ensuring we don't accidentally set two
+ * regions with the same colour (e.g. if a region was split). If this happens
+ * we always try and give the largest original portion the original colour.
  */
 
 #define COLOUR(a) ((a) / (state->n+1))
 #define START(c) ((c) * (state->n+1))
 
-static int lowest_start(game_state *state, int *scratch)
-{
-    int i, c;
-
-    /* Fill in 'scratch' array with the currently-used colours... */
-    memset(scratch, 0, state->n * sizeof(int));
-    for (i = 0; i < state->n; i++) {
-        if (state->nums[i] != 0)
-            scratch[COLOUR(state->nums[i])] = 1;
-    }
-    /* ... and return the first one that was unused. */
-    for (c = 1; c < state->n; c++) { /* NB start at 1 */
-        if (scratch[c] == 0)
-            return START(c);
-    }
-    assert(!"shouldn't get here");
-    return -1; /* suyb */
-}
-
-static int used_colour(game_state *state, int i, int start)
-{
-    int j;
-    for (j = 0; j < i; j++) {
-        if (state->nums[j] == start)
-            return 1;
-    }
-    return 0;
-}
+struct head_meta {
+    int i;      /* position */
+    int sz;     /* size of region */
+    int start;  /* region start number preferred, or 0 if !preference */
+    int preference; /* 0 if we have no preference (and should just pick one) */
+    const char *why;
+};
 
-static int head_number(game_state *state, int i, int *scratch)
+static void head_number(game_state *state, int i, struct head_meta *head)
 {
-    int off = 0, start = -1, ss, j = i, c, n, sz;
-    const char *why = NULL;
+    int off = 0, ss, j = i, c, n, sz;
 
+    /* Insist we really were passed the head of a chain. */
     assert(state->prev[i] == -1 && state->next[i] != -1);
 
+    head->i = i;
+    head->sz = dsf_size(state->dsf, i);
+    head->why = NULL;
+
     /* Search through this chain looking for real numbers, checking that
      * they match up (if there are more than one). */
+    head->preference = 0;
     while (j != -1) {
         if (state->flags[j] & FLAG_IMMUTABLE) {
             ss = state->nums[j] - off;
-            if (start == -1) {
-                start = ss;
-                why = "contains cell with immutable number";
-            } else if (start != ss) {
-                debug(("head_number: chain with non-sequential numbers."));
+            if (!head->preference) {
+                head->start = ss;
+                head->preference = 1;
+                head->why = "contains cell with immutable number";
+            } else if (head->start != ss) {
+                debug(("head_number: chain with non-sequential numbers!"));
                 state->impossible = 1;
             }
         }
@@ -938,17 +920,22 @@ static int head_number(game_state *state, int i, int *scratch)
         j = state->next[j];
         assert(j != i); /* we have created a loop, obviously wrong */
     }
-    if (start != -1) goto found;
-
-    if (state->nums[i] == 0) {
-        if (state->nums[state->next[i]] != 0) {
-            /* make sure we start at a 0 offset. */
-            start = START(COLOUR(state->nums[state->next[i]]));
-            why = "adding blank cell to head of numbered region";
-        } else {
-            start = lowest_start(state, scratch);
-            why = "lowest available colour group";
-        }
+    if (head->preference) goto done;
+
+    if (state->nums[i] == 0 && state->nums[state->next[i]] > state->n) {
+        /* (probably) empty cell onto the head of a coloured region:
+         * make sure we start at a 0 offset. */
+        head->start = START(COLOUR(state->nums[state->next[i]]));
+        head->preference = 1;
+        head->why = "adding blank cell to head of numbered region";
+    } else if (state->nums[i] <= state->n) {
+        /* if we're 0 we're probably just blank -- but even if we're a
+         * (real) numbered region, we don't have an immutable number
+         * in it (any more) otherwise it'd have been caught above, so
+         * reassign the colour. */
+        head->start = 0;
+        head->preference = 0;
+        head->why = "lowest available colour group";
     } else {
         c = COLOUR(state->nums[i]);
         n = 1;
@@ -956,42 +943,51 @@ static int head_number(game_state *state, int i, int *scratch)
         j = i;
         while (state->next[j] != -1) {
             j = state->next[j];
-            if (state->nums[j] == 0) {
-                start = START(c);
-                why = "adding blank cell to end of numbered region";
-                break;
+            if (state->nums[j] == 0 && state->next[j] == -1) {
+                head->start = START(c);
+                head->preference = 1;
+                head->why = "adding blank cell to end of numbered region";
+                goto done;
             }
             if (COLOUR(state->nums[j]) == c)
                 n++;
             else {
                 int start_alternate = START(COLOUR(state->nums[j]));
-                if (n < (sz - n) && !used_colour(state, i, start_alternate)) {
-                    start = start_alternate;
-                    why = "joining two coloured regions, swapping to larger colour";
+                if (n < (sz - n)) {
+                    head->start = start_alternate;
+                    head->preference = 1;
+                    head->why = "joining two coloured regions, swapping to larger colour";
                 } else {
-                    start = START(c);
-                    why = "joining two coloured regions, taking largest";
+                    head->start = START(c);
+                    head->preference = 1;
+                    head->why = "joining two coloured regions, taking largest";
                 }
-                break;
+                goto done;
             }
         }
         /* If we got here then we may have split a region into
          * two; make sure we don't assign a colour we've already used. */
-        if (start == -1) {
-            start = (c == 0) ? lowest_start(state, scratch) : START(c);
-            why = "got to end of coloured region";
-        }
-        if (used_colour(state, i, start)) {
-            start = lowest_start(state, scratch);
-            why = "split region in two, lowest available colour group";
+        if (c == 0) {
+            /* not convinced this shouldn't be an assertion failure here. */
+            head->start = 0;
+            head->preference = 0;
+        } else {
+            head->start = START(c);
+            head->preference = 1;
         }
+        head->why = "got to end of coloured region";
     }
 
-found:
-    assert(start != -1 && why != NULL);
-    debug(("Chain at (%d,%d) numbered at %d: %s.",
-           i%state->w, i/state->w, start, why));
-    return start;
+done:
+    assert(head->why != NULL);
+    if (head->preference)
+        debug(("Chain at (%d,%d) numbered for preference at %d (colour %d): %s.",
+               head->i%state->w, head->i/state->w,
+               head->start, COLOUR(head->start), head->why));
+    else
+        debug(("Chain at (%d,%d) using next available colour: %s.",
+               head->i%state->w, head->i/state->w,
+               head->why));
 }
 
 #if 0
@@ -1030,19 +1026,59 @@ static void connect_numbers(game_state *state)
     }
 }
 
-static void update_numbers(game_state *state)
+static int compare_heads(const void *a, const void *b)
 {
-    int i, j, nnum;
-    int *scratch = snewn(state->n, int);
+    struct head_meta *ha = (struct head_meta *)a;
+    struct head_meta *hb = (struct head_meta *)b;
 
-    for (i = 0; i < state->n; i++) {
-        assert(state->nums[i] >= 0);
-        state->numsi[i] = -1;
+    /* Heads with preferred colours first... */
+    if (ha->preference && !hb->preference) return -1;
+    if (hb->preference && !ha->preference) return 1;
+
+    /* ...then heads with low colours first... */
+    if (ha->start < hb->start) return -1;
+    if (ha->start > hb->start) return 1;
+
+    /* ... then large regions first... */
+    if (ha->sz > hb->sz) return -1;
+    if (ha->sz < hb->sz) return 1;
+
+    /* ... then position. */
+    if (ha->i > hb->i) return -1;
+    if (ha->i < hb->i) return 1;
+
+    return 0;
+}
+
+static int lowest_start(game_state *state, struct head_meta *heads, int nheads)
+{
+    int n, c;
+
+    /* NB start at 1: colour 0 is real numbers */
+    for (c = 1; c < state->n; c++) {
+        for (n = 0; n < nheads; n++) {
+            if (COLOUR(heads[n].start) == c)
+                goto used;
+        }
+        return c;
+used:
+        ;
     }
+    assert(!"No available colours!");
+    return 0;
+}
+
+static void update_numbers(game_state *state)
+{
+    int i, j, n, nnum, nheads;
+    struct head_meta *heads = snewn(state->n, struct head_meta);
+
+    for (n = 0; n < state->n; n++)
+        state->numsi[n] = -1;
 
     for (i = 0; i < state->n; i++) {
         if (state->flags[i] & FLAG_IMMUTABLE) {
-            assert(state->nums[i] >= 0);
+            assert(state->nums[i] > 0);
             assert(state->nums[i] <= state->n);
             state->numsi[state->nums[i]] = i;
         }
@@ -1051,23 +1087,63 @@ static void update_numbers(game_state *state)
     }
     connect_numbers(state);
 
+    /* Construct an array of the heads of all current regions, together
+     * with their preferred colours. */
+    nheads = 0;
     for (i = 0; i < state->n; i++) {
         /* Look for a cell that is the start of a chain
          * (has a next but no prev). */
         if (state->prev[i] != -1 || state->next[i] == -1) continue;
 
-        nnum = head_number(state, i, scratch);
-        j = i;
+        head_number(state, i, &heads[nheads++]);
+    }
+
+    /* Sort that array:
+     * - heads with preferred colours first, then
+     * - heads with low colours first, then
+     * - large regions first
+     */
+    qsort(heads, nheads, sizeof(struct head_meta), compare_heads);
+
+    /* Remove duplicate-coloured regions. */
+    for (n = nheads-1; n >= 0; n--) { /* order is important! */
+        if ((n != 0) && (heads[n].start == heads[n-1].start)) {
+            /* We have a duplicate-coloured region: since we're
+             * sorted in size order and this is not the first
+             * of its colour it's not the largest: recolour it. */
+            heads[n].start = START(lowest_start(state, heads, nheads));
+            heads[n].preference = -1; /* '-1' means 'was duplicate' */
+        }
+        else if (!heads[n].preference) {
+            assert(heads[n].start == 0);
+            heads[n].start = START(lowest_start(state, heads, nheads));
+        }
+    }
+
+    debug(("Region colouring after duplicate removal:"));
+
+    for (n = 0; n < nheads; n++) {
+        debug(("  Chain at (%d,%d) sz %d numbered at %d (colour %d): %s%s",
+               heads[n].i % state->w, heads[n].i / state->w, heads[n].sz,
+               heads[n].start, COLOUR(heads[n].start), heads[n].why,
+               heads[n].preference == 0 ? " (next available)" :
+               heads[n].preference < 0 ? " (duplicate, next available)" : ""));
+
+        nnum = heads[n].start;
+        j = heads[n].i;
         while (j != -1) {
-            if (nnum > 0 && nnum <= state->n)
-                state->numsi[nnum] = j;
-            state->nums[j] = nnum++;
+            if (!(state->flags[j] & FLAG_IMMUTABLE)) {
+                if (nnum > 0 && nnum <= state->n)
+                    state->numsi[nnum] = j;
+                state->nums[j] = nnum;
+            }
+            nnum++;
             j = state->next[j];
-            assert(j != i); /* loop?! */
+            assert(j != heads[n].i); /* loop?! */
         }
     }
     /*debug_numbers(state);*/
-    sfree(scratch);
+    sfree(heads);
 }
 
 static int check_completion(game_state *state, int mark_errors)
@@ -1120,10 +1196,22 @@ static int check_completion(game_state *state, int mark_errors)
         }
     }
 
+    /* Search and mark numbers less than 0, or 0 with links. */
+    for (n = 1; n < state->n; n++) {
+        if ((state->nums[n] < 0) ||
+            (state->nums[n] == 0 &&
+             (state->next[n] != -1 || state->prev[n] != -1))) {
+            error = 1;
+            if (mark_errors)
+                state->flags[n] |= FLAG_ERROR;
+        }
+    }
+
     if (error) return 0;
     return complete;
 }
-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 = NULL;
 
@@ -1253,8 +1341,8 @@ static int solve_state(game_state *state)
     return ret;
 }
 
-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)
 {
     game_state *tosolve;
     char *ret = NULL;
@@ -1292,7 +1380,7 @@ struct game_ui {
     int dx, dy;         /* pixel coords of drag posn */
 };
 
-static game_ui *new_ui(game_state *state)
+static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
 
@@ -1312,17 +1400,17 @@ static void free_ui(game_ui *ui)
     sfree(ui);
 }
 
-static char *encode_ui(game_ui *ui)
+static char *encode_ui(const game_ui *ui)
 {
     return NULL;
 }
 
-static void decode_ui(game_ui *ui, char *encoding)
+static void decode_ui(game_ui *ui, const char *encoding)
 {
 }
 
-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)
 {
     if (!oldstate->completed && newstate->completed)
         ui->cshow = ui->dragging = 0;
@@ -1339,8 +1427,9 @@ struct game_drawstate {
     blitter *dragb;
 };
 
-static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
-                           int mx, int my, int button)
+static char *interpret_move(const game_state *state, game_ui *ui,
+                            const game_drawstate *ds,
+                            int mx, int my, int button)
 {
     int x = FROMCOORD(mx), y = FROMCOORD(my), w = state->w;
     char buf[80];
@@ -1386,10 +1475,14 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
 
         if (button == LEFT_BUTTON) {
             /* disallow dragging from the final number. */
-            if (state->nums[y*w+x] == state->n) return NULL;
+            if ((state->nums[y*w+x] == state->n) &&
+                (state->flags[y*w+x] & FLAG_IMMUTABLE))
+                return NULL;
         } else if (button == RIGHT_BUTTON) {
             /* disallow dragging to the first number. */
-            if (state->nums[y*w+x] == 1) return NULL;
+            if ((state->nums[y*w+x] == 1) &&
+                (state->flags[y*w+x] & FLAG_IMMUTABLE))
+                return NULL;
         }
 
         ui->dragging = TRUE;
@@ -1413,7 +1506,7 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
             if (state->prev[si] == -1 && state->next[si] == -1)
                 return "";
             sprintf(buf, "%c%d,%d",
-                    ui->drag_is_from ? 'C' : 'X', ui->sx, ui->sy);
+                    (int)(ui->drag_is_from ? 'C' : 'X'), ui->sx, ui->sy);
             return dupstr(buf);
         }
 
@@ -1432,7 +1525,7 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
         if (state->prev[si] == -1 && state->next[si] == -1)
             return "";
         sprintf(buf, "%c%d,%d",
-                (button == 'x') ? 'C' : 'X', ui->cx, ui->cy);
+                (int)((button == 'x') ? 'C' : 'X'), ui->cx, ui->cy);
         return dupstr(buf);
     }
 
@@ -1456,7 +1549,7 @@ static void unlink_cell(game_state *state, int si)
     }
 }
 
-static game_state *execute_move(game_state *state, char *move)
+static game_state *execute_move(const game_state *state, const char *move)
 {
     game_state *ret = NULL;
     int sx, sy, ex, ey, si, ei, w = state->w;
@@ -1492,6 +1585,8 @@ static game_state *execute_move(game_state *state, char *move)
         si = sy*w+sx; ei = ey*w+ex;
         makelink(ret, si, ei);
     } else if (sscanf(move, "%c%d,%d", &c, &sx, &sy) == 3) {
+        int sset;
+
         if (c != 'C' && c != 'X') return NULL;
         if (!INGRID(state, sx, sy)) return NULL;
         si = sy*w+sx;
@@ -1500,11 +1595,12 @@ static game_state *execute_move(game_state *state, char *move)
 
         ret = dup_game(state);
 
-        if (c == 'C') {
+        sset = state->nums[si] / (state->n+1);
+        if (c == 'C' || (c == 'X' && sset == 0)) {
             /* Unlink the single cell we dragged from the board. */
             unlink_cell(ret, si);
         } else {
-            int i, set, sset = state->nums[si] / (state->n+1);
+            int i, set;
             for (i = 0; i < state->n; i++) {
                 /* Unlink all cells in the same set as the one we dragged
                  * from the board. */
@@ -1532,8 +1628,8 @@ static game_state *execute_move(game_state *state, char *move)
  * Drawing routines.
  */
 
-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)
 {
     /* Ick: fake up `ds->tilesize' for macro expansion purposes */
     struct { int tilesize, order; } ads, *ds = &ads;
@@ -1544,7 +1640,7 @@ static void game_compute_size(game_params *params, int tilesize,
 }
 
 static void game_set_size(drawing *dr, game_drawstate *ds,
-                         game_params *params, int tilesize)
+                          const game_params *params, int tilesize)
 {
     ds->tilesize = tilesize;
     assert(TILE_SIZE > 0);
@@ -1623,7 +1719,7 @@ static float *game_colours(frontend *fe, int *ncolours)
     return ret;
 }
 
-static game_drawstate *game_new_drawstate(drawing *dr, game_state *state)
+static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
 {
     struct game_drawstate *ds = snew(struct game_drawstate);
     int i;
@@ -1729,8 +1825,8 @@ static int num2col(game_drawstate *ds, int num)
 {
     int set = num / (ds->n+1);
 
-    if (num <= 0) return COL_BACKGROUND;
-    return COL_B0 + (set % 16);
+    if (num <= 0 || set == 0) return COL_B0;
+    return COL_B0 + 1 + ((set-1) % 15);
 }
 
 #define ARROW_HALFSZ (7 * TILE_SIZE / 32)
@@ -1750,8 +1846,19 @@ static void tile_redraw(drawing *dr, game_drawstate *ds, int tx, int ty,
     int cb = TILE_SIZE / 16, textsz;
     char buf[20];
     int arrowcol, sarrowcol, setcol, textcol;
-    int n = num % (ds->n+1), set = num / (ds->n+1);
-    int acx, acy, asz;
+    int acx, acy, asz, empty = 0;
+
+    if (num == 0 && !(f & F_ARROW_POINT) && !(f & F_ARROW_INPOINT)) {
+        empty = 1;
+        /*
+         * We don't display text in empty cells: typically these are
+         * signified by num=0. However, in some cases a cell could
+         * have had the number 0 assigned to it if the user made an
+         * error (e.g. tried to connect a chain of length 5 to the
+         * immutable number 4) so we _do_ display the 0 if the cell
+         * has a link in or a link out.
+         */
+    }
 
     /* Calculate colours. */
 
@@ -1763,7 +1870,7 @@ static void tile_redraw(drawing *dr, game_drawstate *ds, int tx, int ty,
        setcol = sarrowcol = -1;       /* placate optimiser */
     } else {
 
-       setcol = num2col(ds, num);
+       setcol = empty ? COL_BACKGROUND : num2col(ds, num);
 
 #define dim(fg,bg) ( \
       (bg)==COL_BACKGROUND ? COL_ARROW_BG_DIM : \
@@ -1785,7 +1892,7 @@ static void tile_redraw(drawing *dr, game_drawstate *ds, int tx, int ty,
        else if (f & F_ARROW_POINT) arrowcol = mid(COL_ARROW, setcol);
        else arrowcol = COL_ARROW;
 
-       if (f & (F_ERROR)) textcol = COL_ERROR;
+       if ((f & F_ERROR) && !(f & F_IMMUTABLE)) textcol = COL_ERROR;
        else {
            if (f & F_IMMUTABLE) textcol = COL_NUMBER_SET;
            else textcol = COL_NUMBER;
@@ -1833,19 +1940,33 @@ static void tile_redraw(drawing *dr, game_drawstate *ds, int tx, int ty,
 
     /* Draw text (number or set). */
 
-    if (num != 0) {
-        assert(num > 0);
-        if (set == 0) {
-            sprintf(buf, "%d", n);
+    if (!empty) {
+        int set = (num <= 0) ? 0 : num / (ds->n+1);
+
+        char *p = buf;
+        if (set == 0 || num <= 0) {
+            sprintf(buf, "%d", num);
         } else {
-            if (n == 0)
-                sprintf(buf, "%c", (int)(set+'a'-1));
-            else
-                sprintf(buf, "%c+%d", (int)(set+'a'-1), n);
+            int n = num % (ds->n+1);
+            p += sizeof(buf) - 1;
+
+            if (n != 0) {
+                sprintf(buf, "+%d", n);  /* Just to get the length... */
+                p -= strlen(buf);
+                sprintf(p, "+%d", n);
+            } else {
+                *p = '\0';
+            }
+            do {
+                set--;
+                p--;
+                *p = (char)((set % 26)+'a');
+                set /= 26;
+            } while (set);
         }
-        textsz = min(2*asz, (TILE_SIZE - 2 * cb) / (int)strlen(buf));
+        textsz = min(2*asz, (TILE_SIZE - 2 * cb) / (int)strlen(p));
         draw_text(dr, tx + cb, ty + TILE_SIZE/4, FONT_VARIABLE, textsz,
-                  ALIGN_VCENTRE | ALIGN_HLEFT, textcol, buf);
+                  ALIGN_VCENTRE | ALIGN_HLEFT, textcol, p);
     }
 
     if (print_ink < 0) {
@@ -1855,7 +1976,8 @@ static void tile_redraw(drawing *dr, game_drawstate *ds, int tx, int ty,
 }
 
 static void draw_drag_indicator(drawing *dr, game_drawstate *ds,
-                                game_state *state, game_ui *ui, int validdrag)
+                                const game_state *state, const game_ui *ui,
+                                int validdrag)
 {
     int dir, w = ds->w, acol = COL_ARROW;
     int fx = FROMCOORD(ui->dx), fy = FROMCOORD(ui->dy);
@@ -1870,7 +1992,7 @@ static void draw_drag_indicator(drawing *dr, game_drawstate *ds,
         /* Draw an arrow pointing away from/towards the origin cell. */
         int ox = COORD(ui->sx) + TILE_SIZE/2, oy = COORD(ui->sy) + TILE_SIZE/2;
         double tana, offset;
-        double xdiff = fabs(ox - ui->dx), ydiff = fabs(oy - ui->dy);
+        double xdiff = abs(ox - ui->dx), ydiff = abs(oy - ui->dy);
 
         if (xdiff == 0) {
             ang = (oy > ui->dy) ? 0.0F : PI;
@@ -1899,9 +2021,10 @@ static void draw_drag_indicator(drawing *dr, game_drawstate *ds,
     draw_arrow(dr, ui->dx, ui->dy, ARROW_HALFSZ, ang, acol, acol);
 }
 
-static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
-                       game_state *state, int dir, game_ui *ui,
-                       float animtime, float flashtime)
+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 x, y, i, w = ds->w, dirp, force = 0;
     unsigned int f;
@@ -1986,11 +2109,33 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
             if (state->nums[i] != ds->nums[i] ||
                 f != ds->f[i] || dirp != ds->dirp[i] ||
                 force || !ds->started) {
+                int sign;
+                {
+                    /*
+                     * Trivial and foolish configurable option done on
+                     * purest whim. With this option enabled, the
+                     * victory flash is done by rotating each square
+                     * in the opposite direction from its immediate
+                     * neighbours, so that they behave like a field of
+                     * interlocking gears. With it disabled, they all
+                     * rotate in the same direction. Choose for
+                     * yourself which is more brain-twisting :-)
+                     */
+                    static int gear_mode = -1;
+                    if (gear_mode < 0) {
+                        char *env = getenv("SIGNPOST_GEARS");
+                        gear_mode = (env && (env[0] == 'y' || env[0] == 'Y'));
+                    }
+                    if (gear_mode)
+                        sign = 1 - 2 * ((x ^ y) & 1);
+                    else
+                        sign = 1;
+                }
                 tile_redraw(dr, ds,
                             BORDER + x * TILE_SIZE,
                             BORDER + y * TILE_SIZE,
                             state->dirs[i], dirp, state->nums[i], f,
-                            angle_offset, -1);
+                            sign * angle_offset, -1);
                 ds->nums[i] = state->nums[i];
                 ds->f[i] = f;
                 ds->dirp[i] = dirp;
@@ -2009,14 +2154,14 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
     if (!ds->started) ds->started = TRUE;
 }
 
-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)
 {
     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 (!oldstate->completed &&
         newstate->completed && !newstate->used_solve)
@@ -2025,12 +2170,17 @@ static float game_flash_length(game_state *oldstate, game_state *newstate,
         return 0.0F;
 }
 
-static int game_timing_state(game_state *state, game_ui *ui)
+static int game_status(const game_state *state)
+{
+    return state->completed ? +1 : 0;
+}
+
+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;
 
@@ -2039,7 +2189,7 @@ static void game_print_size(game_params *params, float *x, float *y)
     *y = ph / 100.0F;
 }
 
-static void game_print(drawing *dr, game_state *state, int tilesize)
+static void game_print(drawing *dr, const game_state *state, int tilesize)
 {
     int ink = print_mono_colour(dr, 0);
     int x, y;
@@ -2106,10 +2256,11 @@ const struct game thegame = {
     game_redraw,
     game_anim_length,
     game_flash_length,
+    game_status,
     TRUE, FALSE, game_print_size, game_print,
     FALSE,                            /* wants_statusbar */
     FALSE, game_timing_state,
-    REQUIRE_RBUTTON | REQUIRE_NUMPAD,  /* flags */
+    REQUIRE_RBUTTON,                  /* flags */
 };
 
 #ifdef STANDALONE_SOLVER
@@ -2252,7 +2403,7 @@ int main(int argc, const char *argv[])
         }
     }
 
-    sprintf(newseed, "%lu", time(NULL));
+    sprintf(newseed, "%lu", (unsigned long) time(NULL));
     seedstr = dupstr(newseed);
 
     if (id || !stdin_desc) {