[swell-foop/arnaudb/history: 7/7] Add history.



commit 7019d3603829fe54d100760f1a07d7f943bdd1c2
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Mon May 18 16:41:04 2020 +0200

    Add history.

 data/ui/help-overlay.ui |  59 ++++++++---
 data/ui/swell-foop.ui   |  43 ++++++++
 src/game-view.vala      |   6 ++
 src/game.vala           | 268 +++++++++++++++++++++++++++++++++++++++++++-----
 src/swell-foop.vala     |  10 +-
 src/window.vala         |  27 ++++-
 6 files changed, 367 insertions(+), 46 deletions(-)
---
diff --git a/data/ui/help-overlay.ui b/data/ui/help-overlay.ui
index fff5f6f..a568925 100644
--- a/data/ui/help-overlay.ui
+++ b/data/ui/help-overlay.ui
@@ -23,18 +23,26 @@
     <child>
       <object class="GtkShortcutsSection">
         <property name="visible">True</property>
-        <property name="max-height">5</property>
+        <property name="max-height">7</property>
         <child>
           <object class="GtkShortcutsGroup">
             <property name="visible">True</property>
-            <!-- Translators: title of a section in the Keyboard Shortcuts dialog; contains (only) "Start a 
new game" -->
-            <property name="title" translatable="yes" context="shortcut window">Main functions</property>
+            <!-- Translators: title of a section in the Keyboard Shortcuts dialog; contains "Move keyboard 
highlight" and "Destroy selected block" -->
+            <property name="title" translatable="yes" context="shortcut window">Play with keyboard</property>
             <child>
               <object class="GtkShortcutsShortcut">
                 <property name="visible">True</property>
-                <property name="accelerator">&lt;Ctrl&gt;N</property>
-                <!-- Translators: Ctrl-N shortcut description in the Keyboard Shortcuts dialog, section Main 
Functions -->
-                <property name="title" translatable="yes" context="shortcut window">Start a new 
game</property>
+                <!-- Translators: Left/Right/Up/Down arrows actions description in the Keyboard Shortcuts 
dialog, section "Play with keyboard"; moves highlight -->
+                <property name="title" translatable="yes" context="shortcut window">Move keyboard 
highlight</property>
+                <property name="accelerator">Left Right Up Down</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">True</property>
+                <!-- Translators: Return/space actions description in the Keyboard Shortcuts dialog, section 
"Play with keyboard"; does as a mouse click -->
+                <property name="title" translatable="yes" context="shortcut window">Destroy selected 
block</property>
+                <property name="accelerator">Return space</property>
               </object>
             </child>
           </object>
@@ -42,22 +50,45 @@
         <child>
           <object class="GtkShortcutsGroup">
             <property name="visible">True</property>
-            <!-- Translators: title of a section in the Keyboard Shortcuts dialog; contains "Move keyboard 
highlight" and "Destroy selected block" -->
-            <property name="title" translatable="yes" context="shortcut window">Play with keyboard</property>
+            <!-- Translators: title of a section in the Keyboard Shortcuts dialog; contains "Undo" and 
"Redo" -->
+            <property name="title" translatable="yes" context="shortcut window">History</property>
             <child>
               <object class="GtkShortcutsShortcut">
                 <property name="visible">True</property>
-                <!-- Translators: Left/Right/Up/Down arrows actions description in the Keyboard Shortcuts 
dialog, section "Play with keyboard"; moves highlight -->
-                <property name="title" translatable="yes" context="shortcut window">Move keyboard 
highlight</property>
-                <property name="accelerator">Left Right Up Down</property>
+                <!-- Translators: Ctrl-Z shortcut description in the Keyboard Shortcuts dialog, section 
"History"; undoes a move -->
+                <property name="title" translatable="yes" context="shortcut window">Undo</property>
+                <property name="accelerator">&lt;Ctrl&gt;Z</property>
               </object>
             </child>
             <child>
               <object class="GtkShortcutsShortcut">
                 <property name="visible">True</property>
-                <!-- Translators: Return/space actions description in the Keyboard Shortcuts dialog, section 
"Play with keyboard"; does as a mouse click -->
-                <property name="title" translatable="yes" context="shortcut window">Destroy selected 
block</property>
-                <property name="accelerator">Return space</property>
+                <!-- Translators: Ctrl-Shift-Z shortcut description in the Keyboard Shortcuts dialog, 
section "History"; redoes an undone move -->
+                <property name="title" translatable="yes" context="shortcut window">Redo</property>
+                <property name="accelerator">&lt;Ctrl&gt;&lt;Shift&gt;Z</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">False</property>
+                <!-- Translators: future shortcut description in the Keyboard Shortcuts dialog, section 
"History"; resets the current game -->
+                <property name="title" translatable="yes" context="shortcut window">Restart</property>
+                <property name="accelerator">&lt;Ctrl&gt;&lt;Shift&gt;R</property> <!-- TODO implement -->
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">True</property>
+            <!-- Translators: title of a section in the Keyboard Shortcuts dialog; contains (only) "Start a 
new game" -->
+            <property name="title" translatable="yes" context="shortcut window">Main functions</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">True</property>
+                <property name="accelerator">&lt;Ctrl&gt;N</property>
+                <!-- Translators: Ctrl-N shortcut description in the Keyboard Shortcuts dialog, section Main 
Functions -->
+                <property name="title" translatable="yes" context="shortcut window">Start a new 
game</property>
               </object>
             </child>
           </object>
diff --git a/data/ui/swell-foop.ui b/data/ui/swell-foop.ui
index e8c1e81..11fcaab 100644
--- a/data/ui/swell-foop.ui
+++ b/data/ui/swell-foop.ui
@@ -139,6 +139,49 @@
         <property name="show-close-button">True</property>
         <!-- Translators: title of the window displayed on the headerbar; name of the application -->
         <property name="title" translatable="yes">Swell Foop</property>
+        <child>
+          <object class="GtkBox" id="undo_redo_box">
+            <property name="visible">True</property>
+            <property name="valign">center</property>
+            <style>
+              <class name="linked"/>
+            </style>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">True</property>
+                <property name="valign">center</property>
+                <!-- Translators: tooltip text of the Undo button; probably a verb -->
+                <property name="tooltip-text" translatable="yes">Undo</property>
+                <property name="action-name">win.undo</property>
+                <property name="focus-on-click">False</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">edit-undo-symbolic</property>
+                    <property name="visible">True</property>
+                    <property name="icon-size">1</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">True</property>
+                <property name="valign">center</property>
+                <!-- Translators: tooltip text of the Undo button; probably a verb -->
+                <property name="tooltip-text" translatable="yes">Redo</property>
+                <property name="action-name">win.redo</property>
+                <property name="focus-on-click">False</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">edit-redo-symbolic</property>
+                    <property name="visible">True</property>
+                    <property name="icon-size">1</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
         <child>
           <object class="GtkMenuButton" id="hamburger_button">
             <property name="visible">True</property>
diff --git a/src/game-view.vala b/src/game-view.vala
index d106db0..cafb0ce 100644
--- a/src/game-view.vala
+++ b/src/game-view.vala
@@ -122,6 +122,7 @@ private class GameGroup : Clutter.Group
                 SignalHandler.disconnect_matched (game, SignalMatchType.DATA, 0, 0, null, null, this);
             _game = value;
             game_is_set = true;
+            game.undone.connect (move_undone_cb);
             game.complete.connect (game_complete_cb);
             game.update_score.connect (update_score_cb);
 
@@ -428,6 +429,11 @@ private class GameGroup : Clutter.Group
         button_actor.z_position = -50;
         button_actor.set_opacity (255);
     }
+
+    private inline void move_undone_cb ()
+    {
+        game = game;
+    }
 }
 
 /**
diff --git a/src/game.vala b/src/game.vala
index dff4a11..15abfef 100644
--- a/src/game.vala
+++ b/src/game.vala
@@ -46,6 +46,9 @@ private class Tile : Object
     /* Do not use this mothod to initialize the position. */
     internal void update_position (uint8 new_x, uint8 new_y)
     {
+        if (closed)
+            return;
+
         uint8 old_x = grid_x;
         uint8 old_y = grid_y;
 
@@ -102,6 +105,25 @@ private class Game : Object
             create_new_game ();
     }
 
+//    private static string to_string (ref Tile [,] current_board)
+//    {
+//        uint8 rows    = (uint8) current_board.length [0];
+//        uint8 columns = (uint8) current_board.length [1];
+//        string board  = "\n";
+//        for (uint8 row = rows; row > 0; row--)
+//        {
+//            for (uint8 col = 0; col < columns; col++)
+//                if (current_board [row - 1, col] == null)
+//                    board += ". ";
+//                else if (((!) current_board [row - 1, col]).closed)
+//                    board += "0 ";
+//                else
+//                    board += ((!) current_board [row - 1, col]).color.to_string () + " ";
+//            board += "\n";
+//        }
+//        return (board);
+//    }
+
     private inline void create_new_game ()
     {
         initial_board = new uint8 [rows, columns];
@@ -224,7 +246,12 @@ private class Game : Object
 
     internal void remove_connected_tiles (Tile given_tile)
     {
-        _remove_connected_tiles (given_tile, ref current_board);
+        remove_connected_tiles_real (given_tile, /* skip history */ false);
+    }
+
+    private void remove_connected_tiles_real (Tile given_tile, bool skip_history)
+    {
+        _remove_connected_tiles (given_tile, ref current_board, skip_history);
 
         if (!is_started) {
             is_started = true;
@@ -238,19 +265,18 @@ private class Game : Object
             complete ();
         }
     }
-    private void _remove_connected_tiles (Tile given_tile, ref Tile? [,] current_board)
+    private void _remove_connected_tiles (Tile given_tile, ref Tile? [,] current_board, bool skip_history)
     {
         List<Tile> cl = _connected_tiles (given_tile, ref current_board);
 
         if (cl.length () < 2)
             return;
 
-        add_history_entry (given_tile.grid_x, given_tile.grid_y);
-
         foreach (unowned Tile tile in (!) cl)
             tile.closed = true;
 
         uint8 new_x = 0;
+        uint8 [] removed_columns = {};
 
         for (uint8 x = 0; x < columns; x++)
         {
@@ -289,15 +315,23 @@ private class Game : Object
                 if (tile == null)
                     break;
 
-                ((!) tile).update_position (new_x, y);
-
                 if (!((!) tile).closed)
+                {
+                    ((!) tile).update_position (new_x, y);
                     has_empty_col = false;
+                }
             }
 
             /* If the current column is empty, don't increment new_x. Otherwise increment */
             if (!has_empty_col)
                 new_x++;
+            else
+            {
+                int length = removed_columns.length;
+                removed_columns.resize (length + 1);
+                removed_columns.move (/* start */ 0, /* dest */ 1, /* length */ length);
+                removed_columns [0] = new_x;
+            }
         }
 
         /* The remaining columns are do-not-cares. Assign null to them */
@@ -306,6 +340,9 @@ private class Game : Object
                 current_board [y, new_x] = null;
 
         increment_score_from_tiles ((uint16) cl.length ());
+
+        if (!skip_history)
+            add_history_entry (given_tile.grid_x, given_tile.grid_y, given_tile.color, cl, (owned) 
removed_columns);
     }
 
     private static bool has_completed (ref Tile? [,] current_board)
@@ -330,20 +367,28 @@ private class Game : Object
         return true;
     }
 
-    private void increment_score_from_tiles (uint16 n_tiles)
+    private inline void decrement_score_from_tiles (uint16 n_tiles)
     {
-        uint points_awarded = 0;
+        increment_score (-1 * get_score_from_tiles (n_tiles));
+    }
 
-        if (n_tiles >= 3)
-            points_awarded = (uint) (n_tiles - 2) * (uint) (n_tiles - 2);
+    private inline void increment_score_from_tiles (uint16 n_tiles)
+    {
+        increment_score (get_score_from_tiles (n_tiles));
+    }
 
-        increment_score (points_awarded);
+    private inline int get_score_from_tiles (uint16 n_tiles)
+    {
+        return n_tiles < 3 ? 0 : (n_tiles - 2) * (n_tiles - 2);
     }
 
-    private void increment_score (uint increment)
+    private void increment_score (int variation)
     {
-        score += increment;
-        update_score (increment);
+        score += variation;
+        if (variation > 0)
+            update_score (variation);
+        else
+            update_score (0);
     }
 
     /*\
@@ -413,11 +458,24 @@ private class Game : Object
                 break;
             _remove_connected_tiles (current_board [rows - tmp_variant_1.get_child_value (1).get_byte () - 1,
                                                     tmp_variant_1.get_child_value (0).get_byte ()],
-                                     ref current_board);
+                                     ref current_board,
+                                     /* skip history */ false);
+        }
+
+        if (history_index > reversed_history.length ())
+        {
+            clear_history ();
+            return false;
         }
 
+        for (uint16 i = history_index; i != 0; i--)
+            undo_real (ref current_board);
+
         if (has_completed (ref current_board))
+        {
+            clear_history ();
             return false;
+        }
 
         this.current_board = current_board;
         this.initial_board = initial_board;
@@ -446,14 +504,16 @@ private class Game : Object
         builder.close ();
         builder.add ("q", history_index);
         builder.open (new VariantType ("a(yy)"));
-        history.@foreach ((data) => {
+        reversed_history.reverse ();
+        reversed_history.@foreach ((data) => {
                 if (data == null)
                     return;
                 builder.open (new VariantType ("(yy)"));
-                builder.add ("y", ((!) data).x);
-                builder.add ("y", rows - ((!) data).y - 1);
+                builder.add ("y", ((!) data).click.x);
+                builder.add ("y", rows - ((!) data).click.y - 1);
                 builder.close ();
             });
+        reversed_history.reverse ();    // get_saved_game might be called once or twice… so let’s put 
reversed_history back in its (inverted) order
         builder.close ();
         return new Variant.maybe (/* guess the type */ null, builder.end ());
     }
@@ -462,24 +522,178 @@ private class Game : Object
     * * history
     \*/
 
+    [CCode (notify = true)] internal bool can_undo { internal get; private set; default = false; }
+    [CCode (notify = true)] internal bool can_redo { internal get; private set; default = false; }
+    private uint16 history_length = 0;
     private uint16 history_index = 0;
 
-    private struct HistoryEntry
+    private List<HistoryEntry> reversed_history = new List<HistoryEntry> ();
+
+    internal signal void undone ();
+
+    private class Point : Object
+    {
+        public uint8 x { internal get; protected construct; }
+        public uint8 y { internal get; protected construct; }
+
+        internal Point (uint8 x, uint8 y)
+        {
+            Object (x: x, y: y);
+        }
+    }
+
+    private class HistoryEntry : Object
+    {
+        [CCode (notify = false)] public Point click { internal get; protected construct; }
+        [CCode (notify = false)] public uint8 color { internal get; protected construct; }
+
+        internal List<Point> removed_tiles = new List<Point> ();
+        internal uint8 [] removed_columns;
+
+        internal HistoryEntry (uint8 x, uint8 y, uint8 color, List<Tile> cl, owned uint8 [] removed_columns)
+        {
+            Object (click: new Point (x, y), color: color);
+            this.removed_columns = removed_columns;
+
+            // TODO init at construct
+            foreach (unowned Tile tile in cl)
+                removed_tiles.prepend (new Point (tile.grid_x, tile.grid_y));
+            removed_tiles.sort ((tile_1, tile_2) => {
+                    if (tile_1.x < tile_2.x)
+                        return -1;
+                    if (tile_1.x > tile_2.x)
+                        return 1;
+                    if (tile_1.y < tile_2.y)
+                        return -1;
+                    if (tile_1.y > tile_2.y)
+                        return 1;
+                    assert_not_reached ();
+                });
+        }
+    }
+
+    private inline void clear_history ()
     {
-        public uint8 x;
-        public uint8 y;
+        reversed_history = new List<HistoryEntry> ();
+        history_length = 0;
+        history_index = 0;
+        can_undo = false;
+        can_redo = false;
+    }
 
-        internal HistoryEntry (uint8 x, uint8 y)
+    private inline void add_history_entry (uint8 x, uint8 y, uint8 color, List<Tile> cl, owned uint8 [] 
removed_columns)
+    {
+        while (history_index > 0)
         {
-            this.x = x;
-            this.y = y;
+            unowned HistoryEntry? history_data = reversed_history.nth_data (0);
+            if (history_data == null) assert_not_reached ();
+
+            reversed_history.remove ((!) history_data);
+
+            history_index--;
+            history_length--;
         }
+
+        reversed_history.prepend (new HistoryEntry (x, y, color, cl, (owned) removed_columns));
+        history_length++;
+        can_undo = true;
+        can_redo = false;
     }
 
-    private List<HistoryEntry?> history = new List<HistoryEntry> ();
+    internal void undo ()
+    {
+        undo_real (ref current_board);
+    }
+
+    private void undo_real (ref Tile? [,] current_board)
+    {
+        if (!can_undo)
+            return;
+
+        unowned List<HistoryEntry>? history_item = reversed_history.nth (history_index);
+        if (history_item == null) assert_not_reached ();
+
+        unowned HistoryEntry? history_data = ((!) history_item).data;
+        if (history_data == null) assert_not_reached ();
+
+        undo_move ((!) history_data, ref current_board);
 
-    private inline void add_history_entry (uint8 x, uint8 y)
+        if (history_index == history_length)
+            can_undo = false;
+        can_redo = true;
+    }
+    private inline void undo_move (HistoryEntry history_entry, ref Tile? [,] current_board)
     {
-        history.append (HistoryEntry (x, y));
+        if (has_won (ref current_board))
+            increment_score (-1000);
+        decrement_score_from_tiles ((uint16) history_entry.removed_tiles.length ());
+
+        foreach (uint8 removed_column in history_entry.removed_columns)
+        {
+            for (uint8 j = columns - 1; j > removed_column; j--)
+            {
+                for (uint8 i = 0; i < rows; i++)
+                {
+                    if (current_board [i, j - 1] != null)
+                    {
+                        current_board [i, j] = (owned) current_board [i, j - 1];
+                        ((!) current_board [i, j]).update_position (j, i);
+                    }
+                    else
+                        current_board [i, j] = null;
+                }
+            }
+            for (uint8 i = 0; i < rows; i++)
+                current_board [i, removed_column] = null;
+        }
+
+        foreach (unowned Point removed_tile in history_entry.removed_tiles)
+        {
+            uint8 column = removed_tile.x;
+            for (uint8 row = rows - 1; row > removed_tile.y; row--)
+            {
+                if (current_board [row - 1, column] != null)
+                {
+                    current_board [row, column] = (owned) current_board [row - 1, column];
+                    ((!) current_board [row, column]).update_position (column, row);
+                }
+                else
+                    current_board [row, column] = null;
+                if (row == 0)
+                    break;
+            }
+            current_board [removed_tile.y, column] = new Tile (column, removed_tile.y, history_entry.color);
+        }
+
+        history_index++;
+
+        undone ();
+    }
+
+    internal void redo ()
+    {
+        if (!can_redo)
+            return;
+
+        unowned List<HistoryEntry>? history_item = reversed_history.nth (history_index - 1);
+        if (history_item == null) assert_not_reached ();
+
+        unowned HistoryEntry? history_data = ((!) history_item).data;
+        if (history_data == null) assert_not_reached ();
+
+        redo_move ((!) history_data);
+
+        if (history_index == 0)
+            can_redo = false;
+        can_undo = true;
+    }
+    private inline void redo_move (HistoryEntry history_entry)
+    {
+        history_index--;
+
+        // TODO save for real where the user clicked; warning, history_entry.click does not use the same 
coords system
+        remove_connected_tiles_real (current_board [history_entry.removed_tiles.first ().data.y,
+                                                    history_entry.removed_tiles.first ().data.x],
+                                     /* skip history */ true);
     }
 }
diff --git a/src/swell-foop.vala b/src/swell-foop.vala
index 927383b..1b330a8 100644
--- a/src/swell-foop.vala
+++ b/src/swell-foop.vala
@@ -49,10 +49,12 @@ public class SwellFoop : Gtk.Application
         Gtk.Settings.get_default ().@set ("gtk-application-prefer-dark-theme", true);
 
         add_action_entries (action_entries, this);
-        set_accels_for_action ("win.new-game",          { "<Primary>n"      });
-        set_accels_for_action ("app.help",              {          "F1"     });
-        set_accels_for_action ("win.toggle-hamburger",  {          "F10"    });
-        set_accels_for_action ("app.quit",              { "<Primary>q"      });
+        set_accels_for_action ("app.help",              {                 "F1"  });
+        set_accels_for_action ("win.toggle-hamburger",  {                 "F10" });
+        set_accels_for_action ("win.new-game",          {        "<Primary>n"   });
+        set_accels_for_action ("app.quit",              {        "<Primary>q"   });
+        set_accels_for_action ("win.undo",              {        "<Primary>z"   });
+        set_accels_for_action ("win.redo",              { "<Shift><Primary>z"   });
 
         /* Create the main window */
         window = new SwellFoopWindow (this);
diff --git a/src/window.vala b/src/window.vala
index 58ea963..09274ef 100644
--- a/src/window.vala
+++ b/src/window.vala
@@ -71,7 +71,10 @@ private class SwellFoopWindow : ApplicationWindow
         { "change-colors",      null,       "s", "'3'",                 change_colors_cb    },  // cannot be 
done via create_action because it’s an int
         { "new-game",           new_game_cb         },
         { "scores",             scores_cb           },
-        { "toggle-hamburger",   toggle_hamburger    }
+        { "toggle-hamburger",   toggle_hamburger    },
+
+        { "undo",               undo                },
+        { "redo",               redo                }
     };
 
     construct
@@ -173,6 +176,7 @@ private class SwellFoopWindow : ApplicationWindow
 
     private void complete_cb ()
     {
+        undo_action.set_enabled (false);
         Idle.add (() => { add_score (); return Source.REMOVE; });
         game_in_progress = false;
     }
@@ -198,6 +202,10 @@ private class SwellFoopWindow : ApplicationWindow
     * * various calls
     \*/
 
+    // for keeping in memory
+    private SimpleAction undo_action;
+    private SimpleAction redo_action;
+
     private void new_game (Variant? saved_game = null)
     {
         Size size = get_board_size ();
@@ -217,6 +225,13 @@ private class SwellFoopWindow : ApplicationWindow
         view.set_theme_name (settings.get_string ("theme"));
         view.set_is_zealous (settings.get_boolean ("zealous"));
         view.set_game ((!) game);
+
+        /* Update undo and redo actions states */
+        undo_action = (SimpleAction) lookup_action ("undo");
+        game.bind_property ("can-undo", undo_action, "enabled", BindingFlags.SYNC_CREATE);
+
+        redo_action = (SimpleAction) lookup_action ("redo");
+        game.bind_property ("can-redo", redo_action, "enabled", BindingFlags.SYNC_CREATE);
     }
 
     protected override void destroy ()
@@ -299,6 +314,16 @@ private class SwellFoopWindow : ApplicationWindow
         hamburger_button.active = !hamburger_button.active;
     }
 
+    private inline void undo (/* SimpleAction action, Variant? variant */)
+    {
+        game.undo ();
+    }
+
+    private inline void redo (/* SimpleAction action, Variant? variant */)
+    {
+        game.redo ();
+    }
+
     /*\
     * * keyboard
     \*/


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