Stop the analysis pass in Loopy's redraw routine from being
[sgt/puzzles] / midend.c
index 323ac97..53dca21 100644 (file)
--- a/midend.c
+++ b/midend.c
@@ -106,6 +106,22 @@ midend *midend_new(frontend *fe, const game *ourgame,
     me->nstates = me->statesize = me->statepos = 0;
     me->states = NULL;
     me->params = ourgame->default_params();
+    /*
+     * Allow environment-based changing of the default settings by
+     * defining a variable along the lines of `NET_DEFAULT=25x25w'
+     * in which the value is an encoded parameter string.
+     */
+    {
+        char buf[80], *e;
+        int j, k;
+        sprintf(buf, "%s_DEFAULT", me->ourgame->name);
+       for (j = k = 0; buf[j]; j++)
+           if (!isspace((unsigned char)buf[j]))
+               buf[k++] = toupper((unsigned char)buf[j]);
+       buf[k] = '\0';
+        if ((e = getenv(buf)) != NULL)
+            me->ourgame->decode_params(me->params, e);
+    }
     me->curparams = NULL;
     me->desc = me->privdesc = NULL;
     me->seedstr = NULL;
@@ -156,6 +172,20 @@ midend *midend_new(frontend *fe, const game *ourgame,
     return me;
 }
 
+const game *midend_which_game(midend *me)
+{
+    return me->ourgame;
+}
+
+static void midend_purge_states(midend *me)
+{
+    while (me->nstates > me->statepos) {
+        me->ourgame->free_game(me->states[--me->nstates].state);
+        if (me->states[me->nstates].movestr)
+            sfree(me->states[me->nstates].movestr);
+    }
+}
+
 static void midend_free_game(midend *me)
 {
     while (me->nstates > 0) {
@@ -404,6 +434,55 @@ void midend_new_game(midend *me)
        sfree(movestr);
     }
 
+    /*
+     * Soak test, enabled by setting <gamename>_TESTSOLVE in the
+     * environment. This causes an immediate attempt to re-solve the
+     * game without benefit of aux_info. The effect is that (at least
+     * on Unix) you can run 'FOO_TESTSOLVE=1 foo --generate 10000
+     * <params>#12345' and it will generate a lot of game ids and
+     * instantly pass each one back to the solver.
+     *
+     * (It's worth putting in an explicit seed in any such test, so
+     * you can repeat it to diagnose a problem if one comes up!)
+     */
+    {
+        char buf[80];
+        int j, k;
+        static int doing_test_solve = -1;
+        if (doing_test_solve < 0) {
+            sprintf(buf, "%s_TESTSOLVE", me->ourgame->name);
+            for (j = k = 0; buf[j]; j++)
+                if (!isspace((unsigned char)buf[j]))
+                    buf[k++] = toupper((unsigned char)buf[j]);
+            buf[k] = '\0';
+            if (getenv(buf)) {
+                /*
+                 * Since this is used for correctness testing, it's
+                 * helpful to have a visual acknowledgment that the
+                 * user hasn't mistyped the environment variable name.
+                 */
+                fprintf(stderr, "Running solver soak tests\n");
+                doing_test_solve = TRUE;
+            } else {
+                doing_test_solve = FALSE;
+            }
+        }
+        if (doing_test_solve) {
+            game_state *s;
+            char *msg, *movestr;
+
+            msg = NULL;
+            movestr = me->ourgame->solve(me->states[0].state,
+                                         me->states[0].state,
+                                         NULL, &msg);
+            assert(movestr && !msg);
+            s = me->ourgame->execute_move(me->states[0].state, movestr);
+            assert(s);
+            me->ourgame->free_game(s);
+            sfree(movestr);
+        }        
+    }
+
     me->states[me->nstates].movestr = NULL;
     me->states[me->nstates].movetype = NEWGAME;
     me->nstates++;
@@ -419,6 +498,16 @@ void midend_new_game(midend *me)
     me->pressed_mouse_button = 0;
 }
 
+int midend_can_undo(midend *me)
+{
+    return (me->statepos > 1);
+}
+
+int midend_can_redo(midend *me)
+{
+    return (me->statepos < me->nstates);
+}
+
 static int midend_undo(midend *me)
 {
     if (me->statepos > 1) {
@@ -511,8 +600,7 @@ void midend_restart_game(midend *me)
      * Now enter the restarted state as the next move.
      */
     midend_stop_anim(me);
-    while (me->nstates > me->statepos)
-       me->ourgame->free_game(me->states[--me->nstates].state);
+    midend_purge_states(me);
     ensure(me);
     me->states[me->nstates].state = s;
     me->states[me->nstates].movestr = dupstr(me->desc);
@@ -587,8 +675,7 @@ static int midend_really_process_key(midend *me, int x, int y, int button)
             goto done;
         } else if (s) {
            midend_stop_anim(me);
-            while (me->nstates > me->statepos)
-                me->ourgame->free_game(me->states[--me->nstates].state);
+            midend_purge_states(me);
             ensure(me);
             assert(movestr != NULL);
             me->states[me->nstates].state = s;
@@ -753,6 +840,15 @@ int midend_process_key(midend *me, int x, int y, int button)
       button = CURSOR_SELECT2;
 
     /*
+     * Normalise both backspace characters (8 and 127) to \b. Easier
+     * to do this once, here, than to require all front ends to
+     * carefully generate the same one - now each front end can
+     * generate whichever is easiest.
+     */
+    if (button == '\177')
+       button = '\b';
+
+    /*
      * Now send on the event we originally received.
      */
     ret = ret && midend_really_process_key(me, x, y, button);
@@ -1283,11 +1379,7 @@ char *midend_solve(midend *me)
      * Now enter the solved state as the next move.
      */
     midend_stop_anim(me);
-    while (me->nstates > me->statepos) {
-       me->ourgame->free_game(me->states[--me->nstates].state);
-        if (me->states[me->nstates].movestr)
-            sfree(me->states[me->nstates].movestr);
-    }
+    midend_purge_states(me);
     ensure(me);
     me->states[me->nstates].state = s;
     me->states[me->nstates].movestr = movestr;
@@ -1309,11 +1401,29 @@ char *midend_solve(midend *me)
        me->anim_time = 0.0;
        midend_finish_move(me);
     }
-    midend_redraw(me);
+    if (me->drawing)
+        midend_redraw(me);
     midend_set_timer(me);
     return NULL;
 }
 
+int midend_status(midend *me)
+{
+    /*
+     * We should probably never be called when the state stack has no
+     * states on it at all - ideally, midends should never be left in
+     * that state for long enough to get put down and forgotten about.
+     * But if we are, I think we return _true_ - pedantically speaking
+     * a midend in that state is 'vacuously solved', and more
+     * practically, a user whose midend has been left in that state
+     * probably _does_ want the 'new game' option to be prominent.
+     */
+    if (me->statepos == 0)
+        return +1;
+
+    return me->ourgame->status(me->states[me->statepos-1].state);
+}
+
 char *midend_rewrite_statusbar(midend *me, char *text)
 {
     /*
@@ -1844,6 +1954,111 @@ char *midend_deserialise(midend *me,
     return ret;
 }
 
+/*
+ * This function examines a saved game file just far enough to
+ * determine which game type it contains. It returns NULL on success
+ * and the game name string in 'name' (which will be dynamically
+ * allocated and should be caller-freed), or an error message on
+ * failure.
+ */
+char *identify_game(char **name, int (*read)(void *ctx, void *buf, int len),
+                    void *rctx)
+{
+    int nstates = 0, statepos = -1, gotstates = 0;
+    int started = FALSE;
+
+    char *val = NULL;
+    /* Initially all errors give the same report */
+    char *ret = "Data does not appear to be a saved game file";
+
+    *name = NULL;
+
+    /*
+     * Loop round and round reading one key/value pair at a time from
+     * the serialised stream, until we've found the game name.
+     */
+    while (nstates <= 0 || statepos < 0 || gotstates < nstates-1) {
+        char key[9], c;
+        int len;
+
+        do {
+            if (!read(rctx, key, 1)) {
+                /* unexpected EOF */
+                goto cleanup;
+            }
+        } while (key[0] == '\r' || key[0] == '\n');
+
+        if (!read(rctx, key+1, 8)) {
+            /* unexpected EOF */
+            goto cleanup;
+        }
+
+        if (key[8] != ':') {
+            if (started)
+                ret = "Data was incorrectly formatted for a saved game file";
+           goto cleanup;
+        }
+        len = strcspn(key, ": ");
+        assert(len <= 8);
+        key[len] = '\0';
+
+        len = 0;
+        while (1) {
+            if (!read(rctx, &c, 1)) {
+                /* unexpected EOF */
+                goto cleanup;
+            }
+
+            if (c == ':') {
+                break;
+            } else if (c >= '0' && c <= '9') {
+                len = (len * 10) + (c - '0');
+            } else {
+                if (started)
+                    ret = "Data was incorrectly formatted for a"
+                    " saved game file";
+                goto cleanup;
+            }
+        }
+
+        val = snewn(len+1, char);
+        if (!read(rctx, val, len)) {
+            if (started)
+            goto cleanup;
+        }
+        val[len] = '\0';
+
+        if (!started) {
+            if (strcmp(key, "SAVEFILE") || strcmp(val, SERIALISE_MAGIC)) {
+                /* ret already has the right message in it */
+                goto cleanup;
+            }
+            /* Now most errors are this one, unless otherwise specified */
+            ret = "Saved data ended unexpectedly";
+            started = TRUE;
+        } else {
+            if (!strcmp(key, "VERSION")) {
+                if (strcmp(val, SERIALISE_VERSION)) {
+                    ret = "Cannot handle this version of the saved game"
+                        " file format";
+                    goto cleanup;
+                }
+            } else if (!strcmp(key, "GAME")) {
+                *name = dupstr(val);
+                ret = NULL;
+                goto cleanup;
+            }
+        }
+
+        sfree(val);
+        val = NULL;
+    }
+
+    cleanup:
+    sfree(val);
+    return ret;
+}
+
 char *midend_print_puzzle(midend *me, document *doc, int with_soln)
 {
     game_state *soln = NULL;