chiark / gitweb /
Forbid undo of new-game if it would change the params.
[sgt-puzzles.git] / loopy.c
diff --git a/loopy.c b/loopy.c
index 136b9e9fa16c4ab1c8696b731b0b1accb4f0126c..92b27ab51690f10aa89983b8af934fa591dc1449 100644 (file)
--- a/loopy.c
+++ b/loopy.c
@@ -118,6 +118,7 @@ struct game_state {
     char *lines;
 
     unsigned char *line_errors;
+    int exactly_one_loop;
 
     int solved;
     int cheated;
@@ -242,32 +243,55 @@ static void check_caches(const solver_state* sstate);
 #define check_caches(s)
 #endif
 
-/* ------- List of grid generators ------- */
-#define GRIDLIST(A) \
-    A(Squares,GRID_SQUARE,3,3) \
-    A(Triangular,GRID_TRIANGULAR,3,3) \
-    A(Honeycomb,GRID_HONEYCOMB,3,3) \
-    A(Snub-Square,GRID_SNUBSQUARE,3,3) \
-    A(Cairo,GRID_CAIRO,3,4) \
-    A(Great-Hexagonal,GRID_GREATHEXAGONAL,3,3) \
-    A(Octagonal,GRID_OCTAGONAL,3,3) \
-    A(Kites,GRID_KITE,3,3) \
-    A(Floret,GRID_FLORET,1,2) \
-    A(Dodecagonal,GRID_DODECAGONAL,2,2) \
-    A(Great-Dodecagonal,GRID_GREATDODECAGONAL,2,2) \
-    A(Penrose (kite/dart),GRID_PENROSE_P2,3,3) \
-    A(Penrose (rhombs),GRID_PENROSE_P3,3,3)
-
-#define GRID_NAME(title,type,amin,omin) #title,
-#define GRID_CONFIG(title,type,amin,omin) ":" #title
-#define GRID_TYPE(title,type,amin,omin) type,
+/*
+ * Grid type config options available in Loopy.
+ *
+ * Annoyingly, we have to use an enum here which doesn't match up
+ * exactly to the grid-type enum in grid.h. Values in params->types
+ * are given by names such as LOOPY_GRID_SQUARE, which shouldn't be
+ * confused with GRID_SQUARE which is the value you pass to grid_new()
+ * and friends. So beware!
+ *
+ * (This is partly for historical reasons - Loopy's version of the
+ * enum is encoded in game parameter strings, so we keep it for
+ * backwards compatibility. But also, we need to store additional data
+ * here alongside each enum value, such as names for the presets menu,
+ * which isn't stored in grid.h; so we have to have our own list macro
+ * here anyway, and C doesn't make it easy to enforce that that lines
+ * up exactly with grid.h.)
+ *
+ * Do not add values to this list _except_ at the end, or old game ids
+ * will stop working!
+ */
+#define GRIDLIST(A)                                             \
+    A("Squares",SQUARE,3,3)                                     \
+    A("Triangular",TRIANGULAR,3,3)                              \
+    A("Honeycomb",HONEYCOMB,3,3)                                \
+    A("Snub-Square",SNUBSQUARE,3,3)                             \
+    A("Cairo",CAIRO,3,4)                                        \
+    A("Great-Hexagonal",GREATHEXAGONAL,3,3)                     \
+    A("Octagonal",OCTAGONAL,3,3)                                \
+    A("Kites",KITE,3,3)                                         \
+    A("Floret",FLORET,1,2)                                      \
+    A("Dodecagonal",DODECAGONAL,2,2)                            \
+    A("Great-Dodecagonal",GREATDODECAGONAL,2,2)                 \
+    A("Penrose (kite/dart)",PENROSE_P2,3,3)                     \
+    A("Penrose (rhombs)",PENROSE_P3,3,3)                        \
+    A("Great-Great-Dodecagonal",GREATGREATDODECAGONAL,2,2)      \
+    /* end of list */
+
+#define GRID_NAME(title,type,amin,omin) title,
+#define GRID_CONFIG(title,type,amin,omin) ":" title
+#define GRID_LOOPYTYPE(title,type,amin,omin) LOOPY_GRID_ ## type,
+#define GRID_GRIDTYPE(title,type,amin,omin) GRID_ ## type,
 #define GRID_SIZES(title,type,amin,omin) \
     {amin, omin, \
      "Width and height for this grid type must both be at least " #amin, \
      "At least one of width and height for this grid type must be at least " #omin,},
+enum { GRIDLIST(GRID_LOOPYTYPE) LOOPY_GRID_DUMMY_TERMINATOR };
 static char const *const gridnames[] = { GRIDLIST(GRID_NAME) };
 #define GRID_CONFIGS GRIDLIST(GRID_CONFIG)
-static grid_type grid_types[] = { GRIDLIST(GRID_TYPE) };
+static grid_type grid_types[] = { GRIDLIST(GRID_GRIDTYPE) };
 #define NUM_GRID_TYPES (sizeof(grid_types) / sizeof(grid_types[0]))
 static const struct {
     int amin, omin;
@@ -325,6 +349,7 @@ static game_state *dup_game(const game_state *state)
 
     ret->line_errors = snewn(state->game_grid->num_edges, unsigned char);
     memcpy(ret->line_errors, state->line_errors, state->game_grid->num_edges);
+    ret->exactly_one_loop = state->exactly_one_loop;
 
     ret->grid_type = state->grid_type;
     return ret;
@@ -488,61 +513,82 @@ static game_params *dup_params(const game_params *params)
     return ret;
 }
 
-static const game_params presets[] = {
+static const game_params loopy_presets_top[] = {
 #ifdef SMALL_SCREEN
-    {  7,  7, DIFF_EASY, 0 },
-    {  7,  7, DIFF_NORMAL, 0 },
-    {  7,  7, DIFF_HARD, 0 },
-    {  7,  7, DIFF_HARD, 1 },
-    {  7,  7, DIFF_HARD, 2 },
-    {  5,  5, DIFF_HARD, 3 },
-    {  7,  7, DIFF_HARD, 4 },
-    {  5,  4, DIFF_HARD, 5 },
-    {  5,  5, DIFF_HARD, 6 },
-    {  5,  5, DIFF_HARD, 7 },
-    {  3,  3, DIFF_HARD, 8 },
-    {  3,  3, DIFF_HARD, 9 },
-    {  3,  3, DIFF_HARD, 10 },
-    {  6,  6, DIFF_HARD, 11 },
-    {  6,  6, DIFF_HARD, 12 },
+    {  7,  7, DIFF_EASY,   LOOPY_GRID_SQUARE },
+    {  7,  7, DIFF_NORMAL, LOOPY_GRID_SQUARE },
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_SQUARE },
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_TRIANGULAR },
+    {  5,  5, DIFF_HARD,   LOOPY_GRID_SNUBSQUARE },
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_CAIRO },
+    {  5,  5, DIFF_HARD,   LOOPY_GRID_KITE },
+    {  6,  6, DIFF_HARD,   LOOPY_GRID_PENROSE_P2 },
+    {  6,  6, DIFF_HARD,   LOOPY_GRID_PENROSE_P3 },
 #else
-    {  7,  7, DIFF_EASY, 0 },
-    {  10,  10, DIFF_EASY, 0 },
-    {  7,  7, DIFF_NORMAL, 0 },
-    {  10,  10, DIFF_NORMAL, 0 },
-    {  7,  7, DIFF_HARD, 0 },
-    {  10,  10, DIFF_HARD, 0 },
-    {  10,  10, DIFF_HARD, 1 },
-    {  12,  10, DIFF_HARD, 2 },
-    {  7,  7, DIFF_HARD, 3 },
-    {  9,  9, DIFF_HARD, 4 },
-    {  5,  4, DIFF_HARD, 5 },
-    {  7,  7, DIFF_HARD, 6 },
-    {  5,  5, DIFF_HARD, 7 },
-    {  5,  5, DIFF_HARD, 8 },
-    {  5,  4, DIFF_HARD, 9 },
-    {  5,  4, DIFF_HARD, 10 },
-    {  10, 10, DIFF_HARD, 11 },
-    {  10, 10, DIFF_HARD, 12 }
+    {  7,  7, DIFF_EASY,   LOOPY_GRID_SQUARE },
+    { 10, 10, DIFF_EASY,   LOOPY_GRID_SQUARE },
+    {  7,  7, DIFF_NORMAL, LOOPY_GRID_SQUARE },
+    { 10, 10, DIFF_NORMAL, LOOPY_GRID_SQUARE },
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_SQUARE },
+    { 10, 10, DIFF_HARD,   LOOPY_GRID_SQUARE },
+    { 12, 10, DIFF_HARD,   LOOPY_GRID_TRIANGULAR },
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_SNUBSQUARE },
+    {  9,  9, DIFF_HARD,   LOOPY_GRID_CAIRO },
+    {  5,  5, DIFF_HARD,   LOOPY_GRID_KITE },
+    { 10, 10, DIFF_HARD,   LOOPY_GRID_PENROSE_P2 },
+    { 10, 10, DIFF_HARD,   LOOPY_GRID_PENROSE_P3 },
 #endif
 };
 
-static int game_fetch_preset(int i, char **name, game_params **params)
+static const game_params loopy_presets_more[] = {
+#ifdef SMALL_SCREEN
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_HONEYCOMB },
+    {  5,  4, DIFF_HARD,   LOOPY_GRID_GREATHEXAGONAL },
+    {  5,  5, DIFF_HARD,   LOOPY_GRID_OCTAGONAL },
+    {  3,  3, DIFF_HARD,   LOOPY_GRID_FLORET },
+    {  3,  3, DIFF_HARD,   LOOPY_GRID_DODECAGONAL },
+    {  3,  3, DIFF_HARD,   LOOPY_GRID_GREATDODECAGONAL },
+    {  3,  2, DIFF_HARD,   LOOPY_GRID_GREATGREATDODECAGONAL },
+#else
+    { 10, 10, DIFF_HARD,   LOOPY_GRID_HONEYCOMB },
+    {  5,  4, DIFF_HARD,   LOOPY_GRID_GREATHEXAGONAL },
+    {  7,  7, DIFF_HARD,   LOOPY_GRID_OCTAGONAL },
+    {  5,  5, DIFF_HARD,   LOOPY_GRID_FLORET },
+    {  5,  4, DIFF_HARD,   LOOPY_GRID_DODECAGONAL },
+    {  5,  4, DIFF_HARD,   LOOPY_GRID_GREATDODECAGONAL },
+    {  5,  3, DIFF_HARD,   LOOPY_GRID_GREATGREATDODECAGONAL },
+#endif
+};
+
+static void preset_menu_add_preset_with_title(struct preset_menu *menu,
+                                              const game_params *params)
 {
-    game_params *tmppar;
     char buf[80];
+    game_params *dup_params;
 
-    if (i < 0 || i >= lenof(presets))
-        return FALSE;
+    sprintf(buf, "%dx%d %s - %s", params->h, params->w,
+            gridnames[params->type], diffnames[params->diff]);
 
-    tmppar = snew(game_params);
-    *tmppar = presets[i];
-    *params = tmppar;
-    sprintf(buf, "%dx%d %s - %s", tmppar->h, tmppar->w,
-            gridnames[tmppar->type], diffnames[tmppar->diff]);
-    *name = dupstr(buf);
+    dup_params = snew(game_params);
+    *dup_params = *params;
 
-    return TRUE;
+    preset_menu_add_preset(menu, dupstr(buf), dup_params);
+}
+
+static struct preset_menu *game_preset_menu(void)
+{
+    struct preset_menu *top, *more;
+    int i;
+
+    top = preset_menu_new();
+    for (i = 0; i < lenof(loopy_presets_top); i++)
+        preset_menu_add_preset_with_title(top, &loopy_presets_top[i]);
+
+    more = preset_menu_add_submenu(top, dupstr("More..."));
+    for (i = 0; i < lenof(loopy_presets_more); i++)
+        preset_menu_add_preset_with_title(more, &loopy_presets_more[i]);
+
+    return top;
 }
 
 static void free_params(game_params *params)
@@ -1380,6 +1426,7 @@ static char *new_game_desc(const game_params *params, random_state *rs,
     state->clues = snewn(g->num_faces, signed char);
     state->lines = snewn(g->num_edges, char);
     state->line_errors = snewn(g->num_edges, unsigned char);
+    state->exactly_one_loop = FALSE;
 
     state->grid_type = params->type;
 
@@ -1451,6 +1498,7 @@ static game_state *new_game(midend *me, const game_params *params,
     state->clues = snewn(num_faces, signed char);
     state->lines = snewn(num_edges, char);
     state->line_errors = snewn(num_edges, unsigned char);
+    state->exactly_one_loop = FALSE;
 
     state->solved = state->cheated = FALSE;
 
@@ -1491,7 +1539,7 @@ static int check_completion(game_state *state)
     grid *g = state->game_grid;
     int i, ret;
     int *dsf, *component_state;
-    int nsilly, nloop, npath, largest_comp, largest_size;
+    int nsilly, nloop, npath, largest_comp, largest_size, total_pathsize;
     enum { COMP_NONE, COMP_LOOP, COMP_PATH, COMP_SILLY, COMP_EMPTY };
 
     memset(state->line_errors, 0, g->num_edges);
@@ -1560,15 +1608,19 @@ static int check_completion(game_state *state)
      *    hence they all consist of either a simple loop, or a simple
      *    path with two endpoints.
      *
-     *  - If the sensible components are all paths, or if there's
-     *    exactly one of them and it is a loop, then highlight no
-     *    further edge errors. (The former case is normal during play,
-     *    and the latter is a potentially solved puzzle.)
+     *  - For these purposes, group together all the paths and imagine
+     *    them to be a single component (because in most normal
+     *    situations the player will gradually build up the solution
+     *    _not_ all in one connected segment, but as lots of separate
+     *    little path pieces that gradually connect to each other).
      *
-     *  - Otherwise - if there is more than one sensible component
-     *    _and_ at least one of them is a loop - find the largest of
-     *    the sensible components, leave that one unhighlighted, and
-     *    light the rest up in red.
+     *  - After doing that, if there is exactly one (sensible)
+     *    component - be it a collection of paths or a loop - then
+     *    highlight no further edge errors. (The former case is normal
+     *    during play, and the latter is a potentially solved puzzle.)
+     *
+     *  - Otherwise, find the largest of the sensible components,
+     *    leave that one unhighlighted, and light the rest up in red.
      */
 
     dsf = snew_dsf(g->num_dots);
@@ -1636,18 +1688,18 @@ static int check_completion(game_state *state)
      * vertices in the grid data structure, which is fairly arbitrary
      * but at least stays stable throughout the game.) */
     nsilly = nloop = npath = 0;
+    total_pathsize = 0;
     largest_comp = largest_size = -1;
     for (i = 0; i < g->num_dots; i++) {
         if (component_state[i] == COMP_SILLY) {
             nsilly++;
-        } else if (component_state[i] == COMP_PATH ||
-                   component_state[i] == COMP_LOOP) {
+        } 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;
 
-            if (component_state[i] == COMP_PATH)
-                npath++;
-            else if (component_state[i] == COMP_LOOP)
-                nloop++;
+            nloop++;
 
             if ((this_size = dsf_size(dsf, i)) > largest_size) {
                 largest_comp = i;
@@ -1655,6 +1707,10 @@ static int check_completion(game_state *state)
             }
         }
     }
+    if (largest_size < total_pathsize) {
+        largest_comp = -1;             /* means the paths */
+        largest_size = total_pathsize;
+    }
 
     if (nloop > 0 && nloop + npath > 1) {
         /*
@@ -1667,8 +1723,10 @@ static int check_completion(game_state *state)
                 grid_edge *e = g->edges + i;
                 int d1 = e->dot1 - g->dots; /* either endpoint is good enough */
                 int comp = dsf_canonify(dsf, d1);
-                if (component_state[comp] != COMP_SILLY &&
-                    comp != largest_comp)
+                if ((component_state[comp] == COMP_PATH &&
+                     -1 != largest_comp) ||
+                    (component_state[comp] == COMP_LOOP &&
+                     comp != largest_comp))
                     state->line_errors[i] = TRUE;
             }
         }
@@ -1688,8 +1746,17 @@ static int check_completion(game_state *state)
                 break;
             }
         }
+
+        /*
+         * Also, whether or not the puzzle is actually complete, set
+         * the flag that says this game_state has exactly one loop and
+         * nothing else, which will be used to vary the semantics of
+         * clue highlighting at display time.
+         */
+        state->exactly_one_loop = TRUE;
     } else {
         ret = FALSE;
+        state->exactly_one_loop = FALSE;
     }
 
     sfree(component_state);
@@ -2880,7 +2947,8 @@ static char *interpret_move(const game_state *state, game_ui *ui,
     grid *g = state->game_grid;
     grid_edge *e;
     int i;
-    char *ret, buf[80];
+    char *movebuf;
+    int movelen, movesize;
     char button_char = ' ';
     enum line_state old_state;
 
@@ -2942,11 +3010,83 @@ static char *interpret_move(const game_state *state, game_ui *ui,
        return NULL;
     }
 
+    movelen = 0;
+    movesize = 80;
+    movebuf = snewn(movesize, char);
+    movelen = sprintf(movebuf, "%d%c", i, (int)button_char);
+    {
+        static enum { OFF, FIXED, ADAPTIVE, DUNNO } autofollow = DUNNO;
+        if (autofollow == DUNNO) {
+            const char *env = getenv("LOOPY_AUTOFOLLOW");
+            if (env && !strcmp(env, "off"))
+                autofollow = OFF;
+            else if (env && !strcmp(env, "fixed"))
+                autofollow = FIXED;
+            else if (env && !strcmp(env, "adaptive"))
+                autofollow = ADAPTIVE;
+            else
+                autofollow = OFF;
+        }
 
-    sprintf(buf, "%d%c", i, (int)button_char);
-    ret = dupstr(buf);
+        if (autofollow != OFF) {
+            int dotid;
+            for (dotid = 0; dotid < 2; dotid++) {
+                grid_dot *dot = (dotid == 0 ? e->dot1 : e->dot2);
+                grid_edge *e_this = e;
+
+                while (1) {
+                    int j, n_found;
+                    grid_edge *e_next = NULL;
+
+                    for (j = n_found = 0; j < dot->order; j++) {
+                        grid_edge *e_candidate = dot->edges[j];
+                        int i_candidate = e_candidate - g->edges;
+                        if (e_candidate != e_this &&
+                            (autofollow == FIXED ||
+                             state->lines[i] == LINE_NO ||
+                             state->lines[i_candidate] != LINE_NO)) {
+                            e_next = e_candidate;
+                            n_found++;
+                        }
+                    }
 
-    return ret;
+                    if (n_found != 1 ||
+                        state->lines[e_next - g->edges] != state->lines[i])
+                        break;
+
+                    if (e_next == e) {
+                        /*
+                         * Special case: we might have come all the
+                         * way round a loop and found our way back to
+                         * the same edge we started from. In that
+                         * situation, we must terminate not only this
+                         * while loop, but the 'for' outside it that
+                         * was tracing in both directions from the
+                         * starting edge, because if we let it trace
+                         * in the second direction then we'll only
+                         * find ourself traversing the same loop in
+                         * the other order and generate an encoded
+                         * move string that mentions the same set of
+                         * edges twice.
+                         */
+                        goto autofollow_done;
+                    }
+
+                    dot = (e_next->dot1 != dot ? e_next->dot1 : e_next->dot2);
+                    if (movelen > movesize - 40) {
+                        movesize = movesize * 5 / 4 + 128;
+                        movebuf = sresize(movebuf, movesize, char);
+                    }
+                    e_this = e_next;
+                    movelen += sprintf(movebuf+movelen, "%d%c",
+                                       (int)(e_this - g->edges), button_char);
+                }
+            }
+          autofollow_done:;
+        }
+    }
+
+    return sresize(movebuf, movelen+1, char);
 }
 
 static game_state *execute_move(const game_state *state, const char *move)
@@ -3263,16 +3403,53 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
     for (i = 0; i < g->num_faces; i++) {
         grid_face *f = g->faces + i;
         int sides = f->order;
+        int yes_order, no_order;
         int clue_mistake;
         int clue_satisfied;
         int n = state->clues[i];
         if (n < 0)
             continue;
 
-        clue_mistake = (face_order(state, i, LINE_YES) > n ||
-                        face_order(state, i, LINE_NO ) > (sides-n));
-        clue_satisfied = (face_order(state, i, LINE_YES) == n &&
-                          face_order(state, i, LINE_NO ) == (sides-n));
+        yes_order = face_order(state, i, LINE_YES);
+        if (state->exactly_one_loop) {
+            /*
+             * Special case: if the set of LINE_YES edges in the grid
+             * consists of exactly one loop and nothing else, then we
+             * switch to treating LINE_UNKNOWN the same as LINE_NO for
+             * purposes of clue checking.
+             *
+             * This is because some people like to play Loopy without
+             * using the right-click, i.e. never setting anything to
+             * LINE_NO. Without this special case, if a person playing
+             * in that style fills in what they think is a correct
+             * solution loop but in fact it has an underfilled clue,
+             * then we will display no victory flash and also no error
+             * highlight explaining why not. With this special case,
+             * we light up underfilled clues at the instant the loop
+             * is closed. (Of course, *overfilled* clues are fine
+             * either way.)
+             *
+             * (It might still be considered unfortunate that we can't
+             * warn this style of player any earlier, if they make a
+             * mistake very near the beginning which doesn't show up
+             * until they close the last edge of the loop. One other
+             * thing we _could_ do here is to treat any LINE_UNKNOWN
+             * as LINE_NO if either of its endpoints has yes-degree 2,
+             * reflecting the fact that setting that line to YES would
+             * be an obvious error. But I don't think even that could
+             * catch _all_ clue errors in a timely manner; I think
+             * there are some that won't be displayed until the loop
+             * is filled in, even so, and there's no way to avoid that
+             * with complete reliability except to switch to being a
+             * player who sets things to LINE_NO.)
+             */
+            no_order = sides - yes_order;
+        } else {
+            no_order = face_order(state, i, LINE_NO);
+        }
+
+        clue_mistake = (yes_order > n || no_order > (sides-n));
+        clue_satisfied = (yes_order == n && no_order == (sides-n));
 
         if (clue_mistake != ds->clue_error[i] ||
             clue_satisfied != ds->clue_satisfied[i]) {
@@ -3463,7 +3640,7 @@ static void game_print(drawing *dr, const game_state *state, int tilesize)
 const struct game thegame = {
     "Loopy", "games.loopy", "loopy",
     default_params,
-    game_fetch_preset,
+    NULL, game_preset_menu,
     decode_params,
     encode_params,
     free_params,