shithub: puzzles

Download patch

ref: ee8ea9b9785964694cb2b3ad77c3fb2460f49510
parent: e2514a72e6c8d0269264d75d58186875cc5c027c
author: Simon Tatham <anakin@pobox.com>
date: Sat Nov 18 14:54:10 EST 2017

Permit redoing past an undone New Game action.

Now, when we undo a New Game by deserialising the stored version of
the previous game, we start by serialising the _current_ game into a
second serialisation buffer in the midend. Then, if the user tries to
redo back past that undo action, we can re-serialise the earlier game
and re-deserialise the newer one.

A few users had complained (with various degrees of politeness) at the
lack of this ability, because in true xkcd #1172 style, it broke their
workflow. Specifically, if you were a fan of holding down 'u' to undo
all the way back to the start of your current game, you'd have
overshot into the previous game and then have no way to return to the
game you wanted to be at the start of.

This slightly changes the previous interaction of Redo with New Game.
Previously, New Game would save the entire undo chain of the
serialised game, including anything forward of the current position;
so if you took actions move1,move2,undo,newgame then the serialised
game state would include both move1 and move2 with the current
position between them; hence, an undo would restore the old game to a
position just after move1, and then a redo action would re-enact
move2. Now, midend_purge_states is called before serialising the old
game, so in that scenario move2 would be completely lost as a side
effect of the new-game action. (Just as it would be if you made any
_other_ undoable move in that situation.)

Conversely, making a move in the old game after you've undone back
into it will now wipe out the record of the later game, so you can't
redo into it any more.

--- a/midend.c
+++ b/midend.c
@@ -68,7 +68,7 @@
     int nstates, statesize, statepos;
     struct midend_state_entry *states;
 
-    struct midend_serialise_buf newgame_undo;
+    struct midend_serialise_buf newgame_undo, newgame_redo;
 
     game_params *params, *curparams;
     game_drawstate *drawstate;
@@ -164,6 +164,8 @@
     me->states = NULL;
     me->newgame_undo.buf = NULL;
     me->newgame_undo.size = me->newgame_undo.len = 0;
+    me->newgame_redo.buf = NULL;
+    me->newgame_redo.size = me->newgame_redo.len = 0;
     me->params = ourgame->default_params();
     me->game_id_change_notify_function = NULL;
     me->game_id_change_notify_ctx = NULL;
@@ -225,6 +227,7 @@
         if (me->states[me->nstates].movestr)
             sfree(me->states[me->nstates].movestr);
     }
+    me->newgame_redo.len = 0;
 }
 
 static void midend_free_game(midend *me)
@@ -262,6 +265,7 @@
 	drawing_free(me->drawing);
     random_free(me->random);
     sfree(me->newgame_undo.buf);
+    sfree(me->newgame_redo.buf);
     sfree(me->states);
     sfree(me->desc);
     sfree(me->privdesc);
@@ -418,6 +422,7 @@
          * and just confuse us into thinking we had something to undo
          * to.
          */
+        midend_purge_states(me);
         midend_serialise(me, newgame_serialise_write, &me->newgame_undo);
     }
 
@@ -540,7 +545,7 @@
 
 int midend_can_redo(midend *me)
 {
-    return (me->statepos < me->nstates);
+    return (me->statepos < me->nstates || me->newgame_redo.len);
 }
 
 struct newgame_undo_deserialise_read_ctx {
@@ -627,9 +632,19 @@
         me->dir = -1;
         return 1;
     } else if (me->newgame_undo.len) {
-	/* This undo cannot be undone with redo */
 	struct newgame_undo_deserialise_read_ctx rctx;
 	struct newgame_undo_deserialise_check_ctx cctx;
+        struct midend_serialise_buf serbuf;
+
+        /*
+         * Serialise the current game so that you can later redo past
+         * this undo. Once we're committed to the undo actually
+         * happening, we'll copy this data into place.
+         */
+        serbuf.buf = NULL;
+        serbuf.len = serbuf.size = 0;
+        midend_serialise(me, newgame_serialise_write, &serbuf);
+
 	rctx.ser = &me->newgame_undo;
 	rctx.len = me->newgame_undo.len; /* copy for reentrancy safety */
 	rctx.pos = 0;
@@ -644,6 +659,7 @@
              * contain the dummy error message generated by our check
              * function, which we ignore.)
              */
+            sfree(serbuf.buf);
             return 0;
         } else {
             /*
@@ -654,6 +670,22 @@
              * replaced by the wrong file, etc., by user error.
              */
             assert(!deserialise_error);
+
+            /*
+             * Clear the old newgame_undo serialisation, so that we
+             * don't try to undo past the beginning of the game we've
+             * just gone back to and end up at the front of it again.
+             */
+            me->newgame_undo.len = 0;
+
+            /*
+             * Copy the serialisation of the game we've just left into
+             * the midend so that we can redo back into it later.
+             */
+            me->newgame_redo.len = 0;
+            newgame_serialise_write(&me->newgame_redo, serbuf.buf, serbuf.len);
+
+            sfree(serbuf.buf);
             return 1;
         }
     } else
@@ -662,6 +694,8 @@
 
 static int midend_redo(midend *me)
 {
+    const char *deserialise_error;
+
     if (me->statepos < me->nstates) {
         if (me->ui)
             me->ourgame->changed_state(me->ui,
@@ -670,6 +704,63 @@
 	me->statepos++;
         me->dir = +1;
         return 1;
+    } else if (me->newgame_redo.len) {
+	struct newgame_undo_deserialise_read_ctx rctx;
+	struct newgame_undo_deserialise_check_ctx cctx;
+        struct midend_serialise_buf serbuf;
+
+        /*
+         * Serialise the current game so that you can later undo past
+         * this redo. Once we're committed to the undo actually
+         * happening, we'll copy this data into place.
+         */
+        serbuf.buf = NULL;
+        serbuf.len = serbuf.size = 0;
+        midend_serialise(me, newgame_serialise_write, &serbuf);
+
+	rctx.ser = &me->newgame_redo;
+	rctx.len = me->newgame_redo.len; /* copy for reentrancy safety */
+	rctx.pos = 0;
+        cctx.refused = FALSE;
+        deserialise_error = midend_deserialise_internal(
+            me, newgame_undo_deserialise_read, &rctx,
+            newgame_undo_deserialise_check, &cctx);
+        if (cctx.refused) {
+            /*
+             * Our post-deserialisation check shows that we can't use
+             * this saved game after all. (deserialise_error will
+             * contain the dummy error message generated by our check
+             * function, which we ignore.)
+             */
+            sfree(serbuf.buf);
+            return 0;
+        } else {
+            /*
+             * There should never be any _other_ deserialisation
+             * error, because this serialised data has been held in
+             * our memory since it was created, and hasn't had any
+             * opportunity to be corrupted on disk, accidentally
+             * replaced by the wrong file, etc., by user error.
+             */
+            assert(!deserialise_error);
+
+            /*
+             * Clear the old newgame_redo serialisation, so that we
+             * don't try to redo past the end of the game we've just
+             * come into and end up at the back of it again.
+             */
+            me->newgame_redo.len = 0;
+
+            /*
+             * Copy the serialisation of the game we've just left into
+             * the midend so that we can undo back into it later.
+             */
+            me->newgame_undo.len = 0;
+            newgame_serialise_write(&me->newgame_undo, serbuf.buf, serbuf.len);
+
+            sfree(serbuf.buf);
+            return 1;
+        }
     } else
         return 0;
 }
@@ -2226,7 +2317,7 @@
     me->statepos = data.statepos;
 
     /*
-     * Don't save the "new game undo" state.  So "new game" twice or
+     * Don't save the "new game undo/redo" state.  So "new game" twice or
      * (in some environments) switching away and back, will make a
      * "new game" irreversible.  Maybe in the future we will have a
      * more sophisticated way to decide when to discard the previous
@@ -2233,6 +2324,7 @@
      * game state.
      */
     me->newgame_undo.len = 0;
+    me->newgame_redo.len = 0;
 
     {
         game_params *tmp;