[swell-foop/arnaudb/history: 7/7] Add history.
- From: Arnaud B. <arnaudb src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [swell-foop/arnaudb/history: 7/7] Add history.
- Date: Fri, 22 May 2020 22:10:23 +0000 (UTC)
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"><Ctrl>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"><Ctrl>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"><Ctrl><Shift>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"><Ctrl><Shift>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"><Ctrl>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]