[gnome-tetravex] Save game on exit.



commit 2eb4d9e13a643fe86702f0c7748bf894a6344738
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Tue Oct 15 19:59:34 2019 +0200

    Save game on exit.
    
    Fixes #1.

 data/org.gnome.Tetravex.gschema.xml |  17 ++-
 src/gnome-tetravex.vala             |  80 +++++++++++++-
 src/puzzle.vala                     | 210 +++++++++++++++++++++++++++++++++++-
 3 files changed, 293 insertions(+), 14 deletions(-)
---
diff --git a/data/org.gnome.Tetravex.gschema.xml b/data/org.gnome.Tetravex.gschema.xml
index 362b94f..387a17c 100644
--- a/data/org.gnome.Tetravex.gschema.xml
+++ b/data/org.gnome.Tetravex.gschema.xml
@@ -11,17 +11,17 @@
       <range min="2" max="6" />
       <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/grid-size' -->
       <summary>The size of the playing grid</summary>
-      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/grid-size' -->
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Tetravex/grid-size' -->
       <description>The value of this key is used to decide the size of the playing grid.</description>
     </key>
-    <key type="b" name="mouse-use-extra-buttons">
+    <key name="mouse-use-extra-buttons" type="b">
       <default>true</default>
       <!-- Translators: summary of a settings key, see 'dconf-editor 
/org/gnome/Tetravex/mouse-use-extra-buttons' -->
       <summary>Use “Back” and “Forward” mouse button events</summary>
       <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Tetravex/mouse-use-extra-buttons' -->
       <description>For users which have a mouse with “Forward” and “Back” buttons, this key will determine 
if any action is taken inside of the window when either is pressed.</description>
     </key>
-    <key type="i" name="mouse-back-button">
+    <key name="mouse-back-button" type="i">
       <default>8</default>
       <range min="6" max="14"/>
       <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/mouse-back-buttons' 
-->
@@ -29,7 +29,7 @@
       <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Tetravex/mouse-back-buttons' -->
       <description>For users which have a mouse with “Forward” and “Back” buttons, this key will set which 
button activates the “Undo” command. Possible values range between 6 and 14.</description>
     </key>
-    <key type="i" name="mouse-forward-button">
+    <key name="mouse-forward-button" type="i">
       <default>9</default>
       <range min="6" max="14"/>
       <!-- Translators: summary of a settings key, see 'dconf-editor 
/org/gnome/Tetravex/mouse-forward-buttons' -->
@@ -37,6 +37,13 @@
       <!-- Translators: description of a settings key, see 'dconf-editor 
/org/gnome/Tetravex/mouse-forward-buttons' -->
       <description>For users which have a mouse with “Forward” and “Back” buttons, this key will set which 
button activates the “Redo” command. Possible values range between 6 and 14.</description>
     </key>
+    <key name="saved-game" type="m(yyda(yyyyyyyy)ua(yyyyu))">
+      <default>nothing</default>
+      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/saved-game' -->
+      <summary>Saved game, if any</summary>
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Tetravex/saved-game'; 
“nothing” is a technical keyword, you might want to translate it: « “nothing” (your translation) » -->
+      <description>The key has value “nothing” if there is no saved game. Else, it contains a description of 
the board, with its size, the number of colors, and the time elapsed, then the list of tiles, saved as 
properties: their current position (x and y), their colors (north, east, south and west), and their original 
position (x and y), and finally the history, with the last move index, and history entries saved as 
properties: coordinates of the two tiles swapped, and the move id.</description>
+    </key>
     <key name="theme" enum="org.gnome.Tetravex.Theme">
       <default>'extrusion'</default>
       <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/theme' -->
@@ -47,7 +54,7 @@
       <default>600</default>
       <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/window-width' -->
       <summary>The width of the window</summary>
-      <!-- Translators: summary of a settings key, see 'dconf-editor /org/gnome/Tetravex/window-width' -->
+      <!-- Translators: description of a settings key, see 'dconf-editor /org/gnome/Tetravex/window-width' 
-->
       <description>The width of the main window in pixels.</description>
     </key>
     <key name="window-height" type="i">
diff --git a/src/gnome-tetravex.vala b/src/gnome-tetravex.vala
index a4959a4..9715286 100644
--- a/src/gnome-tetravex.vala
+++ b/src/gnome-tetravex.vala
@@ -40,6 +40,7 @@ private class Tetravex : Gtk.Application
     private History history;
 
     private PuzzleView view;
+    private Button pause_button;
 
     private ApplicationWindow window;
     private int window_width;
@@ -158,6 +159,9 @@ private class Tetravex : Gtk.Application
 
         settings = new GLib.Settings ("org.gnome.Tetravex");
 
+        saved_game = settings.get_value ("saved-game");
+        can_restore = Puzzle.is_valid_saved_game (saved_game);
+
         add_action_entries (action_entries, this);
         add_action (settings.create_action ("theme"));
 
@@ -234,7 +238,26 @@ private class Tetravex : Gtk.Application
         undo_redo_box.pack_start (undo_button);
         undo_redo_box.pack_start (redo_button);
         undo_redo_box.show ();
-        headerbar.pack_start (undo_redo_box);
+
+        if (can_restore)
+        {
+            restore_stack = new Stack ();
+            restore_stack.hhomogeneous = false;
+            restore_stack.add_named (undo_redo_box, "undo-redo-box");
+
+            /* Translator: label of a button displayed in the headerbar at game start, if a previous game 
was being played while the window was closed; restores the saved game */
+            Button restore_button = new Button.with_label (_("Restore last game"));
+            restore_button.clicked.connect (restore_game);
+            ((Widget) restore_button).set_focus_on_click (false);
+            restore_button.show ();
+
+            restore_stack.add (restore_button);
+            restore_stack.set_visible_child (restore_button);
+            restore_stack.visible = true;
+            headerbar.pack_start (restore_stack);
+        }
+        else
+            headerbar.pack_start (undo_redo_box);
 
         Grid grid = (Grid) builder.get_object ("grid");
 
@@ -277,7 +300,7 @@ private class Tetravex : Gtk.Application
                                                     /* align end */ false,
                                                     sizegroup);
 
-        Button pause_button     = new BottomButton ("media-playback-pause-symbolic",
+        pause_button            = new BottomButton ("media-playback-pause-symbolic",
                                                     "app.pause",
         /* Translators: tooltip text of the pause button, in the bottom bar */
                                                     _("Pause the game"),
@@ -399,11 +422,14 @@ private class Tetravex : Gtk.Application
     {
         base.shutdown ();
 
-        /* Save window state */
         settings.delay ();
         settings.set_int ("window-width", window_width);
         settings.set_int ("window-height", window_height);
         settings.set_boolean ("window-is-maximized", is_maximized);
+        if (puzzle.game_in_progress && !puzzle.is_solved)
+            settings.set_value ("saved-game", puzzle.to_variant ());
+        else
+            settings.@set ("saved-game", "m(yyda(yyyyyyyy)ua(yyyyu))", null);
         settings.apply ();
     }
 
@@ -412,7 +438,26 @@ private class Tetravex : Gtk.Application
         window.present ();
     }
 
-    private void new_game ()
+    private Stack restore_stack;
+    private Variant saved_game;
+    private bool can_restore = false;
+    private void restore_game ()
+    {
+        if (!can_restore)
+            assert_not_reached ();
+
+        new_game (saved_game);
+        hide_restore_button ();
+    }
+    private void hide_restore_button ()
+    {
+        if (!can_restore)
+            assert_not_reached ();
+
+        restore_stack.set_visible_child_name ("undo-redo-box");
+    }
+
+    private void new_game (Variant? saved_game = null)
     {
         puzzle_is_finished = false;
         has_been_finished = false;
@@ -422,11 +467,24 @@ private class Tetravex : Gtk.Application
         new_game_solve_stack.set_visible_child_name ("solve");
         score_overlay.hide ();
 
+        bool was_paused;
         if (puzzle_init_done)
+        {
+            was_paused = puzzle.paused;
             SignalHandler.disconnect_by_func (puzzle, null, this);
+        }
+        else
+            was_paused = false;
 
         int size = settings.get_int (KEY_GRID_SIZE);
-        puzzle = new Puzzle ((uint8) size, (uint8) colors);
+        if (saved_game == null)
+            puzzle = new Puzzle ((uint8) size, (uint8) colors);
+        else
+        {
+            puzzle = new Puzzle.restore ((!) saved_game);
+            if (puzzle.is_solved_right)
+                solved_right_cb ();
+        }
         puzzle_init_done = true;
         puzzle.tick.connect (tick_cb);
         puzzle.solved.connect (solved_cb);
@@ -435,6 +493,8 @@ private class Tetravex : Gtk.Application
             undo_action.set_enabled (puzzle.can_undo && !puzzle.is_solved && !puzzle.paused));
         puzzle.notify ["can-redo"].connect (() =>
             redo_action.set_enabled (puzzle.can_redo && !puzzle.is_solved && !puzzle.paused));
+        if (can_restore)
+            puzzle.tile_moved.connect (hide_restore_button);
         puzzle.show_end_game.connect (show_end_game_cb);
         view.puzzle = puzzle;
         tick_cb ();
@@ -443,6 +503,12 @@ private class Tetravex : Gtk.Application
         {
             puzzle.paused = true;
             start_paused = false;
+            pause_button.grab_focus ();
+        }
+        else if (was_paused && saved_game != null)
+        {
+            puzzle.paused = true;
+            pause_button.grab_focus ();
         }
         else
             view.grab_focus ();
@@ -686,6 +752,10 @@ private class Tetravex : Gtk.Application
         undo_action.set_enabled (puzzle.can_undo && !puzzle.is_solved && !puzzle.paused);
         redo_action.set_enabled (puzzle.can_redo && !puzzle.is_solved && !puzzle.paused);
         update_bottom_button_states ();
+        if (puzzle.paused)
+            pause_button.grab_focus ();
+        else
+            view.grab_focus ();
     }
 
     private void update_bottom_button_states ()
diff --git a/src/puzzle.vala b/src/puzzle.vala
index a5947bd..cd5bb6c 100644
--- a/src/puzzle.vala
+++ b/src/puzzle.vala
@@ -44,9 +44,9 @@ private class Puzzle : Object
     private Tile? [,] board;
 
     /* Game timer */
-    private double clock_elapsed;
     private Timer? clock;
     private uint clock_timeout;
+    public double initial_time { private get; protected construct; default = 0.0; }
 
     [CCode (notify = false)] internal double elapsed
     {
@@ -54,7 +54,7 @@ private class Puzzle : Object
         {
             if (clock == null)
                 return 0.0;
-            return clock_elapsed + ((!) clock).elapsed ();
+            return initial_time + ((!) clock).elapsed ();
         }
     }
 
@@ -100,6 +100,7 @@ private class Puzzle : Object
         return true;
     }
 
+    public bool restored { private get; protected construct; default = false; }
     internal Puzzle (uint8 size, uint8 colors)
     {
         Object (size: size, colors: colors);
@@ -107,8 +108,11 @@ private class Puzzle : Object
 
     construct
     {
-        do { init_board (size, (int32) colors, out board); }
-        while (solved_on_right ());
+        if (!restored)
+        {
+            do { init_board (size, (int32) colors, out board); }
+            while (solved_on_right ());
+        }
 
         start_clock ();
     }
@@ -590,4 +594,202 @@ private class Puzzle : Object
 
         _switch_tiles (x0, y0, x1, y1, animation_duration, /* no log */ true, /* garbage */ 0);
     }
+
+    /*\
+    * * save and restore
+    \*/
+
+    internal Variant to_variant ()
+    {
+        VariantBuilder builder = new VariantBuilder (new VariantType ("m(yyda(yyyyyyyy)ua(yyyyu))"));
+        builder.open (new VariantType ("(yyda(yyyyyyyy)ua(yyyyu))"));
+
+        // board
+        builder.add ("y", size);
+        builder.add ("y", colors);
+        builder.add ("d", elapsed);
+
+        // tiles
+        builder.open (new VariantType ("a(yyyyyyyy)"));
+        for (uint8 x = 0; x < size * 2; x++)
+            for (uint8 y = 0; y < size; y++)
+            {
+                Tile? tile = board [x, y];
+                if (tile == null)
+                    continue;
+                builder.add ("(yyyyyyyy)",
+                             x, y,
+                             ((!) tile).north, ((!) tile).east, ((!) tile).south, ((!) tile).west,
+                             ((!) tile).x, ((!) tile).y);
+            }
+        builder.close ();
+
+        // history
+        builder.add ("u", history_length - last_move_index);
+        builder.open (new VariantType ("a(yyyyu)"));
+        unowned List<Inversion>? entry = reversed_history.last ();
+        while (entry != null)
+        {
+            builder.add ("(yyyyu)",
+                         ((!) entry).data.x0, ((!) entry).data.y0,
+                         ((!) entry).data.x1, ((!) entry).data.y1,
+                         ((!) entry).data.id);
+            entry = ((!) entry).prev;
+        }
+        builder.close ();
+
+        // end
+        builder.close ();
+        return builder.end ();
+    }
+
+    private struct SavedTile
+    {
+        public uint8 current_x;
+        public uint8 current_y;
+        public uint8 color_north;
+        public uint8 color_east;
+        public uint8 color_south;
+        public uint8 color_west;
+        public uint8 initial_x;
+        public uint8 initial_y;
+    }
+
+    internal static bool is_valid_saved_game (Variant maybe_variant)
+    {
+        Variant? variant = maybe_variant.get_maybe ();
+        if (variant == null)
+            return false;
+
+        uint8 board_size;
+        uint8 colors;
+        double elapsed;
+        ((!) variant).get_child (0, "y", out board_size);
+        ((!) variant).get_child (1, "y", out colors);
+        ((!) variant).get_child (2, "d", out elapsed);
+        Variant array_variant = ((!) variant).get_child_value (3);
+        if (array_variant.n_children () != board_size * board_size)
+            return false;
+        SavedTile [] saved_tiles = new SavedTile [board_size * board_size];
+
+        VariantIter? iter = new VariantIter (array_variant);
+        for (uint8 index = 0; index < board_size * board_size; index++)
+        {
+            variant = ((!) iter).next_value ();
+            if (variant == null)
+                assert_not_reached ();
+            saved_tiles [index] = SavedTile ();
+            ((!) variant).@get ("(yyyyyyyy)", out saved_tiles [index].current_x,
+                                              out saved_tiles [index].current_y,
+                                              out saved_tiles [index].color_north,
+                                              out saved_tiles [index].color_east,
+                                              out saved_tiles [index].color_south,
+                                              out saved_tiles [index].color_west,
+                                              out saved_tiles [index].initial_x,
+                                              out saved_tiles [index].initial_y);
+        }
+
+        // sanity check
+        if (board_size < 2 || board_size > 6)
+            return false;
+
+        if (colors < 2 || colors > 10)
+            return false;
+
+        foreach (unowned SavedTile tile in saved_tiles)
+        {
+            if (tile.initial_x >= board_size)       return false;
+            if (tile.initial_y >= board_size)       return false;
+            if (tile.current_x >= 2 * board_size)   return false;
+            if (tile.current_y >= board_size)       return false;
+            if (tile.color_north >= colors)         return false;
+            if (tile.color_east  >= colors)         return false;
+            if (tile.color_south >= colors)         return false;
+            if (tile.color_west  >= colors)         return false;
+        }
+
+        // check that puzzle is solvable and that tiles do not overlap
+        SavedTile? [,] initial_board = new SavedTile? [board_size, board_size];
+        for (uint8 x = 0; x < board_size; x++)
+            for (uint8 y = 0; y < board_size; y++)
+                initial_board [x, y] = null;
+
+        bool [,] current_board = new bool [board_size * 2, board_size];
+        for (uint8 x = 0; x < board_size * 2; x++)
+            for (uint8 y = 0; y < board_size; y++)
+                current_board [x, y] = false;
+
+        for (uint8 x = 0; x < board_size * board_size; x++)
+        {
+            unowned SavedTile tile = saved_tiles [x];
+            if (initial_board [tile.initial_x, tile.initial_y] != null)
+                return false;
+            if (current_board [tile.current_x, tile.current_y] == true)
+                return false;
+            initial_board [tile.initial_x, tile.initial_y] = tile;
+            current_board [tile.current_x, tile.current_y] = true;
+        }
+
+        for (uint8 x = 0; x < board_size; x++)
+            for (uint8 y = 0; y < board_size - 1; y++)
+            {
+                if (((!) initial_board [x, y]).color_south != ((!) initial_board [x, y + 1]).color_north)
+                    return false;
+                if (((!) initial_board [y, x]).color_east != ((!) initial_board [y + 1, x]).color_west)
+                    return false;
+            }
+
+        // TODO validate history 1/2
+
+        return true;
+    }
+
+    internal Puzzle.restore (Variant maybe_variant)
+    {
+        Variant? variant = maybe_variant.get_maybe ();
+        if (variant == null)
+            assert_not_reached ();
+
+        uint8 _size;
+        uint8 _colors;
+        double _elapsed;
+        ((!) variant).get_child (0, "y", out _size);
+        ((!) variant).get_child (1, "y", out _colors);
+        ((!) variant).get_child (2, "d", out _elapsed);
+        Object (size: _size, colors: _colors, restored: true, initial_time: _elapsed);
+
+        Variant array_variant = ((!) variant).get_child_value (3);
+        board = new Tile? [size * 2, size];
+        for (uint8 x = 0; x < size * 2; x++)
+            for (uint8 y = 0; y < size; y++)
+                board [x, y] = null;
+
+        VariantIter? iter = new VariantIter (array_variant);
+        for (uint8 index = 0; index < size * size; index++)
+        {
+            variant = ((!) iter).next_value ();
+            if (variant == null)
+                assert_not_reached ();
+            uint8 current_x, current_y, color_north, color_east, color_south, color_west, initial_x, 
initial_y;
+            ((!) variant).@get ("(yyyyyyyy)", out current_x,
+                                              out current_y,
+                                              out color_north,
+                                              out color_east,
+                                              out color_south,
+                                              out color_west,
+                                              out initial_x,
+                                              out initial_y);
+            Tile tile = new Tile (initial_x, initial_y);
+            tile.north = color_north;
+            tile.east  = color_east;
+            tile.south = color_south;
+            tile.west  = color_west;
+            board [current_x, current_y] = tile;
+        }
+        game_in_progress = true;
+        if (solved_on_right ())
+            is_solved_right = true;
+    }
+
+    // TODO restore history 2/2
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]