chiark / gitweb /
Fix completion checking in Killer Solo.
[sgt-puzzles.git] / pearl.c
diff --git a/pearl.c b/pearl.c
index 248d64e00a7b30cee9c9b908eb4528a4f7c4e99a..c289ec6ac94af1a918ce9b86e22791bd0016e2d9 100644 (file)
--- a/pearl.c
+++ b/pearl.c
@@ -5,10 +5,17 @@
 /*
  * TODO:
  *
- *  - Keyboard-control cursor. (Would probably have to address both
- *    square centres, for laying multiple edges at a time in a
- *    drag-like style, and grid edges for marking particular line
- *    segments as no-go.)
+ *  - The current keyboard cursor mechanism works well on ordinary PC
+ *    keyboards, but for platforms with only arrow keys and a select
+ *    button or two, we may at some point need a simpler one which can
+ *    handle 'x' markings without needing shift keys. For instance, a
+ *    cursor with twice the grid resolution, so that it can range
+ *    across face centres, edge centres and vertices; 'clicks' on face
+ *    centres begin a drag as currently, clicks on edges toggle
+ *    markings, and clicks on vertices are ignored (but it would be
+ *    too confusing not to let the cursor rest on them). But I'm
+ *    pretty sure that would be less pleasant to play on a full
+ *    keyboard, so probably a #ifdef would be the thing.
  *
  *  - Generation is still pretty slow, due to difficulty coming up in
  *    the first place with a loop that makes a soluble puzzle even
@@ -83,6 +90,7 @@
 
 enum {
     COL_BACKGROUND, COL_HIGHLIGHT, COL_LOWLIGHT,
+    COL_CURSOR_BACKGROUND = COL_LOWLIGHT,
     COL_BLACK, COL_WHITE,
     COL_ERROR, COL_GRID, COL_FLASH,
     COL_DRAGON, COL_DRAGOFF,
@@ -122,7 +130,6 @@ struct game_state {
     char *errors;       /* size w*h: errors detected */
     char *marks;        /* size w*h: 'no line here' marks placed. */
     int completed, used_solve;
-    int loop_length;    /* filled in by check_completion when complete. */
 };
 
 #define DEFAULT_PRESET 3
@@ -172,7 +179,7 @@ static void free_params(game_params *params)
     sfree(params);
 }
 
-static game_params *dup_params(game_params *params)
+static game_params *dup_params(const game_params *params)
 {
     game_params *ret = snew(game_params);
     *ret = *params;                   /* structure copy */
@@ -206,7 +213,7 @@ static void decode_params(game_params *ret, char const *string)
     }
 }
 
-static char *encode_params(game_params *params, int full)
+static char *encode_params(const game_params *params, int full)
 {
     char buf[256];
     sprintf(buf, "%dx%d", params->w, params->h);
@@ -217,7 +224,7 @@ static char *encode_params(game_params *params, int full)
     return dupstr(buf);
 }
 
-static config_item *game_configure(game_params *params)
+static config_item *game_configure(const game_params *params)
 {
     config_item *ret;
     char buf[64];
@@ -254,7 +261,7 @@ static config_item *game_configure(game_params *params)
     return ret;
 }
 
-static game_params *custom_params(config_item *cfg)
+static game_params *custom_params(const config_item *cfg)
 {
     game_params *ret = snew(game_params);
 
@@ -266,7 +273,7 @@ static game_params *custom_params(config_item *cfg)
     return ret;
 }
 
-static char *validate_params(game_params *params, int full)
+static char *validate_params(const game_params *params, int full)
 {
     if (params->w < 5) return "Width must be at least five";
     if (params->h < 5) return "Height must be at least five";
@@ -1150,12 +1157,20 @@ void pearl_loopgen(int w, int h, char *lines, random_state *rs)
 #endif
 }
 
-static int new_clues(game_params *params, random_state *rs,
+static int new_clues(const game_params *params, random_state *rs,
                      char *clues, char *grid)
 {
-    int w = params->w, h = params->h;
+    int w = params->w, h = params->h, diff = params->difficulty;
     int ngen = 0, x, y, d, ret, i;
 
+
+    /*
+     * Difficulty exception: 5x5 Tricky is not generable (the
+     * generator will spin forever trying) and so we fudge it to Easy.
+     */
+    if (w == 5 && h == 5 && diff > DIFF_EASY)
+        diff = DIFF_EASY;
+
     while (1) {
         ngen++;
        pearl_loopgen(w, h, grid, rs);
@@ -1237,7 +1252,7 @@ static int new_clues(game_params *params, random_state *rs,
             /*
              * See if we can solve the puzzle just like this.
              */
-            ret = pearl_solve(w, h, clues, grid, params->difficulty, FALSE);
+            ret = pearl_solve(w, h, clues, grid, diff, FALSE);
             assert(ret > 0);          /* shouldn't be inconsistent! */
             if (ret != 1)
                 continue;                     /* go round and try again */
@@ -1245,8 +1260,8 @@ static int new_clues(game_params *params, random_state *rs,
             /*
              * Check this puzzle isn't too easy.
              */
-            if (params->difficulty > DIFF_EASY) {
-                ret = pearl_solve(w, h, clues, grid, params->difficulty-1, FALSE);
+            if (diff > DIFF_EASY) {
+                ret = pearl_solve(w, h, clues, grid, diff-1, FALSE);
                 assert(ret > 0);
                 if (ret == 1)
                     continue; /* too easy: try again */
@@ -1313,7 +1328,7 @@ static int new_clues(game_params *params, random_state *rs,
                 clue = clues[y*w+x];
                 clues[y*w+x] = 0;             /* try removing this clue */
 
-                ret = pearl_solve(w, h, clues, grid, params->difficulty, FALSE);
+                ret = pearl_solve(w, h, clues, grid, diff, FALSE);
                 assert(ret > 0);
                 if (ret != 1)
                     clues[y*w+x] = clue;   /* oops, put it back again */
@@ -1340,7 +1355,7 @@ static int new_clues(game_params *params, random_state *rs,
     return ngen;
 }
 
-static char *new_game_desc(game_params *params, random_state *rs,
+static char *new_game_desc(const game_params *params, random_state *rs,
                           char **aux, int interactive)
 {
     char *grid, *clues;
@@ -1377,7 +1392,7 @@ static char *new_game_desc(game_params *params, random_state *rs,
     return desc;
 }
 
-static char *validate_desc(game_params *params, char *desc)
+static char *validate_desc(const game_params *params, const char *desc)
 {
     int i, sizesofar;
     const int totalsize = params->w * params->h;
@@ -1400,7 +1415,8 @@ static char *validate_desc(game_params *params, char *desc)
     return NULL;
 }
 
-static game_state *new_game(midend *me, game_params *params, char *desc)
+static game_state *new_game(midend *me, const game_params *params,
+                            const char *desc)
 {
     game_state *state = snew(game_state);
     int i, j, sz = params->w*params->h;
@@ -1436,7 +1452,7 @@ static game_state *new_game(midend *me, game_params *params, char *desc)
     return state;
 }
 
-static game_state *dup_game(game_state *state)
+static game_state *dup_game(const game_state *state)
 {
     game_state *ret = snew(game_state);
     int sz = state->shared->sz, i;
@@ -1479,12 +1495,11 @@ static char nbits[16] = { 0, 1, 1, 2,
 
 #define ERROR_CLUE 16
 
-static void dsf_update_completion(game_state *state, int *loopclass,
-                                 int ax, int ay, char dir,
-                                 int *dsf, int *dsfsize)
+static void dsf_update_completion(game_state *state, int ax, int ay, char dir,
+                                 int *dsf)
 {
     int w = state->shared->w /*, h = state->shared->h */;
-    int ac = ay*w+ax, ae, bx, by, bc, be;
+    int ac = ay*w+ax, bx, by, bc;
 
     if (!(state->lines[ac] & dir)) return; /* no link */
     bx = ax + DX(dir); by = ay + DY(dir);
@@ -1492,34 +1507,19 @@ static void dsf_update_completion(game_state *state, int *loopclass,
     assert(INGRID(state, bx, by)); /* should not have a link off grid */
 
     bc = by*w+bx;
-#if 0
     assert(state->lines[bc] & F(dir)); /* should have reciprocal link */
-#endif
-    /* TODO put above assertion back in once we stop generating partially
-     * soluble puzzles. */
     if (!(state->lines[bc] & F(dir))) return;
 
-    ae = dsf_canonify(dsf, ac);
-    be = dsf_canonify(dsf, bc);
-
-    if (ae == be) { /* detected a loop! */
-        if (*loopclass != -1) /* this is the second loop, doom. */
-            return;
-        *loopclass = ae;
-    } else {
-        int size = dsfsize[ae] + dsfsize[be];
-        dsf_merge(dsf, ac, bc);
-        ae = dsf_canonify(dsf, ac);
-        dsfsize[ae] = size;
-    }
-    return;
+    dsf_merge(dsf, ac, bc);
 }
 
 static void check_completion(game_state *state, int mark)
 {
     int w = state->shared->w, h = state->shared->h, x, y, i, d;
-    int had_error = FALSE /*, is_complete = FALSE */, loopclass;
-    int *dsf, *dsfsize;
+    int had_error = FALSE;
+    int *dsf, *component_state;
+    int nsilly, nloop, npath, largest_comp, largest_size, total_pathsize;
+    enum { COMP_NONE, COMP_LOOP, COMP_PATH, COMP_SILLY, COMP_EMPTY };
 
     if (mark) {
         for (i = 0; i < w*h; i++) {
@@ -1530,57 +1530,111 @@ static void check_completion(game_state *state, int mark)
 #define ERROR(x,y,e) do { had_error = TRUE; if (mark) state->errors[(y)*w+(x)] |= (e); } while(0)
 
     /*
-     * First of all: we should have one single closed loop, passing through all clues.
+     * Analyse the solution into loops, paths and stranger things.
+     * Basic strategy here is all the same as in Loopy - see the big
+     * comment in loopy.c's check_completion() - and for exactly the
+     * same reasons, since Loopy and Pearl have basically the same
+     * form of expected solution.
      */
-    dsf = snewn(w*h, int);
-    dsfsize = snewn(w*h, int);
-    dsf_init(dsf, w*h);
-    for (i = 0; i < w*h; i++) dsfsize[i] = 1;
-    loopclass = -1;
+    dsf = snew_dsf(w*h);
 
+    /* Build the dsf. */
     for (x = 0; x < w; x++) {
         for (y = 0; y < h; y++) {
-            dsf_update_completion(state, &loopclass, x, y, R, dsf, dsfsize);
-            dsf_update_completion(state, &loopclass, x, y, D, dsf, dsfsize);
+            dsf_update_completion(state, x, y, R, dsf);
+            dsf_update_completion(state, x, y, D, dsf);
         }
     }
-    if (loopclass != -1) {
-        /* We have a loop. Check all squares with lines on. */
-        for (x = 0; x < w; x++) {
-            for (y = 0; y < h; y++) {
-                if (state->lines[y*w+x] == BLANK) {
-                    if (state->shared->clues[y*w+x] != NOCLUE) {
-                        /* the loop doesn't include this clue square! */
-                        ERROR(x, y, ERROR_CLUE);
-                    }
-                } else {
-                    if (dsf_canonify(dsf, y*w+x) != loopclass) {
-                        /* these lines are not on the loop: mark them as error. */
-                        ERROR(x, y, state->lines[y*w+x]);
-                    }
-                }
-            }
-        }
+
+    /* Initialise a state variable for each connected component. */
+    component_state = snewn(w*h, int);
+    for (i = 0; i < w*h; i++) {
+        if (dsf_canonify(dsf, i) == i)
+            component_state[i] = COMP_LOOP;
+        else
+            component_state[i] = COMP_NONE;
     }
 
     /*
-     * Second: check no clues are contradicted.
+     * Classify components, and mark errors where a square has more
+     * than two line segments.
      */
-
     for (x = 0; x < w; x++) {
         for (y = 0; y < h; y++) {
             int type = state->lines[y*w+x];
-            /*
-             * Check that no square has more than two line segments.
-             */
-            if (NBITS(type) > 2) {
+            int degree = NBITS(type);
+            int comp = dsf_canonify(dsf, y*w+x);
+            if (degree > 2) {
                 ERROR(x,y,type);
+                component_state[comp] = COMP_SILLY;
+            } else if (degree == 0) {
+                component_state[comp] = COMP_EMPTY;
+            } else if (degree == 1) {
+                if (component_state[comp] != COMP_SILLY)
+                    component_state[comp] = COMP_PATH;
             }
-            /*
-             * Check that no clues are contradicted. This code is similar to
-             * the code that sets up the maximal clue array for any given
-             * loop.
-             */
+        }
+    }
+
+    /* Count the components, and find the largest sensible one. */
+    nsilly = nloop = npath = 0;
+    total_pathsize = 0;
+    largest_comp = largest_size = -1;
+    for (i = 0; i < w*h; i++) {
+        if (component_state[i] == COMP_SILLY) {
+            nsilly++;
+        } else if (component_state[i] == COMP_PATH) {
+            total_pathsize += dsf_size(dsf, i);
+            npath = 1;
+        } else if (component_state[i] == COMP_LOOP) {
+            int this_size;
+
+            nloop++;
+
+            if ((this_size = dsf_size(dsf, i)) > largest_size) {
+                largest_comp = i;
+                largest_size = this_size;
+            }
+        }
+    }
+    if (largest_size < total_pathsize) {
+        largest_comp = -1;             /* means the paths */
+        largest_size = total_pathsize;
+    }
+
+    if (nloop > 0 && nloop + npath > 1) {
+        /*
+         * If there are at least two sensible components including at
+         * least one loop, highlight every sensible component that is
+         * not the largest one.
+         */
+        for (i = 0; i < w*h; i++) {
+            int comp = dsf_canonify(dsf, i);
+            if (component_state[comp] == COMP_PATH)
+                comp = -1; /* part of the 'all paths' quasi-component */
+            if ((component_state[comp] == COMP_PATH &&
+                 -1 != largest_comp) ||
+                (component_state[comp] == COMP_LOOP &&
+                 comp != largest_comp))
+                ERROR(i%w, i/w, state->lines[i]);
+        }
+    }
+
+    /* Now we've finished with the dsf and component states. The only
+     * thing we'll need to remember later on is whether all edges were
+     * part of a single loop, for which our counter variables
+     * nsilly,nloop,npath are enough. */
+    sfree(component_state);
+    sfree(dsf);
+
+    /*
+     * Check that no clues are contradicted. This code is similar to
+     * the code that sets up the maximal clue array for any given
+     * loop.
+     */
+    for (x = 0; x < w; x++) {
+        for (y = 0; y < h; y++) {
+            int type = state->lines[y*w+x];
             if (state->shared->clues[y*w+x] == CORNER) {
                 /* Supposed to be a corner: will find a contradiction if
                  * it actually contains a straight line, or if it touches any
@@ -1621,17 +1675,30 @@ static void check_completion(game_state *state, int mark)
             }
         }
     }
-    if (!had_error && loopclass != -1) {
-        state->completed = TRUE;
-        state->loop_length = dsfsize[loopclass];
-    } else {
-        state->completed = FALSE;
-    }
 
-    sfree(dsf);
-    sfree(dsfsize);
+    if (nloop == 1 && nsilly == 0 && npath == 0) {
+        /*
+         * If there's exactly one loop (so that the puzzle is at least
+         * potentially complete), we need to ensure it hasn't left any
+         * clue out completely.
+         */
+        for (x = 0; x < w; x++) {
+            for (y = 0; y < h; y++) {
+                if (state->lines[y*w+x] == BLANK) {
+                    if (state->shared->clues[y*w+x] != NOCLUE) {
+                        /* the loop doesn't include this clue square! */
+                        ERROR(x, y, ERROR_CLUE);
+                    }
+                }
+            }
+        }
 
-    return;
+        /*
+         * But if not, then we're done!
+         */
+        if (!had_error)
+            state->completed = TRUE;
+    }
 }
 
 /* completion check:
@@ -1639,7 +1706,7 @@ static void check_completion(game_state *state, int mark)
  * - no clues must be contradicted (highlight clue itself in error if so)
  * - if there is a closed loop it must include every line segment laid
  *    - if there's a smaller closed loop then highlight whole loop as error
- * - no square must have more than 3 lines radiating from centre point
+ * - no square must have more than 2 lines radiating from centre point
  *   (highlight all lines in that square as error if so)
  */
 
@@ -1660,8 +1727,8 @@ static char *solve_for_diff(game_state *state, char *old_lines, char *new_lines)
     return move;
 }
 
-static char *solve_game(game_state *state, game_state *currstate,
-                       char *aux, char **error)
+static char *solve_game(const game_state *state, const game_state *currstate,
+                        const char *aux, char **error)
 {
     game_state *solved = dup_game(state);
     int i, ret, sz = state->shared->sz;
@@ -1705,29 +1772,61 @@ done:
     return move;
 }
 
-static int game_can_format_as_text_now(game_params *params)
+static int game_can_format_as_text_now(const game_params *params)
 {
-    return FALSE;
+    return TRUE;
 }
 
-static char *game_text_format(game_state *state)
+static char *game_text_format(const game_state *state)
 {
-    return NULL;
+    int w = state->shared->w, h = state->shared->h, cw = 4, ch = 2;
+    int gw = cw*(w-1) + 2, gh = ch*(h-1) + 1, len = gw * gh, r, c, j;
+    char *board = snewn(len + 1, char);
+
+    assert(board);
+    memset(board, ' ', len);
+
+    for (r = 0; r < h; ++r) {
+       for (c = 0; c < w; ++c) {
+           int i = r*w + c, cell = r*ch*gw + c*cw;
+           board[cell] = "+BW"[(unsigned char)state->shared->clues[i]];
+           if (c < w - 1 && (state->lines[i] & R || state->lines[i+1] & L))
+               memset(board + cell + 1, '-', cw - 1);
+           if (r < h - 1 && (state->lines[i] & D || state->lines[i+w] & U))
+               for (j = 1; j < ch; ++j) board[cell + j*gw] = '|';
+           if (c < w - 1 && (state->marks[i] & R || state->marks[i+1] & L))
+               board[cell + cw/2] = 'x';
+           if (r < h - 1 && (state->marks[i] & D || state->marks[i+w] & U))
+               board[cell + (ch/2 * gw)] = 'x';
+       }
+
+       for (j = 0; j < (r == h - 1 ? 1 : ch); ++j)
+           board[r*ch*gw + (gw - 1) + j*gw] = '\n';
+    }
+
+    board[len] = '\0';
+    return board;
 }
 
 struct game_ui {
     int *dragcoords;       /* list of (y*w+x) coords in drag so far */
-    int ndragcoords;       /* number of entries in dragcoords. 0 = no drag. */
+    int ndragcoords;       /* number of entries in dragcoords.
+                            * 0 = click but no drag yet. -1 = no drag at all */
     int clickx, clicky;    /* pixel position of initial click */
+
+    int curx, cury;        /* grid position of keyboard cursor */
+    int cursor_active;     /* TRUE iff cursor is shown */
 };
 
-static game_ui *new_ui(game_state *state)
+static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
     int sz = state->shared->sz;
 
-    ui->ndragcoords = 0;
+    ui->ndragcoords = -1;
     ui->dragcoords = snewn(sz, int);
+    ui->cursor_active = FALSE;
+    ui->curx = ui->cury = 0;
 
     return ui;
 }
@@ -1738,17 +1837,17 @@ static void free_ui(game_ui *ui)
     sfree(ui);
 }
 
-static char *encode_ui(game_ui *ui)
+static char *encode_ui(const game_ui *ui)
 {
     return NULL;
 }
 
-static void decode_ui(game_ui *ui, char *encoding)
+static void decode_ui(game_ui *ui, const char *encoding)
 {
 }
 
-static void game_changed_state(game_ui *ui, game_state *oldstate,
-                               game_state *newstate)
+static void game_changed_state(game_ui *ui, const game_state *oldstate,
+                               const game_state *newstate)
 {
 }
 
@@ -1761,6 +1860,7 @@ static void game_changed_state(game_ui *ui, game_state *oldstate,
 #define BORDER_WIDTH (max(TILE_SIZE / 32, 1))
 
 #define COORD(x) ( (x) * TILE_SIZE + BORDER )
+#define CENTERED_COORD(x) ( COORD(x) + TILE_SIZE/2 )
 #define FROMCOORD(x) ( ((x) < BORDER) ? -1 : ( ((x) - BORDER) / TILE_SIZE) )
 
 #define DS_ESHIFT 4     /* R/U/L/D shift, for error flags */
@@ -1769,6 +1869,7 @@ static void game_changed_state(game_ui *ui, game_state *oldstate,
 
 #define DS_ERROR_CLUE (1 << 20)
 #define DS_FLASH (1 << 21)
+#define DS_CURSOR (1 << 22)
 
 enum { GUI_MASYU, GUI_LOOPY };
 
@@ -1796,7 +1897,8 @@ struct game_drawstate {
     char *draglines;            /* size w*h; lines flipped by current drag */
 };
 
-static void update_ui_drag(game_state *state, game_ui *ui, int gx, int gy)
+static void update_ui_drag(const game_state *state, game_ui *ui,
+                           int gx, int gy)
 {
     int /* sz = state->shared->sz, */ w = state->shared->w;
     int i, ox, oy, pos;
@@ -1805,6 +1907,9 @@ static void update_ui_drag(game_state *state, game_ui *ui, int gx, int gy)
     if (!INGRID(state, gx, gy))
         return;                        /* square is outside grid */
 
+    if (ui->ndragcoords < 0)
+        return;                        /* drag not in progress anyway */
+
     pos = gy * w + gx;
 
     lastpos = ui->dragcoords[ui->ndragcoords > 0 ? ui->ndragcoords-1 : 0];
@@ -1836,7 +1941,15 @@ static void update_ui_drag(game_state *state, game_ui *ui, int gx, int gy)
     if (ox == gx || oy == gy) {
         int dx = (gx < ox ? -1 : gx > ox ? +1 : 0);
         int dy = (gy < oy ? -1 : gy > oy ? +1 : 0);
+        int dir = (dy>0 ? D : dy<0 ? U : dx>0 ? R : L);
         while (ox != gx || oy != gy) {
+            /*
+             * If the drag attempts to cross a 'no line here' mark,
+             * stop there. We physically don't allow the user to drag
+             * over those marks.
+             */
+            if (state->marks[oy*w+ox] & dir)
+                break;
             ox += dx;
             oy += dy;
             ui->dragcoords[ui->ndragcoords++] = oy * w + ox;
@@ -1869,9 +1982,10 @@ static void update_ui_drag(game_state *state, game_ui *ui, int gx, int gy)
  *         to state newstate, each of which equals either 0 or dir]
  *     }
  */
-static void interpret_ui_drag(game_state *state, game_ui *ui, int *clearing,
-                              int i, int *sx, int *sy, int *dx, int *dy,
-                              int *dir, int *oldstate, int *newstate)
+static void interpret_ui_drag(const game_state *state, const game_ui *ui,
+                              int *clearing, int i, int *sx, int *sy,
+                              int *dx, int *dy, int *dir,
+                              int *oldstate, int *newstate)
 {
     int w = state->shared->w;
     int sp = ui->dragcoords[i], dp = ui->dragcoords[i+1];
@@ -1900,15 +2014,49 @@ static void interpret_ui_drag(game_state *state, game_ui *ui, int *clearing,
     }
 }
 
-static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
-                           int x, int y, int button)
+static char *mark_in_direction(const game_state *state, int x, int y, int dir,
+                              int primary, char *buf)
 {
     int w = state->shared->w /*, h = state->shared->h, sz = state->shared->sz */;
+    int x2 = x + DX(dir);
+    int y2 = y + DY(dir);
+    int dir2 = F(dir);
+
+    char ch = primary ? 'F' : 'M', *other;
+
+    if (!INGRID(state, x, y) || !INGRID(state, x2, y2)) return "";
+
+    /* disallow laying a mark over a line, or vice versa. */
+    other = primary ? state->marks : state->lines;
+    if (other[y*w+x] & dir || other[y2*w+x2] & dir2) return "";
+    
+    sprintf(buf, "%c%d,%d,%d;%c%d,%d,%d", ch, dir, x, y, ch, dir2, x2, y2);
+    return dupstr(buf);
+}
+
+#define KEY_DIRECTION(btn) (\
+    (btn) == CURSOR_DOWN ? D : (btn) == CURSOR_UP ? U :\
+    (btn) == CURSOR_LEFT ? L : R)
+
+static char *interpret_move(const game_state *state, game_ui *ui,
+                            const game_drawstate *ds,
+                            int x, int y, int button)
+{
+    int w = state->shared->w, h = state->shared->h /*, sz = state->shared->sz */;
     int gx = FROMCOORD(x), gy = FROMCOORD(y), i;
+    int release = FALSE;
     char tmpbuf[80];
 
+    int shift = button & MOD_SHFT, control = button & MOD_CTRL;
+    button &= ~MOD_MASK;
+
     if (IS_MOUSE_DOWN(button)) {
-        if (!INGRID(state, gx, gy)) return NULL;
+       ui->cursor_active = FALSE;
+
+        if (!INGRID(state, gx, gy)) {
+            ui->ndragcoords = -1;
+            return NULL;
+        }
 
         ui->clickx = x; ui->clicky = y;
         ui->dragcoords[0] = gy * w + gx;
@@ -1917,13 +2065,58 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
         return "";
     }
 
-    if (button == LEFT_DRAG) {
+    if (button == LEFT_DRAG && ui->ndragcoords >= 0) {
         update_ui_drag(state, ui, gx, gy);
         return "";
     }
 
-    if (IS_MOUSE_RELEASE(button)) {
-        if (ui->ndragcoords) {
+    if (IS_MOUSE_RELEASE(button)) release = TRUE;
+
+    if (IS_CURSOR_MOVE(button)) {
+       if (!ui->cursor_active) {
+           ui->cursor_active = TRUE;
+       } else if (control | shift) {
+           char *move;
+           if (ui->ndragcoords > 0) return NULL;
+           ui->ndragcoords = -1;
+           move = mark_in_direction(state, ui->curx, ui->cury,
+                                    KEY_DIRECTION(button), control, tmpbuf);
+           if (control && !shift && *move)
+               move_cursor(button, &ui->curx, &ui->cury, w, h, FALSE);
+           return move;
+       } else {
+           move_cursor(button, &ui->curx, &ui->cury, w, h, FALSE);
+           if (ui->ndragcoords >= 0)
+               update_ui_drag(state, ui, ui->curx, ui->cury);
+       }
+       return "";
+    }
+
+    if (IS_CURSOR_SELECT(button)) {
+       if (!ui->cursor_active) {
+           ui->cursor_active = TRUE;
+           return "";
+       } else if (button == CURSOR_SELECT) {
+           if (ui->ndragcoords == -1) {
+               ui->ndragcoords = 0;
+               ui->dragcoords[0] = ui->cury * w + ui->curx;
+               ui->clickx = CENTERED_COORD(ui->curx);
+               ui->clicky = CENTERED_COORD(ui->cury);
+               return "";
+           } else release = TRUE;
+       } else if (button == CURSOR_SELECT2 && ui->ndragcoords >= 0) {
+           ui->ndragcoords = -1;
+           return "";
+       }
+    }
+
+    if (button == 27 || button == '\b') {
+        ui->ndragcoords = -1;
+        return "";
+    }
+
+    if (release) {
+        if (ui->ndragcoords > 0) {
             /* End of a drag: process the cached line data. */
             int buflen = 0, bufsize = 256, tmplen;
             char *buf = NULL;
@@ -1949,15 +2142,15 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
                 }
             }
 
-            ui->ndragcoords = 0;
+            ui->ndragcoords = -1;
 
             return buf ? buf : "";
-        } else {
+        } else if (ui->ndragcoords == 0) {
             /* Click (or tiny drag). Work out which edge we were
              * closest to. */
             int cx, cy;
-            int gx2, gy2, l1, l2, ismark = (button == RIGHT_RELEASE);
-            char movec = ismark ? 'M' : 'F';
+
+            ui->ndragcoords = -1;
 
             /*
              * We process clicks based on the mouse-down location,
@@ -1969,8 +2162,8 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
 
             gx = FROMCOORD(x);
             gy = FROMCOORD(y);
-            cx = COORD(gx) + TILE_SIZE/2;
-            cy = COORD(gy) + TILE_SIZE/2;
+            cx = CENTERED_COORD(gx);
+            cy = CENTERED_COORD(gy);
 
             if (!INGRID(state, gx, gy)) return "";
 
@@ -1979,30 +2172,16 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
 
                 return "";
             } else {
+               int direction;
                 if (abs(x-cx) < abs(y-cy)) {
                     /* Closest to top/bottom edge. */
-                    l1 = (y < cy) ? U : D;
+                    direction = (y < cy) ? U : D;
                 } else {
                     /* Closest to left/right edge. */
-                    l1 = (x < cx) ? L : R;
+                    direction = (x < cx) ? L : R;
                 }
-                gx2 = gx + DX(l1); gy2 = gy + DY(l1);
-                l2 = F(l1);
-
-                if (!INGRID(state, gx, gy) || !INGRID(state, gx2, gy2)) return "";
-
-                /* disallow laying a mark over a line, or vice versa. */
-                if (ismark) {
-                    if ((state->lines[gy*w+gx] & l1) || (state->lines[gy2*w+gx2] & l2))
-                        return "";
-                } else {
-                    if ((state->marks[gy*w+gx] & l1) || (state->marks[gy2*w+gx2] & l2))
-                        return "";
-                }
-
-                sprintf(tmpbuf, "%c%d,%d,%d;%c%d,%d,%d",
-                        movec, l1, gx, gy, movec, l2, gx2, gy2);
-                return dupstr(tmpbuf);
+               return mark_in_direction(state, gx, gy, direction,
+                                        (button == LEFT_RELEASE), tmpbuf);
             }
         }
     }
@@ -2010,12 +2189,10 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
     if (button == 'H' || button == 'h')
         return dupstr("H");
 
-    /* TODO cursor */
-
     return NULL;
 }
 
-static game_state *execute_move(game_state *state, char *move)
+static game_state *execute_move(const game_state *state, const char *move)
 {
     int w = state->shared->w, h = state->shared->h;
     char c;
@@ -2037,9 +2214,6 @@ static game_state *execute_move(game_state *state, char *move)
             if (!INGRID(state, x, y)) goto badmove;
             if (l < 0 || l > 15) goto badmove;
 
-            /* TODO trying to set a line over a no-line mark should be
-             * a failed move? */
-
             if (c == 'L')
                 ret->lines[y*w + x] |= (char)l;
             else if (c == 'N')
@@ -2052,6 +2226,16 @@ static game_state *execute_move(game_state *state, char *move)
             else if (c == 'M')
                 ret->marks[y*w + x] ^= (char)l;
 
+            /*
+             * If we ended up trying to lay a line _over_ a mark,
+             * that's a failed move: interpret_move() should have
+             * ensured we never received a move string like that in
+             * the first place.
+             */
+            if ((ret->lines[y*w + x] & (char)l) &&
+                (ret->marks[y*w + x] & (char)l))
+                goto badmove;
+
             move += n;
         } else if (strcmp(move, "H") == 0) {
             pearl_solve(ret->shared->w, ret->shared->h,
@@ -2083,8 +2267,8 @@ badmove:
 
 #define FLASH_TIME 0.5F
 
-static void game_compute_size(game_params *params, int tilesize,
-                             int *x, int *y)
+static void game_compute_size(const game_params *params, int tilesize,
+                              int *x, int *y)
 {
     /* Ick: fake up `ds->tilesize' for macro expansion purposes */
     struct { int halfsz; } ads, *ds = &ads;
@@ -2095,7 +2279,7 @@ static void game_compute_size(game_params *params, int tilesize,
 }
 
 static void game_set_size(drawing *dr, game_drawstate *ds,
-                         game_params *params, int tilesize)
+                          const game_params *params, int tilesize)
 {
     ds->halfsz = (tilesize-1)/2;
 }
@@ -2134,7 +2318,7 @@ static float *game_colours(frontend *fe, int *ncolours)
     return ret;
 }
 
-static game_drawstate *game_new_drawstate(drawing *dr, game_state *state)
+static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
 {
     struct game_drawstate *ds = snew(struct game_drawstate);
     int i;
@@ -2193,7 +2377,7 @@ static void draw_lines_specific(drawing *dr, game_drawstate *ds,
     }
 }
 
-static void draw_square(drawing *dr, game_drawstate *ds, game_ui *ui,
+static void draw_square(drawing *dr, game_drawstate *ds, const game_ui *ui,
                         int x, int y, unsigned int lflags, char clue)
 {
     int ox = COORD(x), oy = COORD(y);
@@ -2207,7 +2391,10 @@ static void draw_square(drawing *dr, game_drawstate *ds, game_ui *ui,
     clip(dr, ox, oy, TILE_SIZE, TILE_SIZE);
 
     /* Clear the square. */
-    draw_rect(dr, ox, oy, TILE_SIZE, TILE_SIZE, COL_BACKGROUND);
+    draw_rect(dr, ox, oy, TILE_SIZE, TILE_SIZE,
+             (lflags & DS_CURSOR) ?
+             COL_CURSOR_BACKGROUND : COL_BACKGROUND);
+             
 
     if (get_gui_style() == GUI_LOOPY) {
         /* Draw small dot, underneath any lines. */
@@ -2255,7 +2442,7 @@ static void draw_square(drawing *dr, game_drawstate *ds, game_ui *ui,
     /* Draw a clue, if present */
     if (clue != NOCLUE) {
         int c = (lflags & DS_FLASH) ? COL_FLASH :
-                (clue == CORNER) ? COL_BLACK : COL_WHITE;
+                (clue == STRAIGHT) ? COL_WHITE : COL_BLACK;
 
         if (lflags & DS_ERROR_CLUE) /* draw a bigger 'error' clue circle. */
             draw_circle(dr, cx, cy, TILE_SIZE*3/8, COL_ERROR, COL_ERROR);
@@ -2267,9 +2454,10 @@ static void draw_square(drawing *dr, game_drawstate *ds, game_ui *ui,
     draw_update(dr, ox, oy, TILE_SIZE, TILE_SIZE);
 }
 
-static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
-                       game_state *state, int dir, game_ui *ui,
-                       float animtime, float flashtime)
+static void game_redraw(drawing *dr, game_drawstate *ds,
+                        const game_state *oldstate, const game_state *state,
+                        int dir, const game_ui *ui,
+                        float animtime, float flashtime)
 {
     int w = state->shared->w, h = state->shared->h, sz = state->shared->sz;
     int x, y, force = 0, flashing = 0;
@@ -2306,7 +2494,7 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
         flashing = DS_FLASH;
 
     memset(ds->draglines, 0, sz);
-    if (ui->dragcoords) {
+    if (ui->ndragcoords > 0) {
         int i, clearing = TRUE;
         for (i = 0; i < ui->ndragcoords - 1; i++) {
             int sx, sy, dx, dy, dir, oldstate, newstate;
@@ -2331,6 +2519,9 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
 
             f |= flashing;
 
+           if (ui->cursor_active && x == ui->curx && y == ui->cury)
+               f |= DS_CURSOR;
+
             if (f != ds->lflags[y*w+x] || force) {
                 ds->lflags[y*w+x] = f;
                 draw_square(dr, ds, ui, x, y, f, state->shared->clues[y*w+x]);
@@ -2339,33 +2530,33 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
     }
 }
 
-static float game_anim_length(game_state *oldstate, game_state *newstate,
-                             int dir, game_ui *ui)
+static float game_anim_length(const game_state *oldstate,
+                              const game_state *newstate, int dir, game_ui *ui)
 {
     return 0.0F;
 }
 
-static float game_flash_length(game_state *oldstate, game_state *newstate,
-                              int dir, game_ui *ui)
+static float game_flash_length(const game_state *oldstate,
+                               const game_state *newstate, int dir, game_ui *ui)
 {
-    if (!oldstate->completed &&
-        newstate->completed && !newstate->used_solve)
+    if (!oldstate->completed && newstate->completed &&
+        !oldstate->used_solve && !newstate->used_solve)
         return FLASH_TIME;
     else
         return 0.0F;
 }
 
-static int game_status(game_state *state)
+static int game_status(const game_state *state)
 {
     return state->completed ? +1 : 0;
 }
 
-static int game_timing_state(game_state *state, game_ui *ui)
+static int game_timing_state(const game_state *state, game_ui *ui)
 {
     return TRUE;
 }
 
-static void game_print_size(game_params *params, float *x, float *y)
+static void game_print_size(const game_params *params, float *x, float *y)
 {
     int pw, ph;
 
@@ -2377,7 +2568,7 @@ static void game_print_size(game_params *params, float *x, float *y)
     *y = ph / 100.0F;
 }
 
-static void game_print(drawing *dr, game_state *state, int tilesize)
+static void game_print(drawing *dr, const game_state *state, int tilesize)
 {
     int w = state->shared->w, h = state->shared->h, x, y;
     int black = print_mono_colour(dr, 0);
@@ -2432,7 +2623,7 @@ const struct game thegame = {
     dup_game,
     free_game,
     TRUE, solve_game,
-    FALSE, game_can_format_as_text_now, game_text_format,
+    TRUE, game_can_format_as_text_now, game_text_format,
     new_ui,
     free_ui,
     encode_ui,