shithub: puzzles

Download patch

ref: 76d50e69059aeebe8b47089615518a8b9e225d40
parent: 7cb29412c1a57a07730c1fee0c9e9fbfebb0b6b2
author: Simon Tatham <anakin@pobox.com>
date: Mon Jun 27 15:34:54 EDT 2005

Re-architecting of the game backend interface. make_move() has been
split into two functions. The first, interpret_move(), takes all the
arguments that make_move() used to get and may have the usual side
effects of modifying the game_ui, but instead of returning a
modified game_state it instead returns a string description of the
move to be made. This string description is then passed to a second
function, execute_move(), together with an input game_state, which
is responsible for actually producing the new state. (solve_game()
also returns a string to be passed to execute_move().)

The point of this is to work towards being able to serialise the
whole of a game midend into a byte stream such as a disk file, which
will eventually support save and load functions in the desktop
puzzles, as well as restoring half-finished games after a quit and
restart in James Harvey's Palm port. Making each game supply a
convert-to-string function for its game_state format would have been
an unreliable way to do this, since those functions would not have
been used in normal play, so they'd only have been tested when you
actually tried to save and load - a recipe for latent bugs if ever I
heard one. This way, you won't even be able to _make_ a move if
execute_move() doesn't work properly, which means that if you can
play a game at all I can have pretty high confidence that
serialising it will work first time.

This is only the groundwork; there will be more checkins to come on
this theme. But the major upheaval should now be done, and as far as
I can tell everything's still working normally.

[originally from svn r6024]

--- a/cube.c
+++ b/cube.c
@@ -984,8 +984,8 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
     return NULL;
 }
@@ -1014,17 +1014,67 @@
     int ox, oy;                        /* pixel position of float origin */
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-			     int x, int y, int button)
+/*
+ * Code shared between interpret_move() and execute_move().
+ */
+static int find_move_dest(game_state *from, int direction,
+			  int *skey, int *dkey)
 {
-    int direction;
-    int pkey[2], skey[2], dkey[2];
+    int mask, dest, i, j;
     float points[4];
-    game_state *ret;
-    float angle;
-    int i, j, dest, mask;
-    struct solid *poly;
 
+    /*
+     * Find the two points in the current grid square which
+     * correspond to this move.
+     */
+    mask = from->squares[from->current].directions[direction];
+    if (mask == 0)
+        return -1;
+    for (i = j = 0; i < from->squares[from->current].npoints; i++)
+        if (mask & (1 << i)) {
+            points[j*2] = from->squares[from->current].points[i*2];
+            points[j*2+1] = from->squares[from->current].points[i*2+1];
+            skey[j] = i;
+            j++;
+        }
+    assert(j == 2);
+
+    /*
+     * Now find the other grid square which shares those points.
+     * This is our move destination.
+     */
+    dest = -1;
+    for (i = 0; i < from->nsquares; i++)
+        if (i != from->current) {
+            int match = 0;
+            float dist;
+
+            for (j = 0; j < from->squares[i].npoints; j++) {
+                dist = (SQ(from->squares[i].points[j*2] - points[0]) +
+                        SQ(from->squares[i].points[j*2+1] - points[1]));
+                if (dist < 0.1)
+                    dkey[match++] = j;
+                dist = (SQ(from->squares[i].points[j*2] - points[2]) +
+                        SQ(from->squares[i].points[j*2+1] - points[3]));
+                if (dist < 0.1)
+                    dkey[match++] = j;
+            }
+
+            if (match == 2) {
+                dest = i;
+                break;
+            }
+        }
+
+    return dest;
+}
+
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
+{
+    int direction, mask, i;
+    int skey[2], dkey[2];
+
     button = button & (~MOD_MASK | MOD_NUM_KEYPAD);
 
     /*
@@ -1056,8 +1106,8 @@
         int cx, cy;
         double angle;
 
-        cx = from->squares[from->current].x * GRID_SCALE + ds->ox;
-        cy = from->squares[from->current].y * GRID_SCALE + ds->oy;
+        cx = state->squares[state->current].x * GRID_SCALE + ds->ox;
+        cy = state->squares[state->current].y * GRID_SCALE + ds->oy;
 
         if (x == cx && y == cy)
             return NULL;               /* clicked in exact centre!  */
@@ -1082,7 +1132,7 @@
          * x-axis, not anticlockwise as most mathematicians would
          * instinctively assume.
          */
-        if (from->squares[from->current].npoints == 4) {
+        if (state->squares[state->current].npoints == 4) {
             /* Square. */
             if (fabs(angle) > 3*PI/4)
                 direction = LEFT;
@@ -1092,7 +1142,7 @@
                 direction = DOWN;
             else
                 direction = UP;
-        } else if (from->squares[from->current].directions[UP] == 0) {
+        } else if (state->squares[state->current].directions[UP] == 0) {
             /* Up-pointing triangle. */
             if (angle < -PI/2 || angle > 5*PI/6)
                 direction = LEFT;
@@ -1102,7 +1152,7 @@
                 direction = RIGHT;
         } else {
             /* Down-pointing triangle. */
-            assert(from->squares[from->current].directions[DOWN] == 0);
+            assert(state->squares[state->current].directions[DOWN] == 0);
             if (angle > PI/2 || angle < -5*PI/6)
                 direction = LEFT;
             else if (angle < -PI/6)
@@ -1113,54 +1163,57 @@
     } else
         return NULL;
 
-    /*
-     * Find the two points in the current grid square which
-     * correspond to this move.
-     */
-    mask = from->squares[from->current].directions[direction];
+    mask = state->squares[state->current].directions[direction];
     if (mask == 0)
         return NULL;
-    for (i = j = 0; i < from->squares[from->current].npoints; i++)
-        if (mask & (1 << i)) {
-            points[j*2] = from->squares[from->current].points[i*2];
-            points[j*2+1] = from->squares[from->current].points[i*2+1];
-            skey[j] = i;
-            j++;
-        }
-    assert(j == 2);
 
     /*
-     * Now find the other grid square which shares those points.
-     * This is our move destination.
+     * Translate diagonal directions into orthogonal ones.
      */
-    dest = -1;
-    for (i = 0; i < from->nsquares; i++)
-        if (i != from->current) {
-            int match = 0;
-            float dist;
+    if (direction > DOWN) {
+	for (i = LEFT; i <= DOWN; i++)
+	    if (state->squares[state->current].directions[i] == mask) {
+		direction = i;
+		break;
+	    }
+	assert(direction <= DOWN);
+    }
 
-            for (j = 0; j < from->squares[i].npoints; j++) {
-                dist = (SQ(from->squares[i].points[j*2] - points[0]) +
-                        SQ(from->squares[i].points[j*2+1] - points[1]));
-                if (dist < 0.1)
-                    dkey[match++] = j;
-                dist = (SQ(from->squares[i].points[j*2] - points[2]) +
-                        SQ(from->squares[i].points[j*2+1] - points[3]));
-                if (dist < 0.1)
-                    dkey[match++] = j;
-            }
+    if (find_move_dest(state, direction, skey, dkey) < 0)
+	return NULL;
 
-            if (match == 2) {
-                dest = i;
-                break;
-            }
-        }
+    if (direction == LEFT)  return dupstr("L");
+    if (direction == RIGHT) return dupstr("R");
+    if (direction == UP)    return dupstr("U");
+    if (direction == DOWN)  return dupstr("D");
 
+    return NULL;		       /* should never happen */
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    game_state *ret;
+    float angle;
+    struct solid *poly;
+    int pkey[2];
+    int skey[2], dkey[2];
+    int i, j, dest;
+    int direction;
+
+    switch (*move) {
+      case 'L': direction = LEFT; break;
+      case 'R': direction = RIGHT; break;
+      case 'U': direction = UP; break;
+      case 'D': direction = DOWN; break;
+      default: return NULL;
+    }
+
+    dest = find_move_dest(from, direction, skey, dkey);
     if (dest < 0)
         return NULL;
 
     ret = dup_game(from);
-    ret->current = i;
+    ret->current = dest;
 
     /*
      * So we know what grid square we're aiming for, and we also
@@ -1662,7 +1715,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/fifteen.c
+++ b/fifteen.c
@@ -379,26 +379,10 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    game_state *ret = dup_game(state);
-    int i;
-
-    /*
-     * Simply replace the grid with a solved one. For this game,
-     * this isn't a useful operation for actually telling the user
-     * what they should have done, but it is useful for
-     * conveniently being able to get hold of a clean state from
-     * which to practise manoeuvres.
-     */
-    for (i = 0; i < ret->n; i++)
-	ret->tiles[i] = (i+1) % ret->n;
-    ret->gap_pos = ret->n-1;
-    ret->used_solve = ret->just_used_solve = TRUE;
-    ret->completed = ret->movecount = 1;
-
-    return ret;
+    return dupstr("S");
 }
 
 static char *game_text_format(game_state *state)
@@ -464,28 +448,29 @@
     int tilesize;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button) {
-    int gx, gy, dx, dy, ux, uy, up, p;
-    game_state *ret;
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
+{
+    int gx, gy, dx, dy;
+    char buf[80];
 
     button &= ~MOD_MASK;
 
-    gx = X(from, from->gap_pos);
-    gy = Y(from, from->gap_pos);
+    gx = X(state, state->gap_pos);
+    gy = Y(state, state->gap_pos);
 
     if (button == CURSOR_RIGHT && gx > 0)
         dx = gx - 1, dy = gy;
-    else if (button == CURSOR_LEFT && gx < from->w-1)
+    else if (button == CURSOR_LEFT && gx < state->w-1)
         dx = gx + 1, dy = gy;
     else if (button == CURSOR_DOWN && gy > 0)
         dy = gy - 1, dx = gx;
-    else if (button == CURSOR_UP && gy < from->h-1)
+    else if (button == CURSOR_UP && gy < state->h-1)
         dy = gy + 1, dx = gx;
     else if (button == LEFT_BUTTON) {
         dx = FROMCOORD(x);
         dy = FROMCOORD(y);
-        if (dx < 0 || dx >= from->w || dy < 0 || dy >= from->h)
+        if (dx < 0 || dx >= state->w || dy < 0 || dy >= state->h)
             return NULL;               /* out of bounds */
         /*
          * Any click location should be equal to the gap location
@@ -496,6 +481,45 @@
     } else
         return NULL;                   /* no move */
 
+    sprintf(buf, "M%d,%d", dx, dy);
+    return dupstr(buf);
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    int gx, gy, dx, dy, ux, uy, up, p;
+    game_state *ret;
+
+    if (!strcmp(move, "S")) {
+	int i;
+
+	ret = dup_game(from);
+
+	/*
+	 * Simply replace the grid with a solved one. For this game,
+	 * this isn't a useful operation for actually telling the user
+	 * what they should have done, but it is useful for
+	 * conveniently being able to get hold of a clean state from
+	 * which to practise manoeuvres.
+	 */
+	for (i = 0; i < ret->n; i++)
+	    ret->tiles[i] = (i+1) % ret->n;
+	ret->gap_pos = ret->n-1;
+	ret->used_solve = ret->just_used_solve = TRUE;
+	ret->completed = ret->movecount = 1;
+
+	return ret;
+    }
+
+    gx = X(from, from->gap_pos);
+    gy = Y(from, from->gap_pos);
+
+    if (move[0] != 'M' ||
+	sscanf(move+1, "%d,%d", &dx, &dy) != 2 ||
+	(dx == gx && dy == gy) || (dx != gx && dy != gy) ||
+	dx < 0 || dx >= from->w || dy < 0 || dy >= from->h)
+	return NULL;
+
     /*
      * Find the unit displacement from the original gap
      * position towards this one.
@@ -859,7 +883,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/flip.c
+++ b/flip.c
@@ -675,8 +675,8 @@
 	row1[i] ^= row2[i];
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
     int w = state->w, h = state->h, wh = w * h;
     unsigned char *equations, *solution, *shortest;
@@ -683,7 +683,7 @@
     int *und, nund;
     int rowsdone, colsdone;
     int i, j, k, len, bestlen;
-    game_state *ret;
+    char *ret;
 
     /*
      * Set up a list of simultaneous equations. Each one is of
@@ -840,17 +840,14 @@
     }
 
     /*
-     * We have a solution. Produce a game state with the solution
-     * marked in annotations.
+     * We have a solution. Produce a move string encoding the
+     * solution.
      */
-    ret = dup_game(currstate);
-    ret->hints_active = TRUE;
-    ret->cheated = TRUE;
-    for (i = 0; i < wh; i++) {
-	ret->grid[i] &= ~2;
-	if (shortest[i])
-	    ret->grid[i] |= 2;
-    }
+    ret = snewn(wh + 2, char);
+    ret[0] = 'S';
+    for (i = 0; i < wh; i++)
+	ret[i+1] = shortest[i] ? '1' : '0';
+    ret[wh+1] = '\0';
 
     sfree(shortest);
     sfree(solution);
@@ -885,41 +882,68 @@
     int tilesize;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
-    int w = from->w, h = from->h, wh = w * h;
-    game_state *ret;
+    int w = state->w, h = state->h /*, wh = w * h */;
+    char buf[80];
 
     if (button == LEFT_BUTTON) {
         int tx = FROMCOORD(x), ty = FROMCOORD(y);
         if (tx >= 0 && tx < w && ty >= 0 && ty < h) {
-            int i, j, done;
+	    sprintf(buf, "M%d,%d", tx, ty);
+	    return dupstr(buf);
+        }
+    }
 
-            ret = dup_game(from);
+    return NULL;
+}
 
-	    if (!ret->completed)
-		ret->moves++;
+static game_state *execute_move(game_state *from, char *move)
+{
+    int w = from->w, h = from->h, wh = w * h;
+    game_state *ret;
+    int x, y;
 
-            i = ty * w + tx;
+    if (move[0] == 'S' && strlen(move) == wh+1) {
+	int i;
 
-	    done = TRUE;
-            for (j = 0; j < wh; j++) {
-                ret->grid[j] ^= ret->matrix->matrix[i*wh+j];
-		if (ret->grid[j] & 1)
-		    done = FALSE;
-	    }
-	    ret->grid[i] ^= 2;	       /* toggle hint */
-	    if (done) {
-		ret->completed = TRUE;
-		ret->hints_active = FALSE;
-	    }
+	ret = dup_game(from);
+	ret->hints_active = TRUE;
+	ret->cheated = TRUE;
+	for (i = 0; i < wh; i++) {
+	    ret->grid[i] &= ~2;
+	    if (move[i+1] != '0')
+		ret->grid[i] |= 2;
+	}
+	return ret;
+    } else if (move[0] == 'M' &&
+	       sscanf(move+1, "%d,%d", &x, &y) == 2 &&
+	x >= 0 && x < w && y >= 0 && y < h) {
+	int i, j, done;
 
-            return ret;
-        }
-    }
+	ret = dup_game(from);
 
-    return NULL;
+	if (!ret->completed)
+	    ret->moves++;
+
+	i = y * w + x;
+
+	done = TRUE;
+	for (j = 0; j < wh; j++) {
+	    ret->grid[j] ^= ret->matrix->matrix[i*wh+j];
+	    if (ret->grid[j] & 1)
+		done = FALSE;
+	}
+	ret->grid[i] ^= 2;	       /* toggle hint */
+	if (done) {
+	    ret->completed = TRUE;
+	    ret->hints_active = FALSE;
+	}
+
+	return ret;
+    } else
+	return NULL;		       /* can't parse move string */
 }
 
 /* ----------------------------------------------------------------------
@@ -1206,7 +1230,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/guess.c
+++ b/guess.c
@@ -364,12 +364,10 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-                              game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    game_state *ret = dup_game(currstate);
-    ret->solved = 1;
-    return ret;
+    return dupstr("S");
 }
 
 static char *game_text_format(game_state *state)
@@ -548,40 +546,45 @@
     return nc_place;
 }
 
-static game_state *mark_move(game_state *from, game_ui *ui)
+static char *encode_move(game_state *from, game_ui *ui)
 {
-    int i, ncleared = 0, nc_place;
-    game_state *to = dup_game(from);
+    char *buf, *p, *sep;
+    int len, i, solved;
+    pegrow tmppegs;
 
-    for (i = 0; i < to->solution->npegs; i++) {
-        to->guesses[from->next_go]->pegs[i] = ui->curr_pegs->pegs[i];
+    len = ui->curr_pegs->npegs * 20 + 2;
+    buf = snewn(len, char);
+    p = buf;
+    *p++ = 'G';
+    sep = "";
+    for (i = 0; i < ui->curr_pegs->npegs; i++) {
+	p += sprintf(p, "%s%d", sep, ui->curr_pegs->pegs[i]);
+	sep = ",";
     }
-    nc_place = mark_pegs(to->guesses[from->next_go], to->solution, to->params.ncolours);
+    *p++ = '\0';
+    assert(p - buf <= len);
+    buf = sresize(buf, len, char);
 
-    if (nc_place == to->solution->npegs) {
-        to->solved = 1; /* win! */
-    } else {
-        to->next_go = from->next_go + 1;
-        if (to->next_go >= to->params.nguesses)
-            to->solved = 1; /* 'lose' so we show the pegs. */
-    }
+    tmppegs = dup_pegrow(ui->curr_pegs);
+    solved = mark_pegs(tmppegs, from->solution, from->params.ncolours);
+    solved = (solved == from->params.ncolours);
+    free_pegrow(tmppegs);
 
-    for (i = 0; i < to->solution->npegs; i++) {
-        if (!ui->holds[i] || to->solved) {
-            ui->curr_pegs->pegs[i] = 0;
-            ncleared++;
-        }
-        if (to->solved) ui->holds[i] = 0;
+    for (i = 0; i < from->solution->npegs; i++) {
+	if (!ui->holds[i] || solved) {
+	    ui->curr_pegs->pegs[i] = 0;
+	}
+	if (solved) ui->holds[i] = 0;
     }
     ui->markable = is_markable(&from->params, ui->curr_pegs);
-    if (!ui->markable && ui->peg_cur == to->solution->npegs)
-        ui->peg_cur--;
+    if (!ui->markable && ui->peg_cur == from->solution->npegs)
+	ui->peg_cur--;
 
-    return to;
+    return buf;
 }
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static char *interpret_move(game_state *from, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
     int over_col = 0;           /* one-indexed */
     int over_guess = -1;        /* zero-indexed */
@@ -588,7 +591,7 @@
     int over_past_guess_y = -1; /* zero-indexed */
     int over_past_guess_x = -1; /* zero-indexed */
     int over_hint = 0;          /* zero or one */
-    game_state *ret = NULL;
+    char *ret = NULL;
 
     int guess_ox = GUESS_X(from->next_go, 0);
     int guess_oy = GUESS_Y(from->next_go, 0);
@@ -643,13 +646,13 @@
             ui->drag_y = y;
             debug(("Start dragging, col = %d, (%d,%d)",
                    ui->drag_col, ui->drag_x, ui->drag_y));
-            ret = from;
+            ret = "";
         }
     } else if (button == LEFT_DRAG && ui->drag_col) {
         ui->drag_x = x;
         ui->drag_y = y;
         debug(("Keep dragging, (%d,%d)", ui->drag_x, ui->drag_y));
-        ret = from;
+        ret = "";
     } else if (button == LEFT_RELEASE && ui->drag_col) {
         if (over_guess > -1) {
             debug(("Dropping colour %d onto guess peg %d",
@@ -666,18 +669,18 @@
         ui->drag_opeg = -1;
         ui->display_cur = 0;
         debug(("Stop dragging."));
-        ret = from;
+        ret = "";
     } else if (button == RIGHT_BUTTON) {
         if (over_guess > -1) {
             /* we use ths feedback in the game_ui to signify
              * 'carry this peg to the next guess as well'. */
             ui->holds[over_guess] = 1 - ui->holds[over_guess];
-            ret = from;
+            ret = "";
         }
     } else if (button == LEFT_RELEASE && over_hint && ui->markable) {
         /* NB this won't trigger if on the end of a drag; that's on
          * purpose, in case you drop by mistake... */
-        ret = mark_move(from, ui);
+        ret = encode_move(from, ui);
     }
 
     /* keyboard input */
@@ -687,7 +690,7 @@
             ui->colour_cur++;
         if (button == CURSOR_UP && ui->colour_cur > 0)
             ui->colour_cur--;
-        ret = from;
+        ret = "";
     } else if (button == CURSOR_LEFT || button == CURSOR_RIGHT) {
         int maxcur = from->params.npegs;
         if (ui->markable) maxcur++;
@@ -697,24 +700,65 @@
             ui->peg_cur++;
         if (button == CURSOR_LEFT && ui->peg_cur > 0)
             ui->peg_cur--;
-        ret = from;
+        ret = "";
     } else if (button == CURSOR_SELECT || button == ' ' || button == '\r' ||
                button == '\n') {
         ui->display_cur = 1;
         if (ui->peg_cur == from->params.npegs) {
-            ret = mark_move(from, ui);
+            ret = encode_move(from, ui);
         } else {
             set_peg(&from->params, ui, ui->peg_cur, ui->colour_cur+1);
-            ret = from;
+            ret = "";
         }
     } else if (button == 'H' || button == 'h') {
         ui->display_cur = 1;
         ui->holds[ui->peg_cur] = 1 - ui->holds[ui->peg_cur];
-        ret = from;
+        ret = "";
     }
     return ret;
 }
 
+static game_state *execute_move(game_state *from, char *move)
+{
+    int i, nc_place;
+    game_state *ret;
+    char *p;
+
+    if (!strcmp(move, "S")) {
+	ret = dup_game(from);
+	ret->solved = 1;
+	return ret;
+    } else if (move[0] == 'G') {
+	p = move+1;
+
+	ret = dup_game(from);
+
+	for (i = 0; i < from->solution->npegs; i++) {
+	    int val = atoi(p);
+	    if (val <= 0 || val > from->params.ncolours) {
+		free_game(ret);
+		return NULL;
+	    }
+	    ret->guesses[from->next_go]->pegs[i] = atoi(p);
+	    while (*p && isdigit((unsigned char)*p)) p++;
+	    if (*p == ',') p++;
+	}
+
+	nc_place = mark_pegs(ret->guesses[from->next_go], ret->solution, ret->params.ncolours);
+
+	if (nc_place == ret->solution->npegs) {
+	    ret->solved = 1; /* win! */
+	} else {
+	    ret->next_go = from->next_go + 1;
+	    if (ret->next_go >= ret->params.nguesses)
+		ret->solved = 1; /* 'lose' so we show the pegs. */
+	}
+
+	return ret;
+    } else
+	return NULL;
+}
+
 /* ----------------------------------------------------------------------
  * Drawing routines.
  */
@@ -1217,7 +1261,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/midend.c
+++ b/midend.c
@@ -367,9 +367,22 @@
 	ret = 0;
 	goto done;
     } else {
-        game_state *s =
-            me->ourgame->make_move(me->states[me->statepos-1].state,
-                                   me->ui, me->drawstate, x, y, button);
+        game_state *s;
+	char *movestr;
+	
+	movestr =
+            me->ourgame->interpret_move(me->states[me->statepos-1].state,
+					me->ui, me->drawstate, x, y, button);
+	if (!movestr)
+	    s = NULL;
+	else if (!*movestr)
+	    s = me->states[me->statepos-1].state;
+	else {
+	    s = me->ourgame->execute_move(me->states[me->statepos-1].state,
+					  movestr);
+	    assert(s != NULL);
+	    sfree(movestr);
+	}
 
         if (s == me->states[me->statepos-1].state) {
             /*
@@ -938,7 +951,7 @@
 char *midend_solve(midend_data *me)
 {
     game_state *s;
-    char *msg;
+    char *msg, *movestr;
 
     if (!me->ourgame->can_solve)
 	return "This game does not support the Solve operation";
@@ -947,11 +960,14 @@
 	return "No game set up to solve";   /* _shouldn't_ happen! */
 
     msg = "Solve operation failed";    /* game _should_ overwrite on error */
-    s = me->ourgame->solve(me->states[0].state,
-			   me->states[me->statepos-1].state,
-			   me->aux_info, &msg);
-    if (!s)
+    movestr = me->ourgame->solve(me->states[0].state,
+				 me->states[me->statepos-1].state,
+				 me->aux_info, &msg);
+    if (!movestr)
 	return msg;
+    s = me->ourgame->execute_move(me->states[me->statepos-1].state, movestr);
+    assert(s);
+    sfree(movestr);
 
     /*
      * Now enter the solved state as the next move.
--- a/mines.c
+++ b/mines.c
@@ -2279,46 +2279,15 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    /*
-     * Simply expose the entire grid as if it were a completed
-     * solution.
-     */
-    game_state *ret;
-    int yy, xx;
-
     if (!state->layout->mines) {
-        *error = "Game has not been started yet";
-        return NULL;
+	*error = "Game has not been started yet";
+	return NULL;
     }
 
-    ret = dup_game(state);
-    for (yy = 0; yy < ret->h; yy++)
-        for (xx = 0; xx < ret->w; xx++) {
-
-            if (ret->layout->mines[yy*ret->w+xx]) {
-                ret->grid[yy*ret->w+xx] = -1;
-            } else {
-                int dx, dy, v;
-
-                v = 0;
-
-                for (dx = -1; dx <= +1; dx++)
-                    for (dy = -1; dy <= +1; dy++)
-                        if (xx+dx >= 0 && xx+dx < ret->w &&
-                            yy+dy >= 0 && yy+dy < ret->h &&
-                            ret->layout->mines[(yy+dy)*ret->w+(xx+dx)])
-                            v++;
-
-                ret->grid[yy*ret->w+xx] = v;
-            }
-        }
-    ret->used_solve = ret->just_used_solve = TRUE;
-    ret->won = TRUE;
-
-    return ret;
+    return dupstr("S");
 }
 
 static char *game_text_format(game_state *state)
@@ -2390,11 +2359,11 @@
      */
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static char *interpret_move(game_state *from, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
-    game_state *ret;
     int cx, cy;
+    char buf[256];
 
     if (from->dead || from->won)
 	return NULL;		       /* no further moves permitted */
@@ -2418,7 +2387,7 @@
 	ui->hx = cx;
 	ui->hy = cy;
 	ui->hradius = (from->grid[cy*from->w+cx] >= 0 ? 1 : 0);
-	return from;
+	return "";
     }
 
     if (button == RIGHT_BUTTON) {
@@ -2436,11 +2405,8 @@
 	    from->grid[cy * from->w + cx] != -1)
 	    return NULL;
 
-	ret = dup_game(from);
-        ret->just_used_solve = FALSE;
-	ret->grid[cy * from->w + cx] ^= (-2 ^ -1);
-
-	return ret;
+	sprintf(buf, "F%d,%d", cx, cy);
+	return dupstr(buf);
     }
 
     if (button == LEFT_RELEASE || button == MIDDLE_RELEASE) {
@@ -2449,10 +2415,10 @@
 
 	/*
 	 * At this stage we must never return NULL: we have adjusted
-	 * the ui, so at worst we return `from'.
+	 * the ui, so at worst we return "".
 	 */
 	if (cx < 0 || cx >= from->w || cy < 0 || cy >= from->h)
-	    return from;
+	    return "";
 
 	/*
 	 * Left-clicking on a covered square opens a tile. Not
@@ -2462,12 +2428,12 @@
 	if (button == LEFT_RELEASE &&
 	    (from->grid[cy * from->w + cx] == -2 ||
 	     from->grid[cy * from->w + cx] == -3)) {
-	    ret = dup_game(from);
-            ret->just_used_solve = FALSE;
-	    open_square(ret, cx, cy);
-            if (ret->dead)
-                ui->deaths++;
-	    return ret;
+	    /* Check if you've killed yourself. */
+	    if (from->layout->mines && from->layout->mines[cy * from->w + cx])
+		ui->deaths++;
+
+	    sprintf(buf, "O%d,%d", cx, cy);
+	    return dupstr(buf);
 	}
 
 	/*
@@ -2490,25 +2456,119 @@
 		    }
 
 	    if (n == from->grid[cy * from->w + cx]) {
-		ret = dup_game(from);
-                ret->just_used_solve = FALSE;
+
+		/*
+		 * Now see if any of the squares we're clearing
+		 * contains a mine (which will happen iff you've
+		 * incorrectly marked the mines around the clicked
+		 * square). If so, we open _just_ those squares, to
+		 * reveal as little additional information as we
+		 * can.
+		 */
+		char *p = buf;
+		char *sep = "";
+
 		for (dy = -1; dy <= +1; dy++)
 		    for (dx = -1; dx <= +1; dx++)
+			if (cx+dx >= 0 && cx+dx < from->w &&
+			    cy+dy >= 0 && cy+dy < from->h) {
+			    if (from->grid[(cy+dy)*from->w+(cx+dx)] != -1 &&
+				from->layout->mines &&
+				from->layout->mines[(cy+dy)*from->w+(cx+dx)]) {
+				p += sprintf(p, "%sO%d,%d", sep, cx+dx, cy+dy);
+				sep = ";";
+			    }
+			}
+
+		if (p > buf) {
+		    ui->deaths++;
+		} else {
+		    sprintf(buf, "C%d,%d", cx, cy);
+		}
+
+		return dupstr(buf);
+	    }
+	}
+
+	return "";
+    }
+
+    return NULL;
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    int cy, cx;
+    game_state *ret;
+
+    if (!strcmp(move, "S")) {
+	/*
+	 * Simply expose the entire grid as if it were a completed
+	 * solution.
+	 */
+	int yy, xx;
+
+	ret = dup_game(from);
+	for (yy = 0; yy < ret->h; yy++)
+	    for (xx = 0; xx < ret->w; xx++) {
+
+		if (ret->layout->mines[yy*ret->w+xx]) {
+		    ret->grid[yy*ret->w+xx] = -1;
+		} else {
+		    int dx, dy, v;
+
+		    v = 0;
+
+		    for (dx = -1; dx <= +1; dx++)
+			for (dy = -1; dy <= +1; dy++)
+			    if (xx+dx >= 0 && xx+dx < ret->w &&
+				yy+dy >= 0 && yy+dy < ret->h &&
+				ret->layout->mines[(yy+dy)*ret->w+(xx+dx)])
+				v++;
+
+		    ret->grid[yy*ret->w+xx] = v;
+		}
+	    }
+	ret->used_solve = ret->just_used_solve = TRUE;
+	ret->won = TRUE;
+
+	return ret;
+    } else {
+	ret = dup_game(from);
+	ret->just_used_solve = FALSE;
+
+	while (*move) {
+	    if (move[0] == 'F' &&
+		sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+		cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
+		ret->grid[cy * from->w + cx] ^= (-2 ^ -1);
+	    } else if (move[0] == 'O' &&
+		       sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+		       cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
+		open_square(ret, cx, cy);
+	    } else if (move[0] == 'C' &&
+		       sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+		       cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
+		int dx, dy;
+
+		for (dy = -1; dy <= +1; dy++)
+		    for (dx = -1; dx <= +1; dx++)
 			if (cx+dx >= 0 && cx+dx < ret->w &&
 			    cy+dy >= 0 && cy+dy < ret->h &&
 			    (ret->grid[(cy+dy)*ret->w+(cx+dx)] == -2 ||
 			     ret->grid[(cy+dy)*ret->w+(cx+dx)] == -3))
 			    open_square(ret, cx+dx, cy+dy);
-                if (ret->dead)
-                    ui->deaths++;
-		return ret;
+	    } else {
+		free_game(ret);
+		return NULL;
 	    }
+
+	    while (*move && *move != ';') move++;
+	    if (*move) move++;
 	}
 
-	return from;
+	return ret;
     }
-
-    return NULL;
 }
 
 /* ----------------------------------------------------------------------
@@ -2962,7 +3022,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/net.c
+++ b/net.c
@@ -1666,10 +1666,14 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    game_state *ret;
+    unsigned char *tiles;
+    char *ret;
+    int retlen, retsize;
+    int i;
+    int tiles_need_freeing;
 
     if (!aux) {
 	/*
@@ -1676,18 +1680,73 @@
 	 * Run the internal solver on the provided grid. This might
 	 * not yield a complete solution.
 	 */
-	ret = dup_game(state);
-	net_solver(ret->width, ret->height, ret->tiles,
-		   ret->barriers, ret->wrapping);
+	tiles = snewn(state->width * state->height, unsigned char);
+	memcpy(tiles, state->tiles, state->width * state->height);
+	net_solver(state->width, state->height, tiles,
+		   state->barriers, state->wrapping);
+	tiles_need_freeing = TRUE;
     } else {
-	assert(aux->width == state->width);
-	assert(aux->height == state->height);
-	ret = dup_game(state);
-	memcpy(ret->tiles, aux->tiles, ret->width * ret->height);
-	ret->used_solve = ret->just_used_solve = TRUE;
-	ret->completed = TRUE;
+	tiles = aux->tiles;
+	tiles_need_freeing = FALSE;
     }
 
+    /*
+     * Now construct a string which can be passed to execute_move()
+     * to transform the current grid into the solved one.
+     */
+    retsize = 256;
+    ret = snewn(retsize, char);
+    retlen = 0;
+    ret[retlen++] = 'S';
+
+    for (i = 0; i < state->width * state->height; i++) {
+	int from = currstate->tiles[i], to = tiles[i];
+	int ft = from & (R|L|U|D), tt = to & (R|L|U|D);
+	int x = i % state->width, y = i / state->width;
+	int chr = '\0';
+	char buf[80], *p = buf;
+
+	if (from == to)
+	    continue;		       /* nothing needs doing at all */
+
+	/*
+	 * To transform this tile into the desired tile: first
+	 * unlock the tile if it's locked, then rotate it if
+	 * necessary, then lock it if necessary.
+	 */
+	if (from & LOCKED)
+	    p += sprintf(p, ";L%d,%d", x, y);
+
+	if (tt == A(ft))
+	    chr = 'A';
+	else if (tt == C(ft))
+	    chr = 'C';
+	else if (tt == F(ft))
+	    chr = 'F';
+	else {
+	    assert(tt == ft);
+	    chr = '\0';
+	}
+	if (chr)
+	    p += sprintf(p, ";%c%d,%d", chr, x, y);
+
+	if (to & LOCKED)
+	    p += sprintf(p, ";L%d,%d", x, y);
+
+	if (p > buf) {
+	    if (retlen + (p - buf) >= retsize) {
+		retsize = retlen + (p - buf) + 512;
+		ret = sresize(ret, retsize, char);
+	    }
+	    memcpy(ret+retlen, buf, p - buf);
+	    retlen += p - buf;
+	}
+    }
+
+    assert(retlen < retsize);
+    ret[retlen] = '\0';
+    ret = sresize(ret, retlen+1, char);
+
     return ret;
 }
 
@@ -1803,10 +1862,11 @@
 /* ----------------------------------------------------------------------
  * Process a move.
  */
-static game_state *make_move(game_state *state, game_ui *ui,
-                             game_drawstate *ds, int x, int y, int button) {
-    game_state *ret, *nullret;
-    int tx, ty, orig;
+static char *interpret_move(game_state *state, game_ui *ui,
+			    game_drawstate *ds, int x, int y, int button)
+{
+    char *nullret;
+    int tx, ty;
     int shift = button & MOD_SHFT, ctrl = button & MOD_CTRL;
 
     button &= ~MOD_MASK;
@@ -1818,7 +1878,7 @@
 
 	if (ui->cur_visible) {
 	    ui->cur_visible = FALSE;
-	    nullret = state;
+	    nullret = "";
 	}
 
 	/*
@@ -1869,7 +1929,7 @@
             OFFSET(ui->cur_x, ui->cur_y, ui->cur_x, ui->cur_y, dir, state);
             ui->cur_visible = TRUE;
         }
-        return state;		       /* UI activity has occurred */
+        return "";		       /* UI activity has occurred */
     } else if (button == 'a' || button == 's' || button == 'd' ||
 	       button == 'A' || button == 'S' || button == 'D' ||
 	       button == CURSOR_SELECT) {
@@ -1900,13 +1960,9 @@
      * unlocks it.)
      */
     if (button == MIDDLE_BUTTON) {
-
-	ret = dup_game(state);
-	ret->just_used_solve = FALSE;
-	tile(ret, tx, ty) ^= LOCKED;
-	ret->last_rotate_dir = ret->last_rotate_x = ret->last_rotate_y = 0;
-	return ret;
-
+	char buf[80];
+	sprintf(buf, "L%d,%d", tx, ty);
+	return dupstr(buf);
     } else if (button == LEFT_BUTTON || button == RIGHT_BUTTON) {
 
         /*
@@ -1920,49 +1976,115 @@
          * Otherwise, turn the tile one way or the other. Left button
          * turns anticlockwise; right button turns clockwise.
          */
-        ret = dup_game(state);
-	ret->just_used_solve = FALSE;
-        orig = tile(ret, tx, ty);
-        if (button == LEFT_BUTTON) {
-            tile(ret, tx, ty) = A(orig);
-            ret->last_rotate_dir = +1;
-        } else {
-            tile(ret, tx, ty) = C(orig);
-            ret->last_rotate_dir = -1;
-        }
-        ret->last_rotate_x = tx;
-        ret->last_rotate_y = ty;
-
+	char buf[80];
+	sprintf(buf, "%c%d,%d", (button == LEFT_BUTTON ? 'A' : 'C'), tx, ty);
+	return dupstr(buf);
     } else if (button == 'J') {
-
         /*
          * Jumble all unlocked tiles to random orientations.
          */
-        int jx, jy;
-        ret = dup_game(state);
-	ret->just_used_solve = FALSE;
-        for (jy = 0; jy < ret->height; jy++) {
-            for (jx = 0; jx < ret->width; jx++) {
-                if (!(tile(ret, jx, jy) & LOCKED)) {
+
+        int jx, jy, maxlen;
+	char *ret, *p;
+
+	/*
+	 * Maximum string length assumes no int can be converted to
+	 * decimal and take more than 11 digits!
+	 */
+	maxlen = state->width * state->height * 25 + 3;
+
+	ret = snewn(maxlen, char);
+	p = ret;
+	*p++ = 'J';
+
+        for (jy = 0; jy < state->height; jy++) {
+            for (jx = 0; jx < state->width; jx++) {
+                if (!(tile(state, jx, jy) & LOCKED)) {
                     int rot = random_upto(ui->rs, 4);
-                    orig = tile(ret, jx, jy);
-                    tile(ret, jx, jy) = ROT(orig, rot);
+		    if (rot) {
+			p += sprintf(p, ";%c%d,%d", "AFC"[rot-1], jx, jy);
+		    }
                 }
             }
         }
-        ret->last_rotate_dir = 0; /* suppress animation */
-        ret->last_rotate_x = ret->last_rotate_y = 0;
+	*p++ = '\0';
+	assert(p - ret < maxlen);
+	ret = sresize(ret, p - ret, char);
 
+	return ret;
     } else {
-	ret = NULL;  /* placate optimisers which don't understand assert(0) */
-	assert(0);
+	return NULL;
     }
+}
 
+static game_state *execute_move(game_state *from, char *move)
+{
+    game_state *ret;
+    int tx, ty, n, noanim, orig;
+
+    ret = dup_game(from);
+    ret->just_used_solve = FALSE;
+
+    if (move[0] == 'J' || move[0] == 'S') {
+	if (move[0] == 'S')
+	    ret->just_used_solve = ret->used_solve = TRUE;
+
+	move++;
+	if (*move == ';')
+	    move++;
+	noanim = TRUE;
+    } else
+	noanim = FALSE;
+
+    ret->last_rotate_dir = 0;	       /* suppress animation */
+    ret->last_rotate_x = ret->last_rotate_y = 0;
+
+    while (*move) {
+	if ((move[0] == 'A' || move[0] == 'C' ||
+	     move[0] == 'F' || move[0] == 'L') &&
+	    sscanf(move+1, "%d,%d%n", &tx, &ty, &n) >= 2 &&
+	    tx >= 0 && tx < from->width && ty >= 0 && ty < from->height) {
+	    orig = tile(ret, tx, ty);
+	    if (move[0] == 'A') {
+		tile(ret, tx, ty) = A(orig);
+		if (!noanim)
+		    ret->last_rotate_dir = +1;
+	    } else if (move[0] == 'F') {
+		tile(ret, tx, ty) = F(orig);
+		if (!noanim) {
+		    free_game(ret);
+		    return NULL;
+		}
+	    } else if (move[0] == 'C') {
+		tile(ret, tx, ty) = C(orig);
+		if (!noanim)
+		    ret->last_rotate_dir = -1;
+	    } else {
+		assert(move[0] == 'L');
+		tile(ret, tx, ty) ^= LOCKED;
+	    }
+
+	    move += 1 + n;
+	    if (*move == ';') move++;
+	} else {
+	    free_game(ret);
+	    return NULL;
+	}
+    }
+    if (!noanim) {
+	ret->last_rotate_x = tx;
+	ret->last_rotate_y = ty;
+    }
+
     /*
      * Check whether the game has been completed.
+     * 
+     * For this purpose it doesn't matter where the source square
+     * is, because we can start from anywhere and correctly
+     * determine whether the game is completed.
      */
     {
-	unsigned char *active = compute_active(ret, ui->cx, ui->cy);
+	unsigned char *active = compute_active(ret, 0, 0);
 	int x1, y1;
 	int complete = TRUE;
 
@@ -1983,6 +2105,7 @@
     return ret;
 }
 
+
 /* ----------------------------------------------------------------------
  * Routines for drawing the game position on the screen.
  */
@@ -2617,7 +2740,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/netslide.c
+++ b/netslide.c
@@ -893,10 +893,11 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    game_state *ret;
+    char *ret;
+    int i;
 
     if (!aux) {
 	*error = "Solution not known for this puzzle";
@@ -905,11 +906,11 @@
 
     assert(aux->width == state->width);
     assert(aux->height == state->height);
-    ret = dup_game(state);
-    memcpy(ret->tiles, aux->tiles, ret->width * ret->height);
-    ret->used_solve = ret->just_used_solve = TRUE;
-    ret->completed = ret->move_count = 1;
-
+    ret = snewn(aux->width * aux->height + 2, char);
+    ret[0] = 'S';
+    for (i = 0; i < aux->width * aux->height; i++)
+	ret[i+1] = "0123456789abcdef"[aux->tiles[i] & 0xF];
+    ret[i+1] = '\0';
     return ret;
 }
 
@@ -1058,12 +1059,12 @@
     unsigned char *visible;
 };
 
-static game_state *make_move(game_state *state, game_ui *ui,
-                             game_drawstate *ds, int x, int y, int button)
+static char *interpret_move(game_state *state, game_ui *ui,
+			    game_drawstate *ds, int x, int y, int button)
 {
     int cx, cy;
     int n, dx, dy;
-    game_state *ret;
+    char buf[80];
 
     button &= ~MOD_MASK;
 
@@ -1099,16 +1100,59 @@
         dy = -dy;
     }
 
-    ret = dup_game(state);
+    if (dx == 0)
+	sprintf(buf, "C%d,%d", cx, dy);
+    else
+	sprintf(buf, "R%d,%d", cy, dx);
+    return dupstr(buf);
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    game_state *ret;
+    int c, d, col;
+
+    if ((move[0] == 'C' || move[0] == 'R') &&
+	sscanf(move+1, "%d,%d", &c, &d) == 2 &&
+	c >= 0 && c < (move[0] == 'C' ? from->width : from->height)) {
+	col = (move[0] == 'C');
+    } else if (move[0] == 'S' &&
+	       strlen(move) == from->width * from->height + 1) {
+	int i;
+	ret = dup_game(from);
+	ret->used_solve = ret->just_used_solve = TRUE;
+	ret->completed = ret->move_count = 1;
+
+	for (i = 0; i < from->width * from->height; i++) {
+	    c = move[i+1];
+	    if (c >= '0' && c <= '9')
+		c -= '0';
+	    else if (c >= 'A' && c <= 'F')
+		c -= 'A' - 10;
+	    else if (c >= 'a' && c <= 'f')
+		c -= 'a' - 10;
+	    else {
+		free_game(ret);
+		return NULL;
+	    }
+	    ret->tiles[i] = c;
+	}
+	return ret;
+    } else
+	return NULL;		       /* can't parse move string */
+
+    ret = dup_game(from);
     ret->just_used_solve = FALSE;
 
-    if (dx == 0) slide_col(ret, dy, cx);
-    else slide_row(ret, dx, cy);
+    if (col)
+	slide_col(ret, d, c);
+    else
+	slide_row(ret, d, c);
 
     ret->move_count++;
-    ret->last_move_row = dx ? cy : -1;
-    ret->last_move_col = dx ? -1 : cx;
-    ret->last_move_dir = dx + dy;
+    ret->last_move_row = col ? -1 : c;
+    ret->last_move_col = col ? c : -1;
+    ret->last_move_dir = d;
 
     /*
      * See if the game has been completed.
@@ -1779,7 +1823,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/nullgame.c
+++ b/nullgame.c
@@ -122,8 +122,8 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
     return NULL;
 }
@@ -151,12 +151,17 @@
     int FIXME;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
     return NULL;
 }
 
+static game_state *execute_move(game_state *state, char *move)
+{
+    return NULL;
+}
+
 /* ----------------------------------------------------------------------
  * Drawing routines.
  */
@@ -251,7 +256,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/pattern.c
+++ b/pattern.c
@@ -664,27 +664,30 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *ai, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *ai, char **error)
 {
-    game_state *ret;
+    unsigned char *matrix;
+    int matrix_needs_freeing;
+    int full, empty;
+    int w = state->w, h = state->h;
+    int i;
+    char *ret;
 
-    ret = dup_game(state);
-    ret->completed = ret->cheated = TRUE;
-
     /*
      * If we already have the solved state in an aux_info, copy it
      * out.
      */
     if (ai) {
+	assert(ai->w == w && ai->h == h);
 
-	assert(ret->w == ai->w);
-	assert(ret->h == ai->h);
-	memcpy(ret->grid, ai->grid, ai->w * ai->h);
-
+	matrix = ai->grid;
+	matrix_needs_freeing = FALSE;
+	full = GRID_FULL;
+	empty = GRID_EMPTY;
     } else {
-	int w = state->w, h = state->h, i, j, done_any, max;
-	unsigned char *matrix, *workspace;
+	int done_any, max;
+	unsigned char *workspace;
 	int *rowdata;
 
 	matrix = snewn(w*h, unsigned char);
@@ -711,23 +714,33 @@
             }
         } while (done_any);
 
-	for (i = 0; i < h; i++) {
-	    for (j = 0; j < w; j++) {
-		int c = (matrix[i*w+j] == BLOCK ? GRID_FULL :
-			 matrix[i*w+j] == DOT ? GRID_EMPTY : GRID_UNKNOWN);
-		ret->grid[i*w+j] = c;
-		if (c == GRID_UNKNOWN)
-		    ret->completed = FALSE;
+	sfree(workspace);
+	sfree(rowdata);
+
+	for (i = 0; i < w*h; i++) {
+	    if (matrix[i] != BLOCK && matrix[i] != DOT) {
+		sfree(matrix);
+		*error = "Solving algorithm cannot complete this puzzle";
+		return NULL;
 	    }
 	}
 
-	if (!ret->completed) {
-	    free_game(ret);
-	    *error = "Solving algorithm cannot complete this puzzle";
-	    return NULL;
-	}
+	matrix_needs_freeing = TRUE;
+	full = BLOCK;
+	empty = DOT;
     }
 
+    ret = snewn(w*h+2, char);
+    ret[0] = 'S';
+    for (i = 0; i < w*h; i++) {
+	assert(matrix[i] == full || matrix[i] == empty);
+	ret[i+1] = (matrix[i] == full ? '1' : '0');
+    }
+    ret[w*h+1] = '\0';
+
+    if (matrix_needs_freeing)
+	sfree(matrix);
+
     return ret;
 }
 
@@ -772,16 +785,15 @@
     unsigned char *visible;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button) {
-    game_state *ret;
-
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
+{
     button &= ~MOD_MASK;
 
-    x = FROMCOORD(from->w, x);
-    y = FROMCOORD(from->h, y);
+    x = FROMCOORD(state->w, x);
+    y = FROMCOORD(state->h, y);
 
-    if (x >= 0 && x < from->w && y >= 0 && y < from->h &&
+    if (x >= 0 && x < state->w && y >= 0 && y < state->h &&
         (button == LEFT_BUTTON || button == RIGHT_BUTTON ||
          button == MIDDLE_BUTTON)) {
 
@@ -804,7 +816,7 @@
         ui->drag_start_x = ui->drag_end_x = x;
         ui->drag_start_y = ui->drag_end_y = y;
 
-        return from;                   /* UI activity occurred */
+        return "";		       /* UI activity occurred */
     }
 
     if (ui->dragging && button == ui->drag) {
@@ -827,13 +839,13 @@
 
         if (x < 0) x = 0;
         if (y < 0) y = 0;
-        if (x >= from->w) x = from->w - 1;
-        if (y >= from->h) y = from->h - 1;
+        if (x >= state->w) x = state->w - 1;
+        if (y >= state->h) y = state->h - 1;
 
         ui->drag_end_x = x;
         ui->drag_end_y = y;
 
-        return from;                   /* UI activity occurred */
+        return "";		       /* UI activity occurred */
     }
 
     if (ui->dragging && button == ui->release) {
@@ -847,57 +859,94 @@
 
         for (yy = y1; yy <= y2; yy++)
             for (xx = x1; xx <= x2; xx++)
-                if (from->grid[yy * from->w + xx] != ui->state)
+                if (state->grid[yy * state->w + xx] != ui->state)
                     move_needed = TRUE;
 
         ui->dragging = FALSE;
 
         if (move_needed) {
-            ret = dup_game(from);
-            for (yy = y1; yy <= y2; yy++)
-                for (xx = x1; xx <= x2; xx++)
-                    ret->grid[yy * ret->w + xx] = ui->state;
+	    char buf[80];
+	    sprintf(buf, "%c%d,%d,%d,%d",
+		    (ui->state == GRID_FULL ? 'F' :
+		     ui->state == GRID_EMPTY ? 'E' : 'U'),
+		    x1, y1, x2-x1+1, y2-y1+1);
+	    return dupstr(buf);
+        } else
+            return "";		       /* UI activity occurred */
+    }
 
-            /*
-             * An actual change, so check to see if we've completed
-             * the game.
-             */
-            if (!ret->completed) {
-                int *rowdata = snewn(ret->rowsize, int);
-                int i, len;
+    return NULL;
+}
 
-                ret->completed = TRUE;
+static game_state *execute_move(game_state *from, char *move)
+{
+    game_state *ret;
+    int x1, x2, y1, y2, xx, yy;
+    int val;
 
-                for (i=0; i<ret->w; i++) {
-                    len = compute_rowdata(rowdata,
-                                          ret->grid+i, ret->h, ret->w);
-                    if (len != ret->rowlen[i] ||
-                        memcmp(ret->rowdata+i*ret->rowsize, rowdata,
-                               len * sizeof(int))) {
-                        ret->completed = FALSE;
-                        break;
-                    }
-                }
-                for (i=0; i<ret->h; i++) {
-                    len = compute_rowdata(rowdata,
-                                          ret->grid+i*ret->w, ret->w, 1);
-                    if (len != ret->rowlen[i+ret->w] ||
-                        memcmp(ret->rowdata+(i+ret->w)*ret->rowsize, rowdata,
-                               len * sizeof(int))) {
-                        ret->completed = FALSE;
-                        break;
-                    }
-                }
+    if (move[0] == 'S' && strlen(move) == from->w * from->h + 1) {
+	int i;
 
-                sfree(rowdata);
-            }
+	ret = dup_game(from);
 
-            return ret;
-        } else
-            return from;               /* UI activity occurred */
-    }
+	for (i = 0; i < ret->w * ret->h; i++)
+	    ret->grid[i] = (move[i+1] == '1' ? GRID_FULL : GRID_EMPTY);
 
-    return NULL;
+	ret->completed = ret->cheated = TRUE;
+
+	return ret;
+    } else if ((move[0] == 'F' || move[0] == 'E' || move[0] == 'U') &&
+	sscanf(move+1, "%d,%d,%d,%d", &x1, &y1, &x2, &y2) == 4 &&
+	x1 >= 0 && x2 >= 0 && x1+x2 <= from->w &&
+	y1 >= 0 && y2 >= 0 && y1+y2 <= from->h) {
+
+	x2 += x1;
+	y2 += y1;
+	val = (move[0] == 'F' ? GRID_FULL :
+		 move[0] == 'E' ? GRID_EMPTY : GRID_UNKNOWN);
+
+	ret = dup_game(from);
+	for (yy = y1; yy < y2; yy++)
+	    for (xx = x1; xx < x2; xx++)
+		ret->grid[yy * ret->w + xx] = val;
+
+	/*
+	 * An actual change, so check to see if we've completed the
+	 * game.
+	 */
+	if (!ret->completed) {
+	    int *rowdata = snewn(ret->rowsize, int);
+	    int i, len;
+
+	    ret->completed = TRUE;
+
+	    for (i=0; i<ret->w; i++) {
+		len = compute_rowdata(rowdata,
+				      ret->grid+i, ret->h, ret->w);
+		if (len != ret->rowlen[i] ||
+		    memcmp(ret->rowdata+i*ret->rowsize, rowdata,
+			   len * sizeof(int))) {
+		    ret->completed = FALSE;
+		    break;
+		}
+	    }
+	    for (i=0; i<ret->h; i++) {
+		len = compute_rowdata(rowdata,
+				      ret->grid+i*ret->w, ret->w, 1);
+		if (len != ret->rowlen[i+ret->w] ||
+		    memcmp(ret->rowdata+(i+ret->w)*ret->rowsize, rowdata,
+			   len * sizeof(int))) {
+		    ret->completed = FALSE;
+		    break;
+		}
+	    }
+
+	    sfree(rowdata);
+	}
+
+	return ret;
+    } else
+	return NULL;
 }
 
 /* ----------------------------------------------------------------------
@@ -1146,7 +1195,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/puzzles.h
+++ b/puzzles.h
@@ -269,8 +269,8 @@
     game_state *(*dup_game)(game_state *state);
     void (*free_game)(game_state *state);
     int can_solve;
-    game_state *(*solve)(game_state *orig, game_state *curr,
-			 game_aux_info *aux, char **error);
+    char *(*solve)(game_state *orig, game_state *curr,
+		   game_aux_info *aux, char **error);
     int can_format_as_text;
     char *(*text_format)(game_state *state);
     game_ui *(*new_ui)(game_state *state);
@@ -277,8 +277,9 @@
     void (*free_ui)(game_ui *ui);
     void (*changed_state)(game_ui *ui, game_state *oldstate,
                           game_state *newstate);
-    game_state *(*make_move)(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button);
+    char *(*interpret_move)(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button);
+    game_state *(*execute_move)(game_state *state, char *move);
     void (*size)(game_params *params, game_drawstate *ds, int *x, int *y,
                  int expand);
     float *(*colours)(frontend *fe, game_state *state, int *ncolours);
--- a/rect.c
+++ b/rect.c
@@ -323,7 +323,8 @@
 }
 
 static int rect_solver(int w, int h, int nrects, struct numberdata *numbers,
-                       game_state *result, random_state *rs)
+                       unsigned char *hedge, unsigned char *vedge,
+		       random_state *rs)
 {
     struct rectlist *rectpositions;
     int *overlaps, *rectbyplace, *workspace;
@@ -848,25 +849,25 @@
         assert(rectpositions[i].n > 0);
         if (rectpositions[i].n > 1) {
             ret = FALSE;
-	} else if (result) {
-	    /*
-	     * Place the rectangle in its only possible position.
-	     */
-	    int x, y;
-	    struct rect *r = &rectpositions[i].rects[0];
+        } else if (hedge && vedge) {
+            /*
+             * Place the rectangle in its only possible position.
+             */
+            int x, y;
+            struct rect *r = &rectpositions[i].rects[0];
 
-	    for (y = 0; y < r->h; y++) {
-		if (r->x > 0)
-		    vedge(result, r->x, r->y+y) = 1;
-		if (r->x+r->w < result->w)
-		    vedge(result, r->x+r->w, r->y+y) = 1;
-	    }
-	    for (x = 0; x < r->w; x++) {
-		if (r->y > 0)
-		    hedge(result, r->x+x, r->y) = 1;
-		if (r->y+r->h < result->h)
-		    hedge(result, r->x+x, r->y+r->h) = 1;
-	    }
+            for (y = 0; y < r->h; y++) {
+                if (r->x > 0)
+		    vedge[(r->y+y) * w + r->x] = 1;
+                if (r->x+r->w < w)
+		    vedge[(r->y+y) * w + r->x+r->w] = 1;
+            }
+            for (x = 0; x < r->w; x++) {
+                if (r->y > 0)
+                    hedge[r->y * w + r->x+x] = 1;
+                if (r->y+r->h < h)
+                    hedge[(r->y+r->h) * w + r->x+x] = 1;
+            }
 	}
     }
 
@@ -1633,7 +1634,7 @@
 
 	    if (params->unique)
 		ret = rect_solver(params->w, params->h, nnumbers, nd,
-				  NULL, rs);
+				  NULL, NULL, rs);
 	    else
 		ret = TRUE;	       /* allow any number placement at all */
 
@@ -1852,10 +1853,13 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *ai, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *ai, char **error)
 {
-    game_state *ret;
+    unsigned char *vedge, *hedge;
+    int edges_need_freeing;
+    int x, y, len;
+    char *ret, *p;
 
     if (!ai) {
 	int i, j, n;
@@ -1884,10 +1888,13 @@
 
 	assert(j == n);
 
-	ret = dup_game(state);
-	ret->cheated = TRUE;
+	vedge = snewn(state->w * state->h, unsigned char);
+	hedge = snewn(state->w * state->h, unsigned char);
+	memset(vedge, 0, state->w * state->h);
+	memset(hedge, 0, state->w * state->h);
+	edges_need_freeing = TRUE;
 
-	rect_solver(state->w, state->h, n, nd, ret, NULL);
+	rect_solver(state->w, state->h, n, nd, hedge, vedge, NULL);
 
 	/*
 	 * Clean up.
@@ -1895,18 +1902,33 @@
 	for (i = 0; i < n; i++)
 	    sfree(nd[i].points);
 	sfree(nd);
-
-	return ret;
+    } else {
+	assert(state->w == ai->w);
+	assert(state->h == ai->h);
+	vedge = ai->vedge;
+	hedge = ai->hedge;
+	edges_need_freeing = FALSE;
     }
 
-    assert(state->w == ai->w);
-    assert(state->h == ai->h);
+    len = 2 + (state->w-1)*state->h + (state->h-1)*state->w;
+    ret = snewn(len, char);
 
-    ret = dup_game(state);
-    memcpy(ret->vedge, ai->vedge, ai->w * ai->h * sizeof(unsigned char));
-    memcpy(ret->hedge, ai->hedge, ai->w * ai->h * sizeof(unsigned char));
-    ret->cheated = TRUE;
+    p = ret;
+    *p++ = 'S';
+    for (y = 0; y < state->h; y++)
+	for (x = 1; x < state->w; x++)
+	    *p++ = vedge[y*state->w+x] ? '1' : '0';
+    for (y = 1; y < state->h; y++)
+	for (x = 0; x < state->w; x++)
+	    *p++ = hedge[y*state->w+x] ? '1' : '0';
+    *p++ = '\0';
+    assert(p - ret == len);
 
+    if (edges_need_freeing) {
+	sfree(vedge);
+	sfree(hedge);
+    }
+
     return ret;
 }
 
@@ -2232,14 +2254,16 @@
     }
 }
 
-static void ui_draw_rect(game_state *state, game_ui *ui,
-                         unsigned char *hedge, unsigned char *vedge, int c)
+/*
+ * Returns TRUE if it has made any change to the grid.
+ */
+static int grid_draw_rect(game_state *state,
+			  unsigned char *hedge, unsigned char *vedge,
+			  int c, int really,
+			  int x1, int y1, int x2, int y2)
 {
     int x, y;
-    int x1 = ui->x1;
-    int y1 = ui->y1;
-    int x2 = ui->x2;
-    int y2 = ui->y2;
+    int changed = FALSE;
 
     /*
      * Draw horizontal edges of rectangles.
@@ -2252,7 +2276,9 @@
                     val = c;
                 else if (c == 1)
                     val = 0;
-                index(state,hedge,x,y) = val;
+		changed = changed || (index(state,hedge,x,y) != val);
+		if (really)
+		    index(state,hedge,x,y) = val;
             }
 
     /*
@@ -2266,10 +2292,22 @@
                     val = c;
                 else if (c == 1)
                     val = 0;
-                index(state,vedge,x,y) = val;
+		changed = changed || (index(state,vedge,x,y) != val);
+                if (really)
+		    index(state,vedge,x,y) = val;
             }
+
+    return changed;
 }
 
+static int ui_draw_rect(game_state *state, game_ui *ui,
+			unsigned char *hedge, unsigned char *vedge, int c,
+			int really)
+{
+    return grid_draw_rect(state, hedge, vedge, c, really,
+			  ui->x1, ui->y1, ui->x2, ui->y2);
+}
+
 static void game_changed_state(game_ui *ui, game_state *oldstate,
                                game_state *newstate)
 {
@@ -2281,11 +2319,12 @@
     unsigned long *visible;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button) {
+static char *interpret_move(game_state *from, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
+{
     int xc, yc;
     int startdrag = FALSE, enddrag = FALSE, active = FALSE;
-    game_state *ret;
+    char buf[80], *ret;
 
     button &= ~MOD_MASK;
 
@@ -2343,44 +2382,24 @@
     if (enddrag) {
 	if (xc >= 0 && xc <= 2*from->w &&
 	    yc >= 0 && yc <= 2*from->h) {
-	    ret = dup_game(from);
 
 	    if (ui->dragged) {
-		ui_draw_rect(ret, ui, ret->hedge, ret->vedge, 1);
+		if (ui_draw_rect(from, ui, from->hedge,
+				 from->vedge, 1, FALSE)) {
+		    sprintf(buf, "R%d,%d,%d,%d",
+			    ui->x1, ui->y1, ui->x2 - ui->x1, ui->y2 - ui->y1);
+		    ret = dupstr(buf);
+		}
 	    } else {
 		if ((xc & 1) && !(yc & 1) && HRANGE(from,xc/2,yc/2)) {
-		    hedge(ret,xc/2,yc/2) = !hedge(ret,xc/2,yc/2);
+		    sprintf(buf, "H%d,%d", xc/2, yc/2);
+		    ret = dupstr(buf);
 		}
 		if ((yc & 1) && !(xc & 1) && VRANGE(from,xc/2,yc/2)) {
-		    vedge(ret,xc/2,yc/2) = !vedge(ret,xc/2,yc/2);
+		    sprintf(buf, "V%d,%d", xc/2, yc/2);
+		    ret = dupstr(buf);
 		}
 	    }
-
-	    if (!memcmp(ret->hedge, from->hedge, from->w*from->h) &&
-		!memcmp(ret->vedge, from->vedge, from->w*from->h)) {
-		free_game(ret);
-		ret = NULL;
-	    }
-
-            /*
-             * We've made a real change to the grid. Check to see
-             * if the game has been completed.
-             */
-            if (ret && !ret->completed) {
-                int x, y, ok;
-                unsigned char *correct = get_correct(ret);
-
-                ok = TRUE;
-                for (x = 0; x < ret->w; x++)
-                    for (y = 0; y < ret->h; y++)
-                        if (!index(ret, correct, x, y))
-                            ok = FALSE;
-
-                sfree(correct);
-
-                if (ok)
-                    ret->completed = TRUE;
-            }
 	}
 
 	ui->drag_start_x = -1;
@@ -2398,11 +2417,84 @@
     if (ret)
 	return ret;		       /* a move has been made */
     else if (active)
-        return from;                   /* UI activity has occurred */
+        return "";		       /* UI activity has occurred */
     else
 	return NULL;
 }
 
+static game_state *execute_move(game_state *from, char *move)
+{
+    game_state *ret;
+    int x1, y1, x2, y2, mode;
+
+    if (move[0] == 'S') {
+	char *p = move+1;
+	int x, y;
+
+	ret = dup_game(from);
+	ret->cheated = TRUE;
+
+	for (y = 0; y < ret->h; y++)
+	    for (x = 1; x < ret->w; x++) {
+		vedge(ret, x, y) = (*p == '1');
+		if (*p) p++;
+	    }
+	for (y = 1; y < ret->h; y++)
+	    for (x = 0; x < ret->w; x++) {
+		hedge(ret, x, y) = (*p == '1');
+		if (*p) p++;
+	    }
+
+	return ret;
+
+    } else if (move[0] == 'R' &&
+	sscanf(move+1, "%d,%d,%d,%d", &x1, &y1, &x2, &y2) == 4 &&
+	x1 >= 0 && x2 >= 0 && x1+x2 <= from->w &&
+	y1 >= 0 && y2 >= 0 && y1+y2 <= from->h) {
+	x2 += x1;
+	y2 += y1;
+	mode = move[0];
+    } else if ((move[0] == 'H' || move[0] == 'V') &&
+	       sscanf(move+1, "%d,%d", &x1, &y1) == 2 &&
+	       (move[0] == 'H' ? HRANGE(from, x1, y1) :
+		VRANGE(from, x1, y1))) {
+	mode = move[0];
+    } else
+	return NULL;		       /* can't parse move string */
+
+    ret = dup_game(from);
+
+    if (mode == 'R') {
+	grid_draw_rect(ret, ret->hedge, ret->vedge, 1, TRUE, x1, y1, x2, y2);
+    } else if (mode == 'H') {
+	hedge(ret,x1,y1) = !hedge(ret,x1,y1);
+    } else if (mode == 'V') {
+	vedge(ret,x1,y1) = !vedge(ret,x1,y1);
+    }
+
+    /*
+     * We've made a real change to the grid. Check to see
+     * if the game has been completed.
+     */
+    if (!ret->completed) {
+	int x, y, ok;
+	unsigned char *correct = get_correct(ret);
+
+	ok = TRUE;
+	for (x = 0; x < ret->w; x++)
+	    for (y = 0; y < ret->h; y++)
+		if (!index(ret, correct, x, y))
+		    ok = FALSE;
+
+	sfree(correct);
+
+	if (ok)
+	    ret->completed = TRUE;
+    }
+
+    return ret;
+}
+
 /* ----------------------------------------------------------------------
  * Drawing routines.
  */
@@ -2559,7 +2651,7 @@
         vedge = snewn(state->w*state->h, unsigned char);
         memcpy(hedge, state->hedge, state->w*state->h);
         memcpy(vedge, state->vedge, state->w*state->h);
-        ui_draw_rect(state, ui, hedge, vedge, 2);
+        ui_draw_rect(state, ui, hedge, vedge, 2, TRUE);
     } else {
         hedge = state->hedge;
         vedge = state->vedge;
@@ -2708,7 +2800,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/samegame.c
+++ b/samegame.c
@@ -355,8 +355,8 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
     return NULL;
 }
@@ -427,20 +427,37 @@
     sel_clear(ui, newstate);
 }
 
-static void sel_remove(game_ui *ui, game_state *state)
+static char *sel_movedesc(game_ui *ui, game_state *state)
 {
-    int i, nremoved = 0;
+    int i;
+    char *ret, *sep, buf[80];
+    int retlen, retsize;
 
-    state->score += npoints(&state->params, ui->nselected);
+    retsize = 256;
+    ret = snewn(retsize, char);
+    retlen = 0;
+    ret[retlen++] = 'M';
+    sep = "";
 
     for (i = 0; i < state->n; i++) {
 	if (ui->tiles[i] & TILE_SELECTED) {
-	    nremoved++;
-	    state->tiles[i] = 0;
+	    sprintf(buf, "%s%d", sep, i);
+	    sep = ",";
+	    if (retlen + strlen(buf) >= retsize) {
+		retsize = retlen + strlen(buf) + 256;
+		ret = sresize(ret, retsize, char);
+	    }
+	    strcpy(ret + retlen, buf);
+	    retlen += strlen(buf);
+
 	    ui->tiles[i] &= ~TILE_SELECTED;
 	}
     }
     ui->nselected = 0;
+
+    assert(retlen < retsize);
+    ret[retlen++] = '\0';
+    return sresize(ret, retlen, char);
 }
 
 static void sel_expand(game_ui *ui, game_state *state, int tx, int ty)
@@ -567,11 +584,13 @@
     int *tiles; /* contains colour and SELECTED. */
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static game_state *execute_move(game_state *from, char *move);
+
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
     int tx, ty;
-    game_state *ret = from;
+    char *ret = "";
 
     ui->displaysel = 0;
 
@@ -583,8 +602,8 @@
 	ui->displaysel = 1;
 	dx = (button == CURSOR_LEFT) ? -1 : ((button == CURSOR_RIGHT) ? +1 : 0);
 	dy = (button == CURSOR_DOWN) ? +1 : ((button == CURSOR_UP)    ? -1 : 0);
-	ui->xsel = (ui->xsel + from->params.w + dx) % from->params.w;
-	ui->ysel = (ui->ysel + from->params.h + dy) % from->params.h;
+	ui->xsel = (ui->xsel + state->params.w + dx) % state->params.w;
+	ui->ysel = (ui->ysel + state->params.h + dy) % state->params.h;
 	return ret;
     } else if (button == CURSOR_SELECT || button == ' ' || button == '\r' ||
 	       button == '\n') {
@@ -594,30 +613,71 @@
     } else
 	return NULL;
 
-    if (tx < 0 || tx >= from->params.w || ty < 0 || ty >= from->params.h)
+    if (tx < 0 || tx >= state->params.w || ty < 0 || ty >= state->params.h)
 	return NULL;
-    if (COL(from, tx, ty) == 0) return NULL;
+    if (COL(state, tx, ty) == 0) return NULL;
 
     if (ISSEL(ui,tx,ty)) {
 	if (button == RIGHT_BUTTON)
-	    sel_clear(ui, from);
+	    sel_clear(ui, state);
 	else {
-	    /* this is the actual move. */
-	    ret = dup_game(from);
-	    sel_remove(ui, ret);
-	    sg_snuggle(ret); /* shifts blanks down and to the left */
-	    sg_check(ret);   /* checks for completeness or impossibility */
+	    game_state *tmp;
+
+	    ret = sel_movedesc(ui, state);
+
+	    /*
+	     * Unfortunately, we must check for completeness or
+	     * impossibility now, in order to update the game_ui;
+	     * and we can't do that without constructing the new
+	     * grid. Sigh.
+	     */
+	    tmp = execute_move(state, ret);
+	    if (tmp->complete || tmp->impossible)
+		ui->displaysel = 0;
+	    free_game(tmp);
 	}
     } else {
-	sel_clear(ui, from); /* might be no-op */
-	sel_expand(ui, from, tx, ty);
+	sel_clear(ui, state); /* might be no-op */
+	sel_expand(ui, state, tx, ty);
     }
-    if (ret->complete || ret->impossible)
-	ui->displaysel = 0;
 
     return ret;
 }
 
+static game_state *execute_move(game_state *from, char *move)
+{
+    int i, n;
+    game_state *ret;
+
+    if (move[0] == 'M') {
+	ret = dup_game(from);
+
+	n = 0;
+	move++;
+
+	while (*move) {
+	    i = atoi(move);
+	    if (i < 0 || i >= ret->n) {
+		free_game(ret);
+		return NULL;
+	    }
+	    n++;
+	    ret->tiles[i] = 0;
+
+	    while (*move && isdigit((unsigned char)*move)) move++;
+	    if (*move == ',') move++;
+	}
+
+	ret->score += npoints(&ret->params, n);
+
+	sg_snuggle(ret); /* shifts blanks down and to the left */
+	sg_check(ret);   /* checks for completeness or impossibility */
+
+	return ret;
+    } else
+	return NULL;		       /* couldn't parse move string */
+}
+
 /* ----------------------------------------------------------------------
  * Drawing routines.
  */
@@ -940,7 +1000,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/sixteen.c
+++ b/sixteen.c
@@ -510,25 +510,10 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    game_state *ret = dup_game(state);
-    int i;
-
-    /*
-     * Simply replace the grid with a solved one. For this game,
-     * this isn't a useful operation for actually telling the user
-     * what they should have done, but it is useful for
-     * conveniently being able to get hold of a clean state from
-     * which to practise manoeuvres.
-     */
-    for (i = 0; i < ret->n; i++)
-	ret->tiles[i] = i+1;
-    ret->used_solve = ret->just_used_solve = TRUE;
-    ret->completed = ret->movecount = 1;
-
-    return ret;
+    return dupstr("S");
 }
 
 static char *game_text_format(game_state *state)
@@ -591,11 +576,11 @@
     int tilesize;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button) {
-    int cx, cy;
-    int dx, dy, tx, ty, n;
-    game_state *ret;
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
+{
+    int cx, cy, dx, dy;
+    char buf[80];
 
     button &= ~MOD_MASK;
     if (button != LEFT_BUTTON && button != RIGHT_BUTTON)
@@ -603,38 +588,81 @@
 
     cx = FROMCOORD(x);
     cy = FROMCOORD(y);
-    if (cx == -1 && cy >= 0 && cy < from->h)
-        n = from->w, dx = +1, dy = 0;
-    else if (cx == from->w && cy >= 0 && cy < from->h)
-        n = from->w, dx = -1, dy = 0;
-    else if (cy == -1 && cx >= 0 && cx < from->w)
-        n = from->h, dy = +1, dx = 0;
-    else if (cy == from->h && cx >= 0 && cx < from->w)
-        n = from->h, dy = -1, dx = 0;
+    if (cx == -1 && cy >= 0 && cy < state->h)
+        dx = -1, dy = 0;
+    else if (cx == state->w && cy >= 0 && cy < state->h)
+        dx = +1, dy = 0;
+    else if (cy == -1 && cx >= 0 && cx < state->w)
+        dy = -1, dx = 0;
+    else if (cy == state->h && cx >= 0 && cx < state->w)
+        dy = +1, dx = 0;
     else
         return NULL;                   /* invalid click location */
 
     /* reverse direction if right hand button is pressed */
-    if (button == RIGHT_BUTTON)
-    {
-        dx = -dx; if (dx) cx = from->w - 1 - cx;
-        dy = -dy; if (dy) cy = from->h - 1 - cy;
+    if (button == RIGHT_BUTTON) {
+        dx = -dx;
+        dy = -dy;
     }
 
+    if (dx)
+	sprintf(buf, "R%d,%d", cy, dx);
+    else
+	sprintf(buf, "C%d,%d", cx, dy);
+    return dupstr(buf);
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    int cx, cy, dx, dy;
+    int tx, ty, n;
+    game_state *ret;
+
+    if (!strcmp(move, "S")) {
+	int i;
+
+	ret = dup_game(from);
+
+	/*
+	 * Simply replace the grid with a solved one. For this game,
+	 * this isn't a useful operation for actually telling the user
+	 * what they should have done, but it is useful for
+	 * conveniently being able to get hold of a clean state from
+	 * which to practise manoeuvres.
+	 */
+	for (i = 0; i < ret->n; i++)
+	    ret->tiles[i] = i+1;
+	ret->used_solve = ret->just_used_solve = TRUE;
+	ret->completed = ret->movecount = 1;
+
+	return ret;
+    }
+
+    if (move[0] == 'R' && sscanf(move+1, "%d,%d", &cy, &dx) == 2 &&
+	cy >= 0 && cy < from->h) {
+	cx = dy = 0;
+	n = from->w;
+    } else if (move[0] == 'C' && sscanf(move+1, "%d,%d", &cx, &dy) == 2 &&
+	       cx >= 0 && cx < from->w) {
+	cy = dx = 0;
+	n = from->h;
+    } else
+	return NULL;
+
     ret = dup_game(from);
     ret->just_used_solve = FALSE;      /* zero this in a hurry */
 
     do {
-        cx += dx;
-        cy += dy;
-        tx = (cx + dx + from->w) % from->w;
-        ty = (cy + dy + from->h) % from->h;
+        tx = (cx - dx + from->w) % from->w;
+        ty = (cy - dy + from->h) % from->h;
         ret->tiles[C(ret, cx, cy)] = from->tiles[C(from, tx, ty)];
+        cx = tx;
+        cy = ty;
     } while (--n > 0);
 
     ret->movecount++;
 
-    ret->last_movement_sense = -(dx+dy);
+    ret->last_movement_sense = dx+dy;
 
     /*
      * See if the game has been completed.
@@ -1033,7 +1061,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/solo.c
+++ b/solo.c
@@ -1759,16 +1759,15 @@
     sfree(state);
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *ai, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *ai, char **error)
 {
-    game_state *ret;
     int c = state->c, r = state->r, cr = c*r;
-    int rsolve_ret;
+    int i, len;
+    char *ret, *p, *sep;
+    digit *grid;
+    int grid_needs_freeing;
 
-    ret = dup_game(state);
-    ret->completed = ret->cheated = TRUE;
-
     /*
      * If we already have the solution in the aux_info, save
      * ourselves some time.
@@ -1777,13 +1776,18 @@
 
 	assert(c == ai->c);
 	assert(r == ai->r);
-	memcpy(ret->grid, ai->grid, cr * cr * sizeof(digit));
+	grid = ai->grid;
+	grid_needs_freeing = FALSE;
 
     } else {
-	rsolve_ret = rsolve(c, r, ret->grid, NULL, 2);
+	int rsolve_ret;
 
+	grid = snewn(cr*cr, digit);
+	memcpy(grid, state->grid, cr*cr);
+	rsolve_ret = rsolve(c, r, grid, NULL, 2);
+
 	if (rsolve_ret != 1) {
-	    free_game(ret);
+	    sfree(grid);
 	    if (rsolve_ret == 0)
 		*error = "No solution exists for this puzzle";
 	    else
@@ -1790,8 +1794,48 @@
 		*error = "Multiple solutions exist for this puzzle";
 	    return NULL;
 	}
+
+	grid_needs_freeing = TRUE;
     }
 
+    /*
+     * It's surprisingly easy to work out _exactly_ how long this
+     * string needs to be. To decimal-encode all the numbers from 1
+     * to n:
+     * 
+     *  - every number has a units digit; total is n.
+     *  - all numbers above 9 have a tens digit; total is max(n-9,0).
+     *  - all numbers above 99 have a hundreds digit; total is max(n-99,0).
+     *  - and so on.
+     */
+    len = 0;
+    for (i = 1; i <= cr; i *= 10)
+	len += max(cr - i + 1, 0);
+    len += cr;		       /* don't forget the commas */
+    len *= cr;		       /* there are cr rows of these */
+
+    /*
+     * Now len is one bigger than the total size of the
+     * comma-separated numbers (because we counted an
+     * additional leading comma). We need to have a leading S
+     * and a trailing NUL, so we're off by one in total.
+     */
+    len++;
+
+    ret = snewn(len, char);
+    p = ret;
+    *p++ = 'S';
+    sep = "";
+    for (i = 0; i < cr*cr; i++) {
+	p += sprintf(p, "%s%d", sep, grid[i]);
+	sep = ",";
+    }
+    *p++ = '\0';
+    assert(p - ret == len);
+
+    if (grid_needs_freeing)
+	sfree(grid);
+
     return ret;
 }
 
@@ -1914,12 +1958,12 @@
     int *entered_items;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
-    int c = from->c, r = from->r, cr = c*r;
+    int c = state->c, r = state->r, cr = c*r;
     int tx, ty;
-    game_state *ret;
+    char buf[80];
 
     button &= ~MOD_MASK;
 
@@ -1928,7 +1972,7 @@
 
     if (tx >= 0 && tx < cr && ty >= 0 && ty < cr) {
         if (button == LEFT_BUTTON) {
-            if (from->immutable[ty*cr+tx]) {
+            if (state->immutable[ty*cr+tx]) {
                 ui->hx = ui->hy = -1;
             } else if (tx == ui->hx && ty == ui->hy && ui->hpencil == 0) {
                 ui->hx = ui->hy = -1;
@@ -1937,13 +1981,13 @@
                 ui->hy = ty;
                 ui->hpencil = 0;
             }
-            return from;		       /* UI activity occurred */
+            return "";		       /* UI activity occurred */
         }
         if (button == RIGHT_BUTTON) {
             /*
              * Pencil-mode highlighting for non filled squares.
              */
-            if (from->grid[ty*cr+tx] == 0) {
+            if (state->grid[ty*cr+tx] == 0) {
                 if (tx == ui->hx && ty == ui->hy && ui->hpencil) {
                     ui->hx = ui->hy = -1;
                 } else {
@@ -1954,7 +1998,7 @@
             } else {
                 ui->hx = ui->hy = -1;
             }
-            return from;		       /* UI activity occurred */
+            return "";		       /* UI activity occurred */
         }
     }
 
@@ -1977,7 +2021,7 @@
          * able to highlight the square, but it never hurts to be
          * careful.
          */
-	if (from->immutable[ui->hy*cr+ui->hx])
+	if (state->immutable[ui->hy*cr+ui->hx])
 	    return NULL;
 
         /*
@@ -1986,16 +2030,57 @@
          * have even been able to pencil-highlight the square, but
          * it never hurts to be careful.
          */
-        if (ui->hpencil && from->grid[ui->hy*cr+ui->hx])
+        if (ui->hpencil && state->grid[ui->hy*cr+ui->hx])
             return NULL;
 
+	sprintf(buf, "%c%d,%d,%d",
+		ui->hpencil && n > 0 ? 'P' : 'R', ui->hx, ui->hy, n);
+
+	ui->hx = ui->hy = -1;
+
+	return dupstr(buf);
+    }
+
+    return NULL;
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    int c = from->c, r = from->r, cr = c*r;
+    game_state *ret;
+    int x, y, n;
+
+    if (move[0] == 'S') {
+	char *p;
+
 	ret = dup_game(from);
-        if (ui->hpencil && n > 0) {
-            int index = (ui->hy*cr+ui->hx) * cr + (n-1);
+	ret->completed = ret->cheated = TRUE;
+
+	p = move+1;
+	for (n = 0; n < cr*cr; n++) {
+	    ret->grid[n] = atoi(p);
+
+	    if (!*p || ret->grid[n] < 1 || ret->grid[n] > cr) {
+		free_game(ret);
+		return NULL;
+	    }
+
+	    while (*p && isdigit((unsigned char)*p)) p++;
+	    if (*p == ',') p++;
+	}
+
+	return ret;
+    } else if ((move[0] == 'P' || move[0] == 'R') &&
+	sscanf(move+1, "%d,%d,%d", &x, &y, &n) == 3 &&
+	x >= 0 && x < cr && y >= 0 && y < cr && n >= 0 && n <= cr) {
+
+	ret = dup_game(from);
+        if (move[0] == 'P' && n > 0) {
+            int index = (y*cr+x) * cr + (n-1);
             ret->pencil[index] = !ret->pencil[index];
         } else {
-            ret->grid[ui->hy*cr+ui->hx] = n;
-            memset(ret->pencil + (ui->hy*cr+ui->hx)*cr, 0, cr);
+            ret->grid[y*cr+x] = n;
+            memset(ret->pencil + (y*cr+x)*cr, 0, cr);
 
             /*
              * We've made a real change to the grid. Check to see
@@ -2005,12 +2090,9 @@
                 ret->completed = TRUE;
             }
         }
-	ui->hx = ui->hy = -1;
-
-	return ret;		       /* made a valid move */
-    }
-
-    return NULL;
+	return ret;
+    } else
+	return NULL;		       /* couldn't parse move string */
 }
 
 /* ----------------------------------------------------------------------
@@ -2339,7 +2421,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,
--- a/twiddle.c
+++ b/twiddle.c
@@ -546,26 +546,10 @@
 	return 0;
 }
 
-static game_state *solve_game(game_state *state, game_state *currstate,
-			      game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+			game_aux_info *aux, char **error)
 {
-    game_state *ret = dup_game(state);
-    int i;
-
-    /*
-     * Simply replace the grid with a solved one. For this game,
-     * this isn't a useful operation for actually telling the user
-     * what they should have done, but it is useful for
-     * conveniently being able to get hold of a clean state from
-     * which to practise manoeuvres.
-     */
-    qsort(ret->grid, ret->w*ret->h, sizeof(int), compare_int);
-    for (i = 0; i < ret->w*ret->h; i++)
-	ret->grid[i] &= ~3;
-    ret->used_solve = ret->just_used_solve = TRUE;
-    ret->completed = ret->movecount = 1;
-
-    return ret;
+    return dupstr("S");
 }
 
 static char *game_text_format(game_state *state)
@@ -636,11 +620,11 @@
     int tilesize;
 };
 
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
-                             int x, int y, int button)
+static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
+			    int x, int y, int button)
 {
-    int w = from->w, h = from->h, n = from->n, wh = w*h;
-    game_state *ret;
+    int w = state->w, h = state->h, n = state->n /* , wh = w*h */;
+    char buf[80];
     int dir;
 
     button = button & (~MOD_MASK | MOD_NUM_KEYPAD);
@@ -698,8 +682,43 @@
     }
 
     /*
-     * This is a valid move. Make it.
+     * If we reach here, we have a valid move.
      */
+    sprintf(buf, "M%d,%d,%d", x, y, dir);
+    return dupstr(buf);
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+    game_state *ret;
+    int w = from->w, h = from->h, n = from->n, wh = w*h;
+    int x, y, dir;
+
+    if (!strcmp(move, "S")) {
+	int i;
+	ret = dup_game(from);
+
+	/*
+	 * Simply replace the grid with a solved one. For this game,
+	 * this isn't a useful operation for actually telling the user
+	 * what they should have done, but it is useful for
+	 * conveniently being able to get hold of a clean state from
+	 * which to practise manoeuvres.
+	 */
+	qsort(ret->grid, ret->w*ret->h, sizeof(int), compare_int);
+	for (i = 0; i < ret->w*ret->h; i++)
+	    ret->grid[i] &= ~3;
+	ret->used_solve = ret->just_used_solve = TRUE;
+	ret->completed = ret->movecount = 1;
+
+	return ret;
+    }
+
+    if (move[0] != 'M' ||
+	sscanf(move+1, "%d,%d,%d", &x, &y, &dir) != 3 ||
+	x < 0 || y < 0 || x > from->w - n || y > from->h - n)
+	return NULL;		       /* can't parse this move string */
+
     ret = dup_game(from);
     ret->just_used_solve = FALSE;  /* zero this in a hurry */
     ret->movecount++;
@@ -1206,7 +1225,8 @@
     new_ui,
     free_ui,
     game_changed_state,
-    make_move,
+    interpret_move,
+    execute_move,
     game_size,
     game_colours,
     game_new_drawstate,