New puzzle! 'Unruly', contributed by Lennard Sprong, is an
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 7 Oct 2012 10:18:33 +0000 (10:18 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 7 Oct 2012 10:18:33 +0000 (10:18 +0000)
implementation of a puzzle usually called 'Tohu wa Vohu'.

git-svn-id: svn://svn.tartarus.org/sgt/puzzles@9680 cda61777-01e9-0310-a592-d414129be87e

LICENCE
icons/Makefile
icons/unruly.sav [new file with mode: 0644]
puzzles.but
unruly.R [new file with mode: 0644]
unruly.c [new file with mode: 0644]

diff --git a/LICENCE b/LICENCE
index ad8c6f0..dc1b67e 100644 (file)
--- a/LICENCE
+++ b/LICENCE
@@ -2,7 +2,7 @@ This software is copyright (c) 2004-2012 Simon Tatham.
 
 Portions copyright Richard Boulton, James Harvey, Mike Pinna, Jonas
 Kölker, Dariusz Olszewski, Michael Schierl, Lambros Lambrou, Bernd
-Schmidt and Steffen Bauer.
+Schmidt, Steffen Bauer and Lennard Sprong.
 
 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation files
index 2d4b65b..6dcc33d 100644 (file)
@@ -4,7 +4,7 @@ PUZZLES = blackbox bridges cube dominosa fifteen filling flip galaxies  \
          guess inertia keen lightup loopy magnets map mines net        \
          netslide pattern pearl pegs range rect samegame signpost      \
          singles sixteen slant solo tents towers twiddle undead        \
-         unequal untangle
+         unequal unruly untangle
 
 BASE = $(patsubst %,%-base.png,$(PUZZLES))
 WEB = $(patsubst %,%-web.png,$(PUZZLES))
diff --git a/icons/unruly.sav b/icons/unruly.sav
new file mode 100644 (file)
index 0000000..0f7ca1b
--- /dev/null
@@ -0,0 +1,22 @@
+SAVEFILE:41:Simon Tatham's Portable Puzzle Collection
+VERSION :1:1
+GAME    :6:Unruly
+PARAMS  :5:6x6de
+CPARAMS :5:6x6de
+DESC    :10:faCADAJeBd
+NSTATES :2:15
+STATEPOS:2:15
+MOVE    :6:P0,1,2
+MOVE    :6:P0,4,2
+MOVE    :6:P0,3,3
+MOVE    :6:P0,3,0
+MOVE    :6:P0,5,1
+MOVE    :6:P0,2,1
+MOVE    :6:P1,4,0
+MOVE    :6:P1,1,1
+MOVE    :6:P1,5,2
+MOVE    :6:P0,0,2
+MOVE    :6:P1,0,3
+MOVE    :6:P1,0,0
+MOVE    :6:P1,0,4
+MOVE    :6:P0,2,4
index 51055f2..bd53e9b 100644 (file)
@@ -3052,13 +3052,60 @@ These parameters are available from the \q{Custom...} option on the
 
 \dd Controls the difficulty of the generated puzzle.
 
+\C{unruly} \i{Unruly}
+
+\cfg{winhelp-topic}{games.unruly}
+
+You are given a grid of squares, which you must colour either black or
+white. Some squares are provided as clues; the rest are left for you
+to fill in. Each row and column must contain the same number of black
+and white squares, and no row or column may contain three consecutive
+squares of the same colour.
+
+This puzzle type was invented by Adolfo Zanellati, under the name
+\q{Tohu wa Vohu}. See \k{janko-unruly} for more details.
+
+Unruly was contributed to this collection by Lennard Sprong.
+
+\B{janko-unruly}
+\W{http://www.janko.at/Raetsel/Tohu-Wa-Vohu/index.htm}\cw{http://www.janko.at/Raetsel/Tohu-Wa-Vohu/index.htm}
+
+\H{unruly-controls} \I{controls, for Unruly}Unruly controls
+
+To play Unruly, click the mouse in a square to change its colour.
+Left-clicking an empty square will turn it black, and right-clicking
+will turn it white. Keep clicking the same button to cycle through the
+three possible states for the square. If you middle-click in a square
+it will be reset to empty.
+
+You can also use the cursor keys to move around the grid. Pressing the
+return or space keys will turn an empty square black or white
+respectively (and then cycle the colours in the same way as the mouse
+buttons), and pressing Backspace will reset a square to empty.
+
+(All the actions described in \k{common-actions} are also available.)
+
+\H{unruly-parameters} \I{parameters, for Unruly}Unruly parameters
+
+These parameters are available from the \q{Custom...} option on the
+\q{Type} menu.
+
+\dt \e{Width}, \e{Height}
+
+\dd Size of grid in squares. (Note that the rules of the game require
+both the width and height to be even numbers.)
+
+\dt \e{Difficulty}
+
+\dd Controls the difficulty of the generated puzzle.
+
 \A{licence} \I{MIT licence}\ii{Licence}
 
 This software is \i{copyright} 2004-2012 Simon Tatham.
 
 Portions copyright Richard Boulton, James Harvey, Mike Pinna, Jonas
 K\u00F6{oe}lker, Dariusz Olszewski, Michael Schierl, Lambros
-Lambrou, Bernd Schmidt and Steffen Bauer.
+Lambrou, Bernd Schmidt, Steffen Bauer and Lennard Sprong.
 
 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation files
diff --git a/unruly.R b/unruly.R
new file mode 100644 (file)
index 0000000..b69a144
--- /dev/null
+++ b/unruly.R
@@ -0,0 +1,21 @@
+# -*- makefile -*-
+
+unruly : [X] GTK COMMON unruly unruly-icon|no-icon
+unruly : [G] WINDOWS COMMON unruly unruly.res|noicon.res
+
+unrulysolver : [U] unruly[STANDALONE_SOLVER] STANDALONE
+unrulysolver : [C] unruly[STANDALONE_SOLVER] STANDALONE
+
+ALL += unruly[COMBINED]
+
+!begin gtk
+GAMES += unruly
+!end
+
+!begin >list.c
+    A(unruly) \
+!end
+
+!begin >wingames.lst
+unruly.exe:Unruly
+!end
diff --git a/unruly.c b/unruly.c
new file mode 100644 (file)
index 0000000..c4e9ae0
--- /dev/null
+++ b/unruly.c
@@ -0,0 +1,1868 @@
+/*
+ * unruly.c: Implementation for Binary Puzzles.
+ * (C) 2012 Lennard Sprong
+ * Created for Simon Tatham's Portable Puzzle Collection
+ * See LICENCE for licence details
+ *
+ * Objective of the game: Fill the grid with zeros and ones, with the
+ * following rules:
+ * - There can't be a run of three or more equal numbers.
+ * - Each row and column contains an equal amount of zeros and ones.
+ *
+ * This puzzle type is known under several names, including
+ * Tohu-Wa-Vohu, One and Two and Binairo.
+ *
+ * Some variants include an extra constraint, stating that no two rows or two
+ * columns may contain the same exact sequence of zeros and ones.
+ * This rule is rarely used, so it has been discarded for this implementation.
+ *
+ * More information:
+ * http://www.janko.at/Raetsel/Tohu-Wa-Vohu/index.htm
+ */
+
+/*
+ * Possible future improvements:
+ *
+ * More solver cleverness
+ *
+ *  - a counting-based deduction in which you find groups of squares
+ *    which must each contain at least one of a given colour, plus
+ *    other squares which are already known to be that colour, and see
+ *    if you have any squares left over when you've worked out where
+ *    they all have to be. This is a generalisation of the current
+ *    check_near_complete: where that only covers rows with three
+ *    unfilled squares, this would handle more, such as
+ *        0 . . 1 0 1 . . 0 .
+ *    in which each of the two-square gaps must contain a 0, and there
+ *    are three 0s placed, and that means the rightmost square can't
+ *    be a 0.
+ *
+ *  - an 'Unreasonable' difficulty level, supporting recursion and
+ *    backtracking.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <ctype.h>
+#include <math.h>
+
+#include "puzzles.h"
+
+#ifdef STANDALONE_SOLVER
+int solver_verbose = FALSE;
+#endif
+
+enum {
+    COL_BACKGROUND,
+    COL_GRID,
+    COL_EMPTY,
+    /*
+     * When editing this enum, maintain the invariants
+     *   COL_n_HIGHLIGHT = COL_n + 1
+     *   COL_n_LOWLIGHT = COL_n + 2
+     */
+    COL_0,
+    COL_0_HIGHLIGHT,
+    COL_0_LOWLIGHT,
+    COL_1,
+    COL_1_HIGHLIGHT,
+    COL_1_LOWLIGHT,
+    COL_CURSOR,
+    COL_ERROR,
+    NCOLOURS
+};
+
+struct game_params {
+    int w2, h2;        /* full grid width and height respectively */
+    int diff;
+};
+#define DIFFLIST(A)                             \
+    A(EASY,Easy, e)                             \
+    A(NORMAL,Normal, n)                         \
+
+#define ENUM(upper,title,lower) DIFF_ ## upper,
+#define TITLE(upper,title,lower) #title,
+#define ENCODE(upper,title,lower) #lower
+#define CONFIG(upper,title,lower) ":" #title
+enum { DIFFLIST(ENUM) DIFFCOUNT };
+static char const *const unruly_diffnames[] = { DIFFLIST(TITLE) };
+
+static char const unruly_diffchars[] = DIFFLIST(ENCODE);
+#define DIFFCONFIG DIFFLIST(CONFIG)
+
+const static struct game_params unruly_presets[] = {
+    { 8,  8, DIFF_EASY},
+    { 8,  8, DIFF_NORMAL},
+    {10, 10, DIFF_EASY},
+    {10, 10, DIFF_NORMAL},
+    {14, 14, DIFF_EASY},
+    {14, 14, DIFF_NORMAL}
+};
+
+#define DEFAULT_PRESET 0
+
+enum {
+    EMPTY,
+    N_ONE,
+    N_ZERO,
+    BOGUS
+};
+
+#define FE_HOR_ROW_LEFT   0x001
+#define FE_HOR_ROW_MID    0x003
+#define FE_HOR_ROW_RIGHT  0x002
+
+#define FE_VER_ROW_TOP    0x004
+#define FE_VER_ROW_MID    0x00C
+#define FE_VER_ROW_BOTTOM 0x008
+
+#define FE_COUNT          0x010
+
+#define FF_ONE            0x020
+#define FF_ZERO           0x040
+#define FF_CURSOR         0x080
+
+#define FF_FLASH1         0x100
+#define FF_FLASH2         0x200
+#define FF_IMMUTABLE      0x400
+
+struct game_state {
+    int w2, h2;
+    char *grid;
+    unsigned char *immutable;
+
+    int completed, cheated;
+};
+
+static game_params *default_params(void)
+{
+    game_params *ret = snew(game_params);
+
+    *ret = unruly_presets[DEFAULT_PRESET];        /* structure copy */
+
+    return ret;
+}
+
+static int game_fetch_preset(int i, char **name, game_params **params)
+{
+    game_params *ret;
+    char buf[80];
+
+    if (i < 0 || i >= lenof(unruly_presets))
+        return FALSE;
+
+    ret = snew(game_params);
+    *ret = unruly_presets[i];     /* structure copy */
+
+    sprintf(buf, "%dx%d %s", ret->w2, ret->h2, unruly_diffnames[ret->diff]);
+
+    *name = dupstr(buf);
+    *params = ret;
+    return TRUE;
+}
+
+static void free_params(game_params *params)
+{
+    sfree(params);
+}
+
+static game_params *dup_params(game_params *params)
+{
+    game_params *ret = snew(game_params);
+    *ret = *params;             /* structure copy */
+    return ret;
+}
+
+static void decode_params(game_params *params, char const *string)
+{
+    char const *p = string;
+
+    params->w2 = atoi(p);
+    while (*p && isdigit((unsigned char)*p)) p++;
+    if (*p == 'x') {
+        p++;
+        params->h2 = atoi(p);
+        while (*p && isdigit((unsigned char)*p)) p++;
+    } else {
+        params->h2 = params->w2;
+    }
+
+    if (*p == 'd') {
+        int i;
+        p++;
+        params->diff = DIFFCOUNT + 1;   /* ...which is invalid */
+        if (*p) {
+            for (i = 0; i < DIFFCOUNT; i++) {
+                if (*p == unruly_diffchars[i])
+                    params->diff = i;
+            }
+            p++;
+        }
+    }
+}
+
+static char *encode_params(game_params *params, int full)
+{
+    char buf[80];
+
+    sprintf(buf, "%dx%d", params->w2, params->h2);
+    if (full)
+        sprintf(buf + strlen(buf), "d%c", unruly_diffchars[params->diff]);
+
+    return dupstr(buf);
+}
+
+static config_item *game_configure(game_params *params)
+{
+    config_item *ret;
+    char buf[80];
+
+    ret = snewn(4, config_item);
+
+    ret[0].name = "Width";
+    ret[0].type = C_STRING;
+    sprintf(buf, "%d", params->w2);
+    ret[0].sval = dupstr(buf);
+    ret[0].ival = 0;
+
+    ret[1].name = "Height";
+    ret[1].type = C_STRING;
+    sprintf(buf, "%d", params->h2);
+    ret[1].sval = dupstr(buf);
+    ret[1].ival = 0;
+
+    ret[2].name = "Difficulty";
+    ret[2].type = C_CHOICES;
+    ret[2].sval = DIFFCONFIG;
+    ret[2].ival = params->diff;
+
+    ret[3].name = NULL;
+    ret[3].type = C_END;
+    ret[3].sval = NULL;
+    ret[3].ival = 0;
+
+    return ret;
+}
+
+static game_params *custom_params(config_item *cfg)
+{
+    game_params *ret = snew(game_params);
+
+    ret->w2 = atoi(cfg[0].sval);
+    ret->h2 = atoi(cfg[1].sval);
+    ret->diff = cfg[2].ival;
+
+    return ret;
+}
+
+static char *validate_params(game_params *params, int full)
+{
+    if ((params->w2 & 1) || (params->h2 & 1))
+        return "Width and height must both be even";
+    if (params->w2 < 6 || params->h2 < 6)
+        return "Width and height must be at least 6";
+    if (params->diff >= DIFFCOUNT)
+        return "Unknown difficulty rating";
+
+    return NULL;
+}
+
+static char *validate_desc(game_params *params, char *desc)
+{
+    int w2 = params->w2, h2 = params->h2;
+    int s = w2 * h2;
+
+    char *p = desc;
+    int pos = 0;
+
+    while (*p) {
+        if (*p >= 'a' && *p < 'z')
+            pos += 1 + (*p - 'a');
+        else if (*p >= 'A' && *p < 'Z')
+            pos += 1 + (*p - 'A');
+        else if (*p == 'Z' || *p == 'z')
+            pos += 25;
+        else
+            return "Description contains invalid characters";
+
+        ++p;
+    }
+
+    if (pos < s+1)
+        return "Description too short";
+    if (pos > s+1)
+        return "Description too long";
+
+    return NULL;
+}
+
+static game_state *blank_state(int w2, int h2)
+{
+    game_state *state = snew(game_state);
+    int s = w2 * h2;
+
+    state->w2 = w2;
+    state->h2 = h2;
+    state->grid = snewn(s, char);
+    state->immutable = snewn(s, unsigned char);
+
+    memset(state->grid, EMPTY, s);
+    memset(state->immutable, FALSE, s);
+
+    state->completed = state->cheated = FALSE;
+
+    return state;
+}
+
+static game_state *new_game(midend *me, game_params *params, char *desc)
+{
+    int w2 = params->w2, h2 = params->h2;
+    int s = w2 * h2;
+
+    game_state *state = blank_state(w2, h2);
+
+    char *p = desc;
+    int pos = 0;
+
+    while (*p) {
+        if (*p >= 'a' && *p < 'z') {
+            pos += (*p - 'a');
+            if (pos < s) {
+                state->grid[pos] = N_ZERO;
+                state->immutable[pos] = TRUE;
+            }
+            pos++;
+        } else if (*p >= 'A' && *p < 'Z') {
+            pos += (*p - 'A');
+            if (pos < s) {
+                state->grid[pos] = N_ONE;
+                state->immutable[pos] = TRUE;
+            }
+            pos++;
+        } else if (*p == 'Z' || *p == 'z') {
+            pos += 25;
+        } else
+            assert(!"Description contains invalid characters");
+
+        ++p;
+    }
+    assert(pos == s+1);
+
+    return state;
+}
+
+static game_state *dup_game(game_state *state)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int s = w2 * h2;
+
+    game_state *ret = blank_state(w2, h2);
+
+    memcpy(ret->grid, state->grid, s);
+    memcpy(ret->immutable, state->immutable, s);
+
+    ret->completed = state->completed;
+    ret->cheated = state->cheated;
+
+    return ret;
+}
+
+static void free_game(game_state *state)
+{
+    sfree(state->grid);
+    sfree(state->immutable);
+
+    sfree(state);
+}
+
+static int game_can_format_as_text_now(game_params *params)
+{
+    return TRUE;
+}
+
+static char *game_text_format(game_state *state)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int lr = w2*2 + 1;
+
+    char *ret = snewn(lr * h2 + 1, char);
+    char *p = ret;
+
+    int x, y;
+    for (y = 0; y < h2; y++) {
+        for (x = 0; x < w2; x++) {
+            /* Place number */
+            char c = state->grid[y * w2 + x];
+            *p++ = (c == N_ONE ? '1' : c == N_ZERO ? '0' : '.');
+            *p++ = ' ';
+        }
+        /* End line */
+        *p++ = '\n';
+    }
+    /* End with NUL */
+    *p++ = '\0';
+
+    return ret;
+}
+
+/* ****** *
+ * Solver *
+ * ****** */
+
+struct unruly_scratch {
+    int *ones_rows;
+    int *ones_cols;
+    int *zeros_rows;
+    int *zeros_cols;
+};
+
+static void unruly_solver_update_remaining(game_state *state,
+                                           struct unruly_scratch *scratch)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int x, y;
+
+    /* Reset all scratch data */
+    memset(scratch->ones_rows, 0, h2 * sizeof(int));
+    memset(scratch->ones_cols, 0, w2 * sizeof(int));
+    memset(scratch->zeros_rows, 0, h2 * sizeof(int));
+    memset(scratch->zeros_cols, 0, w2 * sizeof(int));
+
+    for (x = 0; x < w2; x++)
+        for (y = 0; y < h2; y++) {
+            if (state->grid[y * w2 + x] == N_ONE) {
+                scratch->ones_rows[y]++;
+                scratch->ones_cols[x]++;
+            } else if (state->grid[y * w2 + x] == N_ZERO) {
+                scratch->zeros_rows[y]++;
+                scratch->zeros_cols[x]++;
+            }
+        }
+}
+
+static struct unruly_scratch *unruly_new_scratch(game_state *state)
+{
+    int w2 = state->w2, h2 = state->h2;
+
+    struct unruly_scratch *ret = snew(struct unruly_scratch);
+
+    ret->ones_rows = snewn(h2, int);
+    ret->ones_cols = snewn(w2, int);
+    ret->zeros_rows = snewn(h2, int);
+    ret->zeros_cols = snewn(w2, int);
+
+    unruly_solver_update_remaining(state, ret);
+
+    return ret;
+}
+
+static void unruly_free_scratch(struct unruly_scratch *scratch)
+{
+    sfree(scratch->ones_rows);
+    sfree(scratch->ones_cols);
+    sfree(scratch->zeros_rows);
+    sfree(scratch->zeros_cols);
+
+    sfree(scratch);
+}
+
+static int unruly_solver_check_threes(game_state *state, int *rowcount,
+                                      int *colcount, int horizontal,
+                                      char check, char block)
+{
+    int w2 = state->w2, h2 = state->h2;
+
+    int dx = horizontal ? 1 : 0, dy = 1 - dx;
+    int sx = dx, sy = dy;
+    int ex = w2 - dx, ey = h2 - dy;
+
+    int x, y;
+    int ret = 0;
+
+    /* Check for any three squares which almost form three in a row */
+    for (y = sy; y < ey; y++) {
+        for (x = sx; x < ex; x++) {
+            int i1 = (y-dy) * w2 + (x-dx);
+            int i2 = y * w2 + x;
+            int i3 = (y+dy) * w2 + (x+dx);
+
+            if (state->grid[i1] == check && state->grid[i2] == check
+                && state->grid[i3] == EMPTY) {
+                ret++;
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: %i,%i and %i,%i confirm %c at %i,%i\n",
+                           i1 % w2, i1 / w2, i2 % w2, i2 / w2,
+                           (block == N_ONE ? '1' : '0'), i3 % w2,
+                           i3 / w2);
+                }
+#endif
+                state->grid[i3] = block;
+                rowcount[i3 / w2]++;
+                colcount[i3 % w2]++;
+            }
+            if (state->grid[i1] == check && state->grid[i2] == EMPTY
+                && state->grid[i3] == check) {
+                ret++;
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: %i,%i and %i,%i confirm %c at %i,%i\n",
+                           i1 % w2, i1 / w2, i3 % w2, i3 / w2,
+                           (block == N_ONE ? '1' : '0'), i2 % w2,
+                           i2 / w2);
+                }
+#endif
+                state->grid[i2] = block;
+                rowcount[i2 / w2]++;
+                colcount[i2 % w2]++;
+            }
+            if (state->grid[i1] == EMPTY && state->grid[i2] == check
+                && state->grid[i3] == check) {
+                ret++;
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: %i,%i and %i,%i confirm %c at %i,%i\n",
+                           i2 % w2, i2 / w2, i3 % w2, i3 / w2,
+                           (block == N_ONE ? '1' : '0'), i1 % w2,
+                           i1 / w2);
+                }
+#endif
+                state->grid[i1] = block;
+                rowcount[i1 / w2]++;
+                colcount[i1 % w2]++;
+            }
+        }
+    }
+
+    return ret;
+}
+
+static int unruly_solver_check_all_threes(game_state *state,
+                                          struct unruly_scratch *scratch)
+{
+    int ret = 0;
+
+    ret +=
+        unruly_solver_check_threes(state, scratch->zeros_rows,
+                                   scratch->zeros_cols, TRUE, N_ONE, N_ZERO);
+    ret +=
+        unruly_solver_check_threes(state, scratch->ones_rows,
+                                   scratch->ones_cols, TRUE, N_ZERO, N_ONE);
+    ret +=
+        unruly_solver_check_threes(state, scratch->zeros_rows,
+                                   scratch->zeros_cols, FALSE, N_ONE,
+                                   N_ZERO);
+    ret +=
+        unruly_solver_check_threes(state, scratch->ones_rows,
+                                   scratch->ones_cols, FALSE, N_ZERO, N_ONE);
+
+    return ret;
+}
+
+static int unruly_solver_fill_row(game_state *state, int i, int horizontal,
+                                  int *rowcount, int *colcount, char fill)
+{
+    int ret = 0;
+    int w2 = state->w2, h2 = state->h2;
+    int j;
+
+#ifdef STANDALONE_SOLVER
+    if (solver_verbose) {
+        printf("Solver: Filling %s %i with %c:",
+               (horizontal ? "Row" : "Col"), i,
+               (fill == N_ZERO ? '0' : '1'));
+    }
+#endif
+    /* Place a number in every empty square in a row/column */
+    for (j = 0; j < (horizontal ? w2 : h2); j++) {
+        int p = (horizontal ? i * w2 + j : j * w2 + i);
+
+        if (state->grid[p] == EMPTY) {
+#ifdef STANDALONE_SOLVER
+            if (solver_verbose) {
+                printf(" (%i,%i)", (horizontal ? j : i),
+                       (horizontal ? i : j));
+            }
+#endif
+            ret++;
+            state->grid[p] = fill;
+            rowcount[(horizontal ? i : j)]++;
+            colcount[(horizontal ? j : i)]++;
+        }
+    }
+
+#ifdef STANDALONE_SOLVER
+    if (solver_verbose) {
+        printf("\n");
+    }
+#endif
+
+    return ret;
+}
+
+static int unruly_solver_check_complete_nums(game_state *state,
+                                             int *complete, int horizontal,
+                                             int *rowcount, int *colcount,
+                                             char fill)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int count = (horizontal ? h2 : w2); /* number of rows to check */
+    int target = (horizontal ? w2 : h2) / 2; /* target number of 0s/1s */
+    int *other = (horizontal ? rowcount : colcount);
+
+    int ret = 0;
+
+    int i;
+    /* Check for completed rows/cols for one number, then fill in the rest */
+    for (i = 0; i < count; i++) {
+        if (complete[i] == target && other[i] < target) {
+#ifdef STANDALONE_SOLVER
+            if (solver_verbose) {
+                printf("Solver: Row %i satisfied for %c\n", i,
+                       (fill != N_ZERO ? '0' : '1'));
+            }
+#endif
+            ret += unruly_solver_fill_row(state, i, horizontal, rowcount,
+                                          colcount, fill);
+        }
+    }
+
+    return ret;
+}
+
+static int unruly_solver_check_all_complete_nums(game_state *state,
+                                                 struct unruly_scratch *scratch)
+{
+    int ret = 0;
+
+    ret +=
+        unruly_solver_check_complete_nums(state, scratch->ones_rows, TRUE,
+                                          scratch->zeros_rows,
+                                          scratch->zeros_cols, N_ZERO);
+    ret +=
+        unruly_solver_check_complete_nums(state, scratch->ones_cols, FALSE,
+                                          scratch->zeros_rows,
+                                          scratch->zeros_cols, N_ZERO);
+    ret +=
+        unruly_solver_check_complete_nums(state, scratch->zeros_rows, TRUE,
+                                          scratch->ones_rows,
+                                          scratch->ones_cols, N_ONE);
+    ret +=
+        unruly_solver_check_complete_nums(state, scratch->zeros_cols, FALSE,
+                                          scratch->ones_rows,
+                                          scratch->ones_cols, N_ONE);
+
+    return ret;
+}
+
+static int unruly_solver_check_near_complete(game_state *state,
+                                             int *complete, int horizontal,
+                                             int *rowcount, int *colcount,
+                                             char fill)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int w = w2/2, h = h2/2;
+
+    int dx = horizontal ? 1 : 0, dy = 1 - dx;
+
+    int sx = dx, sy = dy;
+    int ex = w2 - dx, ey = h2 - dy;
+
+    int x, y;
+    int ret = 0;
+
+    /*
+     * This function checks for a row with one Y remaining, then looks
+     * for positions that could cause the remaining squares in the row
+     * to make 3 X's in a row. Example:
+     *
+     * Consider the following row:
+     * 1 1 0 . . .
+     * If the last 1 was placed in the last square, the remaining
+     * squares would be 0:
+     * 1 1 0 0 0 1
+     * This violates the 3 in a row rule. We now know that the last 1
+     * shouldn't be in the last cell.
+     * 1 1 0 . . 0
+     */
+
+    /* Check for any two blank and one filled square */
+    for (y = sy; y < ey; y++) {
+        /* One type must have 1 remaining, the other at least 2 */
+        if (horizontal && (complete[y] < w - 1 || rowcount[y] > w - 2))
+            continue;
+
+        for (x = sx; x < ex; x++) {
+            int i, i1, i2, i3;
+            if (!horizontal
+                && (complete[x] < h - 1 || colcount[x] > h - 2))
+                continue;
+
+            i = (horizontal ? y : x);
+            i1 = (y-dy) * w2 + (x-dx);
+            i2 = y * w2 + x;
+            i3 = (y+dy) * w2 + (x+dx);
+
+            if (state->grid[i1] == fill && state->grid[i2] == EMPTY
+                && state->grid[i3] == EMPTY) {
+                /*
+                 * Temporarily fill the empty spaces with something else.
+                 * This avoids raising the counts for the row and column
+                 */
+                state->grid[i2] = BOGUS;
+                state->grid[i3] = BOGUS;
+
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: Row %i nearly satisfied for %c\n", i,
+                           (fill != N_ZERO ? '0' : '1'));
+                }
+#endif
+                ret +=
+                    unruly_solver_fill_row(state, i, horizontal, rowcount,
+                                         colcount, fill);
+
+                state->grid[i2] = EMPTY;
+                state->grid[i3] = EMPTY;
+            }
+
+            else if (state->grid[i1] == EMPTY && state->grid[i2] == fill
+                     && state->grid[i3] == EMPTY) {
+                state->grid[i1] = BOGUS;
+                state->grid[i3] = BOGUS;
+
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: Row %i nearly satisfied for %c\n", i,
+                           (fill != N_ZERO ? '0' : '1'));
+                }
+#endif
+                ret +=
+                    unruly_solver_fill_row(state, i, horizontal, rowcount,
+                                         colcount, fill);
+
+                state->grid[i1] = EMPTY;
+                state->grid[i3] = EMPTY;
+            }
+
+            else if (state->grid[i1] == EMPTY && state->grid[i2] == EMPTY
+                     && state->grid[i3] == fill) {
+                state->grid[i1] = BOGUS;
+                state->grid[i2] = BOGUS;
+
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: Row %i nearly satisfied for %c\n", i,
+                           (fill != N_ZERO ? '0' : '1'));
+                }
+#endif
+                ret +=
+                    unruly_solver_fill_row(state, i, horizontal, rowcount,
+                                         colcount, fill);
+
+                state->grid[i1] = EMPTY;
+                state->grid[i2] = EMPTY;
+            }
+
+            else if (state->grid[i1] == EMPTY && state->grid[i2] == EMPTY
+                     && state->grid[i3] == EMPTY) {
+                state->grid[i1] = BOGUS;
+                state->grid[i2] = BOGUS;
+                state->grid[i3] = BOGUS;
+
+#ifdef STANDALONE_SOLVER
+                if (solver_verbose) {
+                    printf("Solver: Row %i nearly satisfied for %c\n", i,
+                           (fill != N_ZERO ? '0' : '1'));
+                }
+#endif
+                ret +=
+                    unruly_solver_fill_row(state, i, horizontal, rowcount,
+                                         colcount, fill);
+
+                state->grid[i1] = EMPTY;
+                state->grid[i2] = EMPTY;
+                state->grid[i3] = EMPTY;
+            }
+        }
+    }
+
+    return ret;
+}
+
+static int unruly_solver_check_all_near_complete(game_state *state,
+                                                 struct unruly_scratch *scratch)
+{
+    int ret = 0;
+
+    ret +=
+        unruly_solver_check_near_complete(state, scratch->ones_rows, TRUE,
+                                        scratch->zeros_rows,
+                                        scratch->zeros_cols, N_ZERO);
+    ret +=
+        unruly_solver_check_near_complete(state, scratch->ones_cols, FALSE,
+                                        scratch->zeros_rows,
+                                        scratch->zeros_cols, N_ZERO);
+    ret +=
+        unruly_solver_check_near_complete(state, scratch->zeros_rows, TRUE,
+                                        scratch->ones_rows,
+                                        scratch->ones_cols, N_ONE);
+    ret +=
+        unruly_solver_check_near_complete(state, scratch->zeros_cols, FALSE,
+                                        scratch->ones_rows,
+                                        scratch->ones_cols, N_ONE);
+
+    return ret;
+}
+
+static int unruly_validate_rows(game_state *state, int horizontal,
+                                char check, int *errors)
+{
+    int w2 = state->w2, h2 = state->h2;
+
+    int dx = horizontal ? 1 : 0, dy = 1 - dx;
+
+    int sx = dx, sy = dy;
+    int ex = w2 - dx, ey = h2 - dy;
+
+    int x, y;
+    int ret = 0;
+
+    int err1 = (horizontal ? FE_HOR_ROW_LEFT : FE_VER_ROW_TOP);
+    int err2 = (horizontal ? FE_HOR_ROW_MID : FE_VER_ROW_MID);
+    int err3 = (horizontal ? FE_HOR_ROW_RIGHT : FE_VER_ROW_BOTTOM);
+
+    /* Check for any three in a row, and mark errors accordingly (if
+     * required) */
+    for (y = sy; y < ey; y++) {
+        for (x = sx; x < ex; x++) {
+            int i1 = (y-dy) * w2 + (x-dx);
+            int i2 = y * w2 + x;
+            int i3 = (y+dy) * w2 + (x+dx);
+
+            if (state->grid[i1] == check && state->grid[i2] == check
+                && state->grid[i3] == check) {
+                ret++;
+                if (errors) {
+                    errors[i1] |= err1;
+                    errors[i2] |= err2;
+                    errors[i3] |= err3;
+                }
+            }
+        }
+    }
+
+    return ret;
+}
+
+static int unruly_validate_all_rows(game_state *state, int *errors)
+{
+    int errcount = 0;
+
+    errcount += unruly_validate_rows(state, TRUE, N_ONE, errors);
+    errcount += unruly_validate_rows(state, FALSE, N_ONE, errors);
+    errcount += unruly_validate_rows(state, TRUE, N_ZERO, errors);
+    errcount += unruly_validate_rows(state, FALSE, N_ZERO, errors);
+
+    if (errcount)
+        return -1;
+    return 0;
+}
+
+static int unruly_validate_counts(game_state *state,
+                                  struct unruly_scratch *scratch, int *errors)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int w = w2/2, h = h2/2;
+    char below = FALSE;
+    char above = FALSE;
+    int i;
+
+    /* See if all rows/columns are satisfied. If one is exceeded,
+     * mark it as an error (if required)
+     */
+
+    char hasscratch = TRUE;
+    if (!scratch) {
+        scratch = unruly_new_scratch(state);
+        hasscratch = FALSE;
+    }
+
+    for (i = 0; i < w2; i++) {
+        if (scratch->ones_cols[i] < h)
+            below = TRUE;
+        if (scratch->zeros_cols[i] < h)
+            below = TRUE;
+
+        if (scratch->ones_cols[i] > h) {
+            above = TRUE;
+            if (errors)
+                errors[2*h2 + i] = TRUE;
+        } else if (errors)
+            errors[2*h2 + i] = FALSE;
+
+        if (scratch->zeros_cols[i] > h) {
+            above = TRUE;
+            if (errors)
+                errors[2*h2 + w2 + i] = TRUE;
+        } else if (errors)
+            errors[2*h2 + w2 + i] = FALSE;
+    }
+    for (i = 0; i < h2; i++) {
+        if (scratch->ones_rows[i] < w)
+            below = TRUE;
+        if (scratch->zeros_rows[i] < w)
+            below = TRUE;
+
+        if (scratch->ones_rows[i] > w) {
+            above = TRUE;
+            if (errors)
+                errors[i] = TRUE;
+        } else if (errors)
+            errors[i] = FALSE;
+
+        if (scratch->zeros_rows[i] > w) {
+            above = TRUE;
+            if (errors)
+                errors[h2 + i] = TRUE;
+        } else if (errors)
+            errors[h2 + i] = FALSE;
+    }
+
+    if (!hasscratch)
+        unruly_free_scratch(scratch);
+
+    return (above ? -1 : below ? 1 : 0);
+}
+
+static int unruly_solve_game(game_state *state,
+                             struct unruly_scratch *scratch, int diff)
+{
+    int done, maxdiff = -1;
+
+    while (TRUE) {
+        done = 0;
+
+        /* Check for impending 3's */
+        done += unruly_solver_check_all_threes(state, scratch);
+
+        /* Keep using the simpler techniques while they produce results */
+        if (done) {
+            if (maxdiff < DIFF_EASY)
+                maxdiff = DIFF_EASY;
+            continue;
+        }
+
+        /* Check for completed rows */
+        done += unruly_solver_check_all_complete_nums(state, scratch);
+
+        if (done) {
+            if (maxdiff < DIFF_EASY)
+                maxdiff = DIFF_EASY;
+            continue;
+        }
+
+        /* Normal techniques */
+        if (diff < DIFF_NORMAL)
+            break;
+
+        /* Check for nearly completed rows */
+        done += unruly_solver_check_all_near_complete(state, scratch);
+
+        if (done) {
+            if (maxdiff < DIFF_NORMAL)
+                maxdiff = DIFF_NORMAL;
+            continue;
+        }
+
+        break;
+    }
+    return maxdiff;
+}
+
+static char *solve_game(game_state *state, game_state *currstate,
+                        char *aux, char **error)
+{
+    game_state *solved = dup_game(state);
+    struct unruly_scratch *scratch = unruly_new_scratch(solved);
+    char *ret = NULL;
+    int result;
+
+    unruly_solve_game(solved, scratch, DIFFCOUNT);
+
+    result = unruly_validate_counts(solved, scratch, NULL);
+    if (unruly_validate_all_rows(solved, NULL) == -1)
+        result = -1;
+
+    if (result == 0) {
+        int w2 = solved->w2, h2 = solved->h2;
+        int s = w2 * h2;
+        char *p;
+        int i;
+
+        ret = snewn(s + 2, char);
+        p = ret;
+        *p++ = 'S';
+
+        for (i = 0; i < s; i++)
+            *p++ = (solved->grid[i] == N_ONE ? '1' : '0');
+
+        *p++ = '\0';
+    } else if (result == 1)
+        *error = "No solution found.";
+    else if (result == -1)
+        *error = "Puzzle is invalid.";
+
+    free_game(solved);
+    unruly_free_scratch(scratch);
+    return ret;
+}
+
+/* ********* *
+ * Generator *
+ * ********* */
+
+static int unruly_fill_game(game_state *state, struct unruly_scratch *scratch,
+                            random_state *rs)
+{
+
+    int w2 = state->w2, h2 = state->h2;
+    int s = w2 * h2;
+    int i, j;
+    int *spaces;
+
+#ifdef STANDALONE_SOLVER
+    if (solver_verbose) {
+        printf("Generator: Attempt to fill grid\n");
+    }
+#endif
+
+    /* Generate random array of spaces */
+    spaces = snewn(s, int);
+    for (i = 0; i < s; i++)
+        spaces[i] = i;
+    shuffle(spaces, s, sizeof(*spaces), rs);
+
+    /*
+     * Construct a valid filled grid by repeatedly picking an unfilled
+     * space and fill it, then calling the solver to fill in any
+     * spaces forced by the change.
+     */
+    for (j = 0; j < s; j++) {
+        i = spaces[j];
+
+        if (state->grid[i] != EMPTY)
+            continue;
+
+        if (random_upto(rs, 2)) {
+            state->grid[i] = N_ONE;
+            scratch->ones_rows[i / w2]++;
+            scratch->ones_cols[i % w2]++;
+        } else {
+            state->grid[i] = N_ZERO;
+            scratch->zeros_rows[i / w2]++;
+            scratch->zeros_cols[i % w2]++;
+        }
+
+        unruly_solve_game(state, scratch, DIFFCOUNT);
+    }
+    sfree(spaces);
+
+    if (unruly_validate_all_rows(state, NULL) != 0
+        || unruly_validate_counts(state, scratch, NULL) != 0)
+        return FALSE;
+
+    return TRUE;
+}
+
+static char *new_game_desc(game_params *params, random_state *rs,
+                           char **aux, int interactive)
+{
+#ifdef STANDALONE_SOLVER
+    char *debug;
+    int temp_verbose = FALSE;
+#endif
+
+    int w2 = params->w2, h2 = params->h2;
+    int s = w2 * h2;
+    int *spaces;
+    int i, j, run;
+    char *ret, *p;
+
+    game_state *state;
+    struct unruly_scratch *scratch;
+
+    int attempts = 0;
+
+    while (1) {
+
+        while (TRUE) {
+            attempts++;
+            state = blank_state(w2, h2);
+            scratch = unruly_new_scratch(state);
+            if (unruly_fill_game(state, scratch, rs))
+                break;
+            free_game(state);
+            unruly_free_scratch(scratch);
+        }
+
+#ifdef STANDALONE_SOLVER
+        if (solver_verbose) {
+            printf("Puzzle generated in %i attempts\n", attempts);
+            debug = game_text_format(state);
+            fputs(debug, stdout);
+            sfree(debug);
+
+            temp_verbose = solver_verbose;
+            solver_verbose = FALSE;
+        }
+#endif
+
+        unruly_free_scratch(scratch);
+
+        /* Generate random array of spaces */
+        spaces = snewn(s, int);
+        for (i = 0; i < s; i++)
+            spaces[i] = i;
+        shuffle(spaces, s, sizeof(*spaces), rs);
+
+        /*
+         * Winnow the clues by starting from our filled grid, repeatedly
+         * picking a filled space and emptying it, as long as the solver
+         * reports that the puzzle can still be solved after doing so.
+         */
+        for (j = 0; j < s; j++) {
+            char c;
+            game_state *solver;
+
+            i = spaces[j];
+
+            c = state->grid[i];
+            state->grid[i] = EMPTY;
+
+            solver = dup_game(state);
+            scratch = unruly_new_scratch(state);
+
+            unruly_solve_game(solver, scratch, params->diff);
+
+            if (unruly_validate_counts(solver, scratch, NULL) != 0)
+                state->grid[i] = c;
+
+            free_game(solver);
+            unruly_free_scratch(scratch);
+        }
+        sfree(spaces);
+
+#ifdef STANDALONE_SOLVER
+        if (temp_verbose) {
+            solver_verbose = TRUE;
+
+            printf("Final puzzle:\n");
+            debug = game_text_format(state);
+            fputs(debug, stdout);
+            sfree(debug);
+        }
+#endif
+
+        /*
+         * See if the game has accidentally come out too easy.
+         */
+        if (params->diff > 0) {
+            int ok;
+            game_state *solver;
+
+            solver = dup_game(state);
+            scratch = unruly_new_scratch(state);
+
+            unruly_solve_game(solver, scratch, params->diff - 1);
+
+            ok = unruly_validate_counts(solver, scratch, NULL);
+
+            free_game(solver);
+            unruly_free_scratch(scratch);
+
+            if (ok)
+                break;
+        } else {
+            /*
+             * Puzzles of the easiest difficulty can't be too easy.
+             */
+            break;
+        }
+    }
+
+    /* Encode description */
+    ret = snewn(s + 1, char);
+    p = ret;
+    run = 0;
+    for (i = 0; i < s+1; i++) {
+        if (i == s || state->grid[i] == N_ZERO) {
+            while (run > 24) {
+                *p++ = 'z';
+                run -= 25;
+            }
+            *p++ = 'a' + run;
+            run = 0;
+        } else if (state->grid[i] == N_ONE) {
+            while (run > 24) {
+                *p++ = 'Z';
+                run -= 25;
+            }
+            *p++ = 'A' + run;
+            run = 0;
+        } else {
+            run++;
+        }
+    }
+    *p = '\0';
+
+    free_game(state);
+
+    return ret;
+}
+
+/* ************** *
+ * User Interface *
+ * ************** */
+
+struct game_ui {
+    int cx, cy;
+    char cursor;
+};
+
+static game_ui *new_ui(game_state *state)
+{
+    game_ui *ret = snew(game_ui);
+
+    ret->cx = ret->cy = 0;
+    ret->cursor = FALSE;
+
+    return ret;
+}
+
+static void free_ui(game_ui *ui)
+{
+    sfree(ui);
+}
+
+static char *encode_ui(game_ui *ui)
+{
+    return NULL;
+}
+
+static void decode_ui(game_ui *ui, char *encoding)
+{
+}
+
+static void game_changed_state(game_ui *ui, game_state *oldstate,
+                               game_state *newstate)
+{
+}
+
+struct game_drawstate {
+    int tilesize;
+    int w2, h2;
+    int started;
+
+    int *gridfs;
+    int *rowfs;
+
+    int *grid;
+};
+
+static game_drawstate *game_new_drawstate(drawing *dr, game_state *state)
+{
+    struct game_drawstate *ds = snew(struct game_drawstate);
+
+    int w2 = state->w2, h2 = state->h2;
+    int s = w2 * h2;
+    int i;
+
+    ds->tilesize = 0;
+    ds->w2 = w2;
+    ds->h2 = h2;
+    ds->started = FALSE;
+
+    ds->gridfs = snewn(s, int);
+    ds->rowfs = snewn(2 * (w2 + h2), int);
+
+    ds->grid = snewn(s, int);
+    for (i = 0; i < s; i++)
+        ds->grid[i] = -1;
+
+    return ds;
+}
+
+static void game_free_drawstate(drawing *dr, game_drawstate *ds)
+{
+    sfree(ds->gridfs);
+    sfree(ds->rowfs);
+    sfree(ds->grid);
+    sfree(ds);
+}
+
+#define COORD(x)     ( (x) * ds->tilesize + ds->tilesize/2 )
+#define FROMCOORD(x) ( ((x)-(ds->tilesize/2)) / ds->tilesize )
+
+static char *interpret_move(game_state *state, game_ui *ui,
+                            const game_drawstate *ds, int ox, int oy,
+                            int button)
+{
+    int hx = ui->cx;
+    int hy = ui->cy;
+
+    int gx = FROMCOORD(ox);
+    int gy = FROMCOORD(oy);
+
+    int w2 = state->w2, h2 = state->h2;
+
+    button &= ~MOD_MASK;
+
+    /* Mouse click */
+    if (button == LEFT_BUTTON || button == RIGHT_BUTTON ||
+        button == MIDDLE_BUTTON) {
+        if (ox >= (ds->tilesize / 2) && gx < w2
+            && oy >= (ds->tilesize / 2) && gy < h2) {
+            hx = gx;
+            hy = gy;
+            ui->cursor = FALSE;
+        } else
+            return NULL;
+    }
+
+    /* Keyboard move */
+    if (IS_CURSOR_MOVE(button)) {
+        move_cursor(button, &ui->cx, &ui->cy, w2, h2, 0);
+        ui->cursor = TRUE;
+        return "";
+    }
+
+    /* Place one */
+    if ((ui->cursor && (button == CURSOR_SELECT || button == CURSOR_SELECT2
+                        || button == '\b' || button == '0' || button == '1'
+                        || button == '2')) ||
+        button == LEFT_BUTTON || button == RIGHT_BUTTON ||
+        button == MIDDLE_BUTTON) {
+        char buf[80];
+        char c, i;
+
+        if (state->immutable[hy * w2 + hx])
+            return NULL;
+
+        c = '-';
+        i = state->grid[hy * w2 + hx];
+
+        if (button == '0' || button == '2')
+            c = '0';
+        else if (button == '1')
+            c = '1';
+        else if (button == MIDDLE_BUTTON)
+            c = '-';
+
+        /* Cycle through options */
+        else if (button == CURSOR_SELECT || button == RIGHT_BUTTON)
+            c = (i == EMPTY ? '0' : i == N_ZERO ? '1' : '-');
+        else if (button == CURSOR_SELECT2 || button == LEFT_BUTTON)
+            c = (i == EMPTY ? '1' : i == N_ONE ? '0' : '-');
+
+        if (state->grid[hy * w2 + hx] ==
+            (c == '0' ? N_ZERO : c == '1' ? N_ONE : EMPTY))
+            return NULL;               /* don't put no-ops on the undo chain */
+
+        sprintf(buf, "P%c,%d,%d", c, hx, hy);
+
+        return dupstr(buf);
+    }
+    return NULL;
+}
+
+static game_state *execute_move(game_state *state, char *move)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int s = w2 * h2;
+    int x, y, i;
+    char c;
+
+    game_state *ret;
+
+    if (move[0] == 'S') {
+        char *p;
+
+        ret = dup_game(state);
+        p = move + 1;
+
+        for (i = 0; i < s; i++) {
+
+            if (!*p || !(*p == '1' || *p == '0')) {
+                free_game(ret);
+                return NULL;
+            }
+
+            ret->grid[i] = (*p == '1' ? N_ONE : N_ZERO);
+            p++;
+        }
+
+        ret->completed = ret->cheated = TRUE;
+        return ret;
+    } else if (move[0] == 'P'
+               && sscanf(move + 1, "%c,%d,%d", &c, &x, &y) == 3 && x >= 0
+               && x < w2 && y >= 0 && y < h2 && (c == '-' || c == '0'
+                                                 || c == '1')) {
+        ret = dup_game(state);
+        i = y * w2 + x;
+
+        if (state->immutable[i]) {
+            free_game(ret);
+            return NULL;
+        }
+
+        ret->grid[i] = (c == '1' ? N_ONE : c == '0' ? N_ZERO : EMPTY);
+
+        if (!ret->completed && unruly_validate_counts(ret, NULL, NULL) == 0
+            && (unruly_validate_all_rows(ret, NULL) == 0))
+            ret->completed = TRUE;
+
+        return ret;
+    }
+
+    return NULL;
+}
+
+/* ----------------------------------------------------------------------
+ * Drawing routines.
+ */
+
+static void game_compute_size(game_params *params, int tilesize,
+                              int *x, int *y)
+{
+    *x = tilesize * (params->w2 + 1);
+    *y = tilesize * (params->h2 + 1);
+}
+
+static void game_set_size(drawing *dr, game_drawstate *ds,
+                          game_params *params, int tilesize)
+{
+    ds->tilesize = tilesize;
+}
+
+static float *game_colours(frontend *fe, int *ncolours)
+{
+    float *ret = snewn(3 * NCOLOURS, float);
+    int i;
+
+    frontend_default_colour(fe, &ret[COL_BACKGROUND * 3]);
+
+    for (i = 0; i < 3; i++) {
+        ret[COL_1 * 3 + i] = 0.2F;
+        ret[COL_1_HIGHLIGHT * 3 + i] = 0.4F;
+        ret[COL_1_LOWLIGHT * 3 + i] = 0.0F;
+        ret[COL_0 * 3 + i] = 0.95F;
+        ret[COL_0_HIGHLIGHT * 3 + i] = 1.0F;
+        ret[COL_0_LOWLIGHT * 3 + i] = 0.9F;
+        ret[COL_EMPTY * 3 + i] = 0.5F;
+        ret[COL_GRID * 3 + i] = 0.3F;
+    }
+    game_mkhighlight_specific(fe, ret, COL_0, COL_0_HIGHLIGHT, COL_0_LOWLIGHT);
+    game_mkhighlight_specific(fe, ret, COL_1, COL_1_HIGHLIGHT, COL_1_LOWLIGHT);
+
+    ret[COL_ERROR * 3 + 0] = 1.0F;
+    ret[COL_ERROR * 3 + 1] = 0.0F;
+    ret[COL_ERROR * 3 + 2] = 0.0F;
+
+    ret[COL_CURSOR * 3 + 0] = 0.0F;
+    ret[COL_CURSOR * 3 + 1] = 0.7F;
+    ret[COL_CURSOR * 3 + 2] = 0.0F;
+
+    *ncolours = NCOLOURS;
+    return ret;
+}
+
+static void unruly_draw_err_rectangle(drawing *dr, int x, int y, int w, int h,
+                                      int tilesize)
+{
+    double thick = tilesize / 10;
+    double margin = tilesize / 20;
+
+    draw_rect(dr, x+margin, y+margin, w-2*margin, thick, COL_ERROR);
+    draw_rect(dr, x+margin, y+margin, thick, h-2*margin, COL_ERROR);
+    draw_rect(dr, x+margin, y+h-margin-thick, w-2*margin, thick, COL_ERROR);
+    draw_rect(dr, x+w-margin-thick, y+margin, thick, h-2*margin, COL_ERROR);
+}
+
+static void unruly_draw_tile(drawing *dr, int x, int y, int tilesize, int tile)
+{
+    clip(dr, x, y, tilesize, tilesize);
+
+    /* Draw the grid edge first, so the tile can overwrite it */
+    draw_rect(dr, x, y, tilesize, tilesize, COL_GRID);
+
+    /* Background of the tile */
+    {
+        int val = (tile & FF_ZERO ? 0 : tile & FF_ONE ? 2 : 1);
+        val = (val == 0 ? COL_0 : val == 2 ? COL_1 : COL_EMPTY);
+
+        if ((tile & (FF_FLASH1 | FF_FLASH2)) &&
+             (val == COL_0 || val == COL_1)) {
+            val += (tile & FF_FLASH1 ? 1 : 2);
+        }
+
+        draw_rect(dr, x, y, tilesize-1, tilesize-1, val);
+
+        if ((val == COL_0 || val == COL_1) && (tile & FF_IMMUTABLE)) {
+            draw_rect(dr, x + tilesize/6, y + tilesize/6,
+                      tilesize - 2*(tilesize/6) - 2, 1, val + 2);
+            draw_rect(dr, x + tilesize/6, y + tilesize/6,
+                      1, tilesize - 2*(tilesize/6) - 2, val + 2);
+            draw_rect(dr, x + tilesize/6 + 1, y + tilesize - tilesize/6 - 2,
+                      tilesize - 2*(tilesize/6) - 2, 1, val + 1);
+            draw_rect(dr, x + tilesize - tilesize/6 - 2, y + tilesize/6 + 1,
+                      1, tilesize - 2*(tilesize/6) - 2, val + 1);
+        }
+    }
+
+    /* 3-in-a-row errors */
+    if (tile & (FE_HOR_ROW_LEFT | FE_HOR_ROW_RIGHT)) {
+        int left = x, right = x + tilesize - 1;
+        if ((tile & FE_HOR_ROW_LEFT))
+            right += tilesize/2;
+        if ((tile & FE_HOR_ROW_RIGHT))
+            left -= tilesize/2;
+        unruly_draw_err_rectangle(dr, left, y, right-left, tilesize-1, tilesize);
+    }
+    if (tile & (FE_VER_ROW_TOP | FE_VER_ROW_BOTTOM)) {
+        int top = y, bottom = y + tilesize - 1;
+        if ((tile & FE_VER_ROW_TOP))
+            bottom += tilesize/2;
+        if ((tile & FE_VER_ROW_BOTTOM))
+            top -= tilesize/2;
+        unruly_draw_err_rectangle(dr, x, top, tilesize-1, bottom-top, tilesize);
+    }
+
+    /* Count errors */
+    if (tile & FE_COUNT) {
+        draw_text(dr, x + tilesize/2, y + tilesize/2, FONT_VARIABLE,
+                  tilesize/2, ALIGN_HCENTRE | ALIGN_VCENTRE, COL_ERROR, "!");
+    }
+
+    /* Cursor rectangle */
+    if (tile & FF_CURSOR) {
+        draw_rect(dr, x, y, tilesize/12, tilesize-1, COL_CURSOR);
+        draw_rect(dr, x, y, tilesize-1, tilesize/12, COL_CURSOR);
+        draw_rect(dr, x+tilesize-1-tilesize/12, y, tilesize/12, tilesize-1,
+                  COL_CURSOR);
+        draw_rect(dr, x, y+tilesize-1-tilesize/12, tilesize-1, tilesize/12,
+                  COL_CURSOR);
+    }
+
+    unclip(dr);
+    draw_update(dr, x, y, tilesize, tilesize);
+}
+
+#define TILE_SIZE (ds->tilesize)
+#define DEFAULT_TILE_SIZE 32
+#define FLASH_FRAME 0.12F
+#define FLASH_TIME (FLASH_FRAME * 3)
+
+static void game_redraw(drawing *dr, game_drawstate *ds,
+                        game_state *oldstate, game_state *state, int dir,
+                        game_ui *ui, float animtime, float flashtime)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int s = w2 * h2;
+    int flash;
+    int x, y, i;
+
+    if (!ds->started) {
+        /* Main window background */
+        draw_rect(dr, 0, 0, TILE_SIZE * (w2+1), TILE_SIZE * (h2+1),
+                  COL_BACKGROUND);
+        /* Outer edge of grid */
+        draw_rect(dr, COORD(0)-TILE_SIZE/10, COORD(0)-TILE_SIZE/10,
+                  TILE_SIZE*w2 + 2*(TILE_SIZE/10) - 1,
+                  TILE_SIZE*h2 + 2*(TILE_SIZE/10) - 1, COL_GRID);
+
+        draw_update(dr, 0, 0, TILE_SIZE * (w2+1), TILE_SIZE * (h2+1));
+        ds->started = TRUE;
+    }
+
+    flash = 0;
+    if (flashtime > 0)
+        flash = (int)(flashtime / FLASH_FRAME) == 1 ? FF_FLASH2 : FF_FLASH1;
+
+    for (i = 0; i < s; i++)
+        ds->gridfs[i] = 0;
+    unruly_validate_all_rows(state, ds->gridfs);
+    for (i = 0; i < 2 * (h2 + w2); i++)
+        ds->rowfs[i] = 0;
+    unruly_validate_counts(state, NULL, ds->rowfs);
+
+    for (y = 0; y < h2; y++) {
+        for (x = 0; x < w2; x++) {
+            int tile;
+
+            i = y * w2 + x;
+
+            tile = ds->gridfs[i];
+
+            if (state->grid[i] == N_ONE) {
+                tile |= FF_ONE;
+                if (ds->rowfs[y] || ds->rowfs[2*h2 + x])
+                    tile |= FE_COUNT;
+            } else if (state->grid[i] == N_ZERO) {
+                tile |= FF_ZERO;
+                if (ds->rowfs[h2 + y] || ds->rowfs[2*h2 + w2 + x])
+                    tile |= FE_COUNT;
+            }
+
+            tile |= flash;
+
+            if (state->immutable[i])
+                tile |= FF_IMMUTABLE;
+
+            if (ui->cursor && ui->cx == x && ui->cy == y)
+                tile |= FF_CURSOR;
+
+            if (ds->grid[i] != tile) {
+                ds->grid[i] = tile;
+                unruly_draw_tile(dr, COORD(x), COORD(y), TILE_SIZE, tile);
+            }
+        }
+    }
+}
+
+static float game_anim_length(game_state *oldstate, 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)
+{
+    if (!oldstate->completed && newstate->completed &&
+        !oldstate->cheated && !newstate->cheated)
+        return FLASH_TIME;
+    return 0.0F;
+}
+
+static int game_status(game_state *state)
+{
+    return state->completed ? +1 : 0;
+}
+
+static int game_timing_state(game_state *state, game_ui *ui)
+{
+    return TRUE;
+}
+
+static void game_print_size(game_params *params, float *x, float *y)
+{
+    int pw, ph;
+
+    /* Using 7mm squares */
+    game_compute_size(params, 700, &pw, &ph);
+    *x = pw / 100.0F;
+    *y = ph / 100.0F;
+}
+
+static void game_print(drawing *dr, game_state *state, int tilesize)
+{
+    int w2 = state->w2, h2 = state->h2;
+    int x, y;
+
+    int ink = print_mono_colour(dr, 0);
+    char buf[2];
+    buf[1] = '\0';
+
+    for (y = 0; y < h2; y++)
+        for (x = 0; x < w2; x++) {
+            int tx = x * tilesize + (tilesize / 2);
+            int ty = y * tilesize + (tilesize / 2);
+
+            /* Draw the border */
+            int coords[8];
+            coords[0] = tx;
+            coords[1] = ty - 1;
+            coords[2] = tx + tilesize;
+            coords[3] = ty - 1;
+            coords[4] = tx + tilesize;
+            coords[5] = ty + tilesize - 1;
+            coords[6] = tx;
+            coords[7] = ty + tilesize - 1;
+            draw_polygon(dr, coords, 4, -1, ink);
+
+            if (state->grid[y * w2 + x] == N_ONE)
+                draw_rect(dr, tx, ty, tilesize, tilesize, ink);
+            else if (state->grid[y * w2 + x] == N_ZERO)
+                draw_circle(dr, tx + tilesize/2, ty + tilesize/2,
+                            tilesize/12, ink, ink);
+        }
+}
+
+#ifdef COMBINED
+#define thegame unruly
+#endif
+
+const struct game thegame = {
+    "Unruly", "games.unruly", "unruly",
+    default_params,
+    game_fetch_preset,
+    decode_params,
+    encode_params,
+    free_params,
+    dup_params,
+    TRUE, game_configure, custom_params,
+    validate_params,
+    new_game_desc,
+    validate_desc,
+    new_game,
+    dup_game,
+    free_game,
+    TRUE, solve_game,
+    TRUE, game_can_format_as_text_now, game_text_format,
+    new_ui,
+    free_ui,
+    encode_ui,
+    decode_ui,
+    game_changed_state,
+    interpret_move,
+    execute_move,
+    DEFAULT_TILE_SIZE, game_compute_size, game_set_size,
+    game_colours,
+    game_new_drawstate,
+    game_free_drawstate,
+    game_redraw,
+    game_anim_length,
+    game_flash_length,
+    game_status,
+    TRUE, FALSE, game_print_size, game_print,
+    FALSE,                      /* wants_statusbar */
+    FALSE, game_timing_state,
+    0,                          /* flags */
+};
+
+/* ***************** *
+ * Standalone solver *
+ * ***************** */
+
+#ifdef STANDALONE_SOLVER
+#include <time.h>
+#include <stdarg.h>
+
+/* Most of the standalone solver code was copied from unequal.c and singles.c */
+
+const char *quis;
+
+static void usage_exit(const char *msg)
+{
+    if (msg)
+        fprintf(stderr, "%s: %s\n", quis, msg);
+    fprintf(stderr,
+            "Usage: %s [-v] [--seed SEED] <params> | [game_id [game_id ...]]\n",
+            quis);
+    exit(1);
+}
+
+int main(int argc, char *argv[])
+{
+    random_state *rs;
+    time_t seed = time(NULL);
+
+    game_params *params = NULL;
+
+    char *id = NULL, *desc = NULL, *err;
+
+    quis = argv[0];
+
+    while (--argc > 0) {
+        char *p = *++argv;
+        if (!strcmp(p, "--seed")) {
+            if (argc == 0)
+                usage_exit("--seed needs an argument");
+            seed = (time_t) atoi(*++argv);
+            argc--;
+        } else if (!strcmp(p, "-v"))
+            solver_verbose = TRUE;
+        else if (*p == '-')
+            usage_exit("unrecognised option");
+        else
+            id = p;
+    }
+
+    if (id) {
+        desc = strchr(id, ':');
+        if (desc)
+            *desc++ = '\0';
+
+        params = default_params();
+        decode_params(params, id);
+        err = validate_params(params, TRUE);
+        if (err) {
+            fprintf(stderr, "Parameters are invalid\n");
+            fprintf(stderr, "%s: %s", argv[0], err);
+            exit(1);
+        }
+    }
+
+    if (!desc) {
+        char *desc_gen, *aux;
+        rs = random_new((void *) &seed, sizeof(time_t));
+        if (!params)
+            params = default_params();
+        printf("Generating puzzle with parameters %s\n",
+               encode_params(params, TRUE));
+        desc_gen = new_game_desc(params, rs, &aux, FALSE);
+
+        if (!solver_verbose) {
+            char *fmt = game_text_format(new_game(NULL, params, desc_gen));
+            fputs(fmt, stdout);
+            sfree(fmt);
+        }
+
+        printf("Game ID: %s\n", desc_gen);
+    } else {
+        game_state *input;
+        struct unruly_scratch *scratch;
+        int maxdiff, errcode;
+
+        err = validate_desc(params, desc);
+        if (err) {
+            fprintf(stderr, "Description is invalid\n");
+            fprintf(stderr, "%s", err);
+            exit(1);
+        }
+
+        input = new_game(NULL, params, desc);
+        scratch = unruly_new_scratch(input);
+
+        maxdiff = unruly_solve_game(input, scratch, DIFFCOUNT);
+
+        errcode = unruly_validate_counts(input, scratch, NULL);
+        if (unruly_validate_all_rows(input, NULL) == -1)
+            errcode = -1;
+
+        if (errcode != -1) {
+            char *fmt = game_text_format(input);
+            fputs(fmt, stdout);
+            sfree(fmt);
+            if (maxdiff < 0)
+                printf("Difficulty: already solved!\n");
+            else
+                printf("Difficulty: %s\n", unruly_diffnames[maxdiff]);
+        }
+
+        if (errcode == 1)
+            printf("No solution found.\n");
+        else if (errcode == -1)
+            printf("Puzzle is invalid.\n");
+
+        free_game(input);
+        unruly_free_scratch(scratch);
+    }
+
+    return 0;
+}
+#endif