Add a limited-shuffle mode like that added to Sixteen and Twiddle in r5769,
[sgt/puzzles] / solo.c
diff --git a/solo.c b/solo.c
index 3cb050d..6a964ea 100644 (file)
--- a/solo.c
+++ b/solo.c
@@ -3,6 +3,30 @@
  *
  * TODO:
  *
+ *  - reports from users are that `Trivial'-mode puzzles are still
+ *    rather hard compared to newspapers' easy ones, so some better
+ *    low-end difficulty grading would be nice
+ *     + it's possible that really easy puzzles always have
+ *       _several_ things you can do, so don't make you hunt too
+ *       hard for the one deduction you can currently make
+ *     + it's also possible that easy puzzles require fewer
+ *       cross-eliminations: perhaps there's a higher incidence of
+ *       things you can deduce by looking only at (say) rows,
+ *       rather than things you have to check both rows and columns
+ *       for
+ *     + but really, what I need to do is find some really easy
+ *       puzzles and _play_ them, to see what's actually easy about
+ *       them
+ *     + while I'm revamping this area, filling in the _last_
+ *       number in a nearly-full row or column should certainly be
+ *       permitted even at the lowest difficulty level.
+ *     + also Owen noticed that `Basic' grids requiring numeric
+ *       elimination are actually very hard, so I wonder if a
+ *       difficulty gradation between that and positional-
+ *       elimination-only might be in order
+ *     + but it's not good to have _too_ many difficulty levels, or
+ *       it'll take too long to randomly generate a given level.
+ * 
  *  - it might still be nice to do some prioritisation on the
  *    removal of numbers from the grid
  *     + one possibility is to try to minimise the maximum number
  *      click, _or_ you highlight a square and then type. At most
  *      one thing is ever highlighted at a time, so there's no way
  *      to confuse the two.
- *     + `pencil marks' might be useful for more subtle forms of
- *       deduction, now we can create puzzles that require them.
+ *     + then again, I don't actually like sudoku.com's interface;
+ *       it's too much like a paint package whereas I prefer to
+ *       think of Solo as a text editor.
+ *     + another PDA-friendly possibility is a drag interface:
+ *       _drag_ numbers from the palette into the grid squares.
+ *       Thought experiments suggest I'd prefer that to the
+ *       sudoku.com approach, but I haven't actually tried it.
  */
 
 /*
@@ -96,6 +125,7 @@ enum {
     COL_CLUE,
     COL_USER,
     COL_HIGHLIGHT,
+    COL_PENCIL,
     NCOLOURS
 };
 
@@ -106,6 +136,7 @@ struct game_params {
 struct game_state {
     int c, r;
     digit *grid;
+    unsigned char *pencil;             /* c*r*c*r elements */
     unsigned char *immutable;         /* marks which digits are clues */
     int completed, cheated;
 };
@@ -116,7 +147,7 @@ static game_params *default_params(void)
 
     ret->c = ret->r = 3;
     ret->symm = SYMM_ROT2;            /* a plausible default */
-    ret->diff = DIFF_SIMPLE;           /* so is this */
+    ret->diff = DIFF_BLOCK;           /* so is this */
 
     return ret;
 }
@@ -141,6 +172,7 @@ static int game_fetch_preset(int i, char **name, game_params **params)
     } presets[] = {
         { "2x2 Trivial", { 2, 2, SYMM_ROT2, DIFF_BLOCK } },
         { "2x3 Basic", { 2, 3, SYMM_ROT2, DIFF_SIMPLE } },
+        { "3x3 Trivial", { 3, 3, SYMM_ROT2, DIFF_BLOCK } },
         { "3x3 Basic", { 3, 3, SYMM_ROT2, DIFF_SIMPLE } },
         { "3x3 Intermediate", { 3, 3, SYMM_ROT2, DIFF_INTERSECT } },
         { "3x3 Advanced", { 3, 3, SYMM_ROT2, DIFF_SET } },
@@ -158,12 +190,9 @@ static int game_fetch_preset(int i, char **name, game_params **params)
     return TRUE;
 }
 
-static game_params *decode_params(char const *string)
+static void decode_params(game_params *ret, char const *string)
 {
-    game_params *ret = default_params();
-
     ret->c = ret->r = atoi(string);
-    ret->symm = SYMM_ROT2;
     while (*string && isdigit((unsigned char)*string)) string++;
     if (*string == 'x') {
         string++;
@@ -199,20 +228,28 @@ static game_params *decode_params(char const *string)
         } else
             string++;                  /* eat unknown character */
     }
-
-    return ret;
 }
 
-static char *encode_params(game_params *params)
+static char *encode_params(game_params *params, int full)
 {
     char str[80];
 
-    /*
-     * Symmetry is a game generation preference and hence is left
-     * out of the encoding. Users can add it back in as they see
-     * fit.
-     */
     sprintf(str, "%dx%d", params->c, params->r);
+    if (full) {
+        switch (params->symm) {
+          case SYMM_REF4: strcat(str, "m4"); break;
+          case SYMM_ROT4: strcat(str, "r4"); break;
+          /* case SYMM_ROT2: strcat(str, "r2"); break; [default] */
+          case SYMM_NONE: strcat(str, "a"); break;
+        }
+        switch (params->diff) {
+          /* case DIFF_BLOCK: strcat(str, "dt"); break; [default] */
+          case DIFF_SIMPLE: strcat(str, "db"); break;
+          case DIFF_INTERSECT: strcat(str, "di"); break;
+          case DIFF_SET: strcat(str, "da"); break;
+          case DIFF_RECURSIVE: strcat(str, "du"); break;
+        }
+    }
     return dupstr(str);
 }
 
@@ -1359,7 +1396,7 @@ struct game_aux_info {
     digit *grid;
 };
 
-static char *new_game_seed(game_params *params, random_state *rs,
+static char *new_game_desc(game_params *params, random_state *rs,
                           game_aux_info **aux)
 {
     int c = params->c, r = params->r, cr = c*r;
@@ -1368,7 +1405,7 @@ static char *new_game_seed(game_params *params, random_state *rs,
     struct xy { int x, y; } *locs;
     int nlocs;
     int ret;
-    char *seed;
+    char *desc;
     int coords[16], ncoords;
     int xlim, ylim;
     int maxdiff, recursing;
@@ -1502,14 +1539,14 @@ static char *new_game_seed(game_params *params, random_state *rs,
 
     /*
      * Now we have the grid as it will be presented to the user.
-     * Encode it in a game seed.
+     * Encode it in a game desc.
      */
     {
        char *p;
        int run, i;
 
-       seed = snewn(5 * area, char);
-       p = seed;
+       desc = snewn(5 * area, char);
+       p = desc;
        run = 0;
        for (i = 0; i <= area; i++) {
            int n = (i < area ? grid[i] : -1);
@@ -1531,7 +1568,7 @@ static char *new_game_seed(game_params *params, random_state *rs,
                     * bottom right, there's no point putting an
                     * unnecessary _ before or after it.
                     */
-                   if (p > seed && n > 0)
+                   if (p > desc && n > 0)
                        *p++ = '_';
                }
                if (n > 0)
@@ -1539,14 +1576,14 @@ static char *new_game_seed(game_params *params, random_state *rs,
                run = 0;
            }
        }
-       assert(p - seed < 5 * area);
+       assert(p - desc < 5 * area);
        *p++ = '\0';
-       seed = sresize(seed, p - seed, char);
+       desc = sresize(desc, p - desc, char);
     }
 
     sfree(grid);
 
-    return seed;
+    return desc;
 }
 
 static void game_free_aux_info(game_aux_info *aux)
@@ -1555,23 +1592,23 @@ static void game_free_aux_info(game_aux_info *aux)
     sfree(aux);
 }
 
-static char *validate_seed(game_params *params, char *seed)
+static char *validate_desc(game_params *params, char *desc)
 {
     int area = params->r * params->r * params->c * params->c;
     int squares = 0;
 
-    while (*seed) {
-        int n = *seed++;
+    while (*desc) {
+        int n = *desc++;
         if (n >= 'a' && n <= 'z') {
             squares += n - 'a' + 1;
         } else if (n == '_') {
             /* do nothing */;
         } else if (n > '0' && n <= '9') {
             squares++;
-            while (*seed >= '0' && *seed <= '9')
-                seed++;
+            while (*desc >= '0' && *desc <= '9')
+                desc++;
         } else
-            return "Invalid character in game specification";
+            return "Invalid character in game description";
     }
 
     if (squares < area)
@@ -1583,7 +1620,7 @@ static char *validate_seed(game_params *params, char *seed)
     return NULL;
 }
 
-static game_state *new_game(game_params *params, char *seed)
+static game_state *new_game(game_params *params, char *desc)
 {
     game_state *state = snew(game_state);
     int c = params->c, r = params->r, cr = c*r, area = cr * cr;
@@ -1593,14 +1630,16 @@ static game_state *new_game(game_params *params, char *seed)
     state->r = params->r;
 
     state->grid = snewn(area, digit);
+    state->pencil = snewn(area * cr, unsigned char);
+    memset(state->pencil, 0, area * cr);
     state->immutable = snewn(area, unsigned char);
     memset(state->immutable, FALSE, area);
 
     state->completed = state->cheated = FALSE;
 
     i = 0;
-    while (*seed) {
-        int n = *seed++;
+    while (*desc) {
+        int n = *desc++;
         if (n >= 'a' && n <= 'z') {
             int run = n - 'a' + 1;
             assert(i + run <= area);
@@ -1611,9 +1650,9 @@ static game_state *new_game(game_params *params, char *seed)
         } else if (n > '0' && n <= '9') {
             assert(i < area);
            state->immutable[i] = TRUE;
-            state->grid[i++] = atoi(seed-1);
-            while (*seed >= '0' && *seed <= '9')
-                seed++;
+            state->grid[i++] = atoi(desc-1);
+            while (*desc >= '0' && *desc <= '9')
+                desc++;
         } else {
             assert(!"We can't get here");
         }
@@ -1634,6 +1673,9 @@ static game_state *dup_game(game_state *state)
     ret->grid = snewn(area, digit);
     memcpy(ret->grid, state->grid, area);
 
+    ret->pencil = snewn(area * cr, unsigned char);
+    memcpy(ret->pencil, state->pencil, area * cr);
+
     ret->immutable = snewn(area, unsigned char);
     memcpy(ret->immutable, state->immutable, area);
 
@@ -1646,6 +1688,7 @@ static game_state *dup_game(game_state *state)
 static void free_game(game_state *state)
 {
     sfree(state->immutable);
+    sfree(state->pencil);
     sfree(state->grid);
     sfree(state);
 }
@@ -1756,6 +1799,11 @@ struct game_ui {
      * enter that number or letter in the grid.
      */
     int hx, hy;
+    /*
+     * This indicates whether the current highlight is a
+     * pencil-mark one or a real one.
+     */
+    int hpencil;
 };
 
 static game_ui *new_ui(game_state *state)
@@ -1763,6 +1811,7 @@ static game_ui *new_ui(game_state *state)
     game_ui *ui = snew(game_ui);
 
     ui->hx = ui->hy = -1;
+    ui->hpencil = 0;
 
     return ui;
 }
@@ -1779,16 +1828,26 @@ static game_state *make_move(game_state *from, game_ui *ui, int x, int y,
     int tx, ty;
     game_state *ret;
 
+    button &= ~MOD_MASK;
+
     tx = (x + TILE_SIZE - BORDER) / TILE_SIZE - 1;
     ty = (y + TILE_SIZE - BORDER) / TILE_SIZE - 1;
 
-    if (tx >= 0 && tx < cr && ty >= 0 && ty < cr && button == LEFT_BUTTON) {
+    if (tx >= 0 && tx < cr && ty >= 0 && ty < cr &&
+        (button == LEFT_BUTTON || button == RIGHT_BUTTON)) {
+        /*
+         * Prevent pencil-mode highlighting of a filled square.
+         */
+        if (button == RIGHT_BUTTON && from->grid[ty*cr+tx])
+            return NULL;
+
        if (tx == ui->hx && ty == ui->hy) {
            ui->hx = ui->hy = -1;
        } else {
            ui->hx = tx;
            ui->hy = ty;
        }
+        ui->hpencil = (button == RIGHT_BUTTON);
        return from;                   /* UI activity occurred */
     }
 
@@ -1808,17 +1867,32 @@ static game_state *make_move(game_state *from, game_ui *ui, int x, int y,
        if (from->immutable[ui->hy*cr+ui->hx])
            return NULL;               /* can't overwrite this square */
 
+        /*
+         * Can't make pencil marks in a filled square. In principle
+         * this shouldn't happen anyway because we should never
+         * 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])
+            return NULL;
+
        ret = dup_game(from);
-       ret->grid[ui->hy*cr+ui->hx] = n;
-       ui->hx = ui->hy = -1;
+        if (ui->hpencil && n > 0) {
+            int index = (ui->hy*cr+ui->hx) * 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);
 
-       /*
-        * We've made a real change to the grid. Check to see
-        * if the game has been completed.
-        */
-       if (!ret->completed && check_valid(c, r, ret->grid)) {
-           ret->completed = TRUE;
-       }
+            /*
+             * We've made a real change to the grid. Check to see
+             * if the game has been completed.
+             */
+            if (!ret->completed && check_valid(c, r, ret->grid)) {
+                ret->completed = TRUE;
+            }
+        }
+       ui->hx = ui->hy = -1;
 
        return ret;                    /* made a valid move */
     }
@@ -1834,6 +1908,7 @@ struct game_drawstate {
     int started;
     int c, r, cr;
     digit *grid;
+    unsigned char *pencil;
     unsigned char *hl;
 };
 
@@ -1870,6 +1945,10 @@ static float *game_colours(frontend *fe, game_state *state, int *ncolours)
     ret[COL_HIGHLIGHT * 3 + 1] = 0.85F * ret[COL_BACKGROUND * 3 + 1];
     ret[COL_HIGHLIGHT * 3 + 2] = 0.85F * ret[COL_BACKGROUND * 3 + 2];
 
+    ret[COL_PENCIL * 3 + 0] = 0.5F * ret[COL_BACKGROUND * 3 + 0];
+    ret[COL_PENCIL * 3 + 1] = 0.5F * ret[COL_BACKGROUND * 3 + 1];
+    ret[COL_PENCIL * 3 + 2] = ret[COL_BACKGROUND * 3 + 2];
+
     *ncolours = NCOLOURS;
     return ret;
 }
@@ -1885,6 +1964,8 @@ static game_drawstate *game_new_drawstate(game_state *state)
     ds->cr = cr;
     ds->grid = snewn(cr*cr, digit);
     memset(ds->grid, 0, cr*cr);
+    ds->pencil = snewn(cr*cr*cr, digit);
+    memset(ds->pencil, 0, cr*cr*cr);
     ds->hl = snewn(cr*cr, unsigned char);
     memset(ds->hl, 0, cr*cr);
 
@@ -1894,6 +1975,7 @@ static game_drawstate *game_new_drawstate(game_state *state)
 static void game_free_drawstate(game_drawstate *ds)
 {
     sfree(ds->hl);
+    sfree(ds->pencil);
     sfree(ds->grid);
     sfree(ds);
 }
@@ -1906,7 +1988,9 @@ static void draw_number(frontend *fe, game_drawstate *ds, game_state *state,
     int cx, cy, cw, ch;
     char str[2];
 
-    if (ds->grid[y*cr+x] == state->grid[y*cr+x] && ds->hl[y*cr+x] == hl)
+    if (ds->grid[y*cr+x] == state->grid[y*cr+x] &&
+        ds->hl[y*cr+x] == hl &&
+        !memcmp(ds->pencil+(y*cr+x)*cr, state->pencil+(y*cr+x)*cr, cr))
        return;                        /* no change required */
 
     tx = BORDER + x * TILE_SIZE + 2;
@@ -1928,9 +2012,20 @@ static void draw_number(frontend *fe, game_drawstate *ds, game_state *state,
 
     clip(fe, cx, cy, cw, ch);
 
-    /* background needs erasing? */
-    if (ds->grid[y*cr+x] || ds->hl[y*cr+x] != hl)
-       draw_rect(fe, cx, cy, cw, ch, hl ? COL_HIGHLIGHT : COL_BACKGROUND);
+    /* background needs erasing */
+    draw_rect(fe, cx, cy, cw, ch, hl == 1 ? COL_HIGHLIGHT : COL_BACKGROUND);
+
+    /* pencil-mode highlight */
+    if (hl == 2) {
+        int coords[6];
+        coords[0] = cx;
+        coords[1] = cy;
+        coords[2] = cx+cw/2;
+        coords[3] = cy;
+        coords[4] = cx;
+        coords[5] = cy+ch/2;
+        draw_polygon(fe, coords, 3, TRUE, COL_HIGHLIGHT);
+    }
 
     /* new number needs drawing? */
     if (state->grid[y*cr+x]) {
@@ -1941,6 +2036,23 @@ static void draw_number(frontend *fe, game_drawstate *ds, game_state *state,
        draw_text(fe, tx + TILE_SIZE/2, ty + TILE_SIZE/2,
                  FONT_VARIABLE, TILE_SIZE/2, ALIGN_VCENTRE | ALIGN_HCENTRE,
                  state->immutable[y*cr+x] ? COL_CLUE : COL_USER, str);
+    } else {
+        /* pencil marks required? */
+        int i, j;
+
+        for (i = j = 0; i < cr; i++)
+            if (state->pencil[(y*cr+x)*cr+i]) {
+                int dx = j % r, dy = j / r, crm = max(c, r);
+                str[1] = '\0';
+                str[0] = i + '1';
+                if (str[0] > '9')
+                    str[0] += 'a' - ('9'+1);
+                draw_text(fe, tx + (4*dx+3) * TILE_SIZE / (4*r+2),
+                          ty + (4*dy+3) * TILE_SIZE / (4*c+2),
+                          FONT_VARIABLE, TILE_SIZE/(crm*5/4),
+                          ALIGN_VCENTRE | ALIGN_HCENTRE, COL_PENCIL, str);
+                j++;
+            }
     }
 
     unclip(fe);
@@ -1948,6 +2060,7 @@ static void draw_number(frontend *fe, game_drawstate *ds, game_state *state,
     draw_update(fe, cx, cy, cw, ch);
 
     ds->grid[y*cr+x] = state->grid[y*cr+x];
+    memcpy(ds->pencil+(y*cr+x)*cr, state->pencil+(y*cr+x)*cr, cr);
     ds->hl[y*cr+x] = hl;
 }
 
@@ -1987,11 +2100,14 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate,
      */
     for (x = 0; x < cr; x++) {
        for (y = 0; y < cr; y++) {
-           draw_number(fe, ds, state, x, y,
-                       (x == ui->hx && y == ui->hy) ||
-                       (flashtime > 0 &&
-                        (flashtime <= FLASH_TIME/3 ||
-                         flashtime >= FLASH_TIME*2/3)));
+            int highlight = 0;
+            if (flashtime > 0 &&
+                (flashtime <= FLASH_TIME/3 ||
+                 flashtime >= FLASH_TIME*2/3))
+                highlight = 1;
+            if (x == ui->hx && y == ui->hy)
+                highlight = ui->hpencil ? 2 : 1;
+           draw_number(fe, ds, state, x, y, highlight);
        }
     }
 
@@ -2038,9 +2154,9 @@ const struct game thegame = {
     dup_params,
     TRUE, game_configure, custom_params,
     validate_params,
-    new_game_seed,
+    new_game_desc,
     game_free_aux_info,
-    validate_seed,
+    validate_desc,
     new_game,
     dup_game,
     free_game,
@@ -2101,7 +2217,7 @@ int main(int argc, char **argv)
     game_params *p;
     game_state *s;
     int recurse = TRUE;
-    char *id = NULL, *seed, *err;
+    char *id = NULL, *desc, *err;
     int y, x;
     int grade = FALSE;
 
@@ -2130,20 +2246,21 @@ int main(int argc, char **argv)
         return 1;
     }
 
-    seed = strchr(id, ':');
-    if (!seed) {
+    desc = strchr(id, ':');
+    if (!desc) {
         fprintf(stderr, "%s: game id expects a colon in it\n", argv[0]);
         return 1;
     }
-    *seed++ = '\0';
+    *desc++ = '\0';
 
-    p = decode_params(id);
-    err = validate_seed(p, seed);
+    p = default_params();
+    decode_params(p, id);
+    err = validate_desc(p, desc);
     if (err) {
         fprintf(stderr, "%s: %s\n", argv[0], err);
         return 1;
     }
-    s = new_game(p, seed);
+    s = new_game(p, desc);
 
     if (recurse) {
         int ret = rsolve(p->c, p->r, s->grid, NULL, 2);