[gnome-tetravex] Add CLI.



commit 652b50f6b07fabc9fdfbafb6728516939c713ed1
Author: Arnaud Bonatti <arnaud bonatti gmail com>
Date:   Wed Oct 30 02:55:57 2019 +0100

    Add CLI.

 data/gnome-tetravex.6   |   3 +
 src/cli.vala            | 313 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/gnome-tetravex.vala | 130 +++++++++++++++-----
 src/history.vala        |  53 ++++++++
 src/meson.build         |   1 +
 src/puzzle.vala         | 145 ++++++++++++++++------
 src/score-overlay.vala  |  41 +++++++
 7 files changed, 620 insertions(+), 66 deletions(-)
---
diff --git a/data/gnome-tetravex.6 b/data/gnome-tetravex.6
index dd994b1..b2c613e 100644
--- a/data/gnome-tetravex.6
+++ b/data/gnome-tetravex.6
@@ -27,6 +27,9 @@ that the same numbers are touching each other. Your game is timed,
 these times are stored in a system-wide scoreboard.
 .SH OPTIONS
 .TP
+.B \-\-cli=COMMAND
+Play in the terminal (see “--cli=help”)
+.TP
 .B \-c, \-\-colors=NUMBER
 Set number of colors (2-10)
 .TP
diff --git a/src/cli.vala b/src/cli.vala
new file mode 100644
index 0000000..a8dc928
--- /dev/null
+++ b/src/cli.vala
@@ -0,0 +1,313 @@
+/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+
+   This file is part of GNOME Tetravex.
+
+   Copyright (C) 2019 Arnaud Bonatti
+
+   GNOME Tetravex is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 2 of the License, or
+   (at your option) any later version.
+
+   GNOME Tetravex is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License along
+   with this GNOME Tetravex.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+private const string KEY_GRID_SIZE = "grid-size";
+
+namespace CLI
+{
+    private static int play_cli (string cli, string schema_name, out GLib.Settings settings, out Variant 
saved_game, out bool can_restore, out Puzzle puzzle, ref int colors, ref int game_size)
+    {
+        settings = new GLib.Settings (schema_name);
+
+        saved_game = settings.get_value ("saved-game");
+        can_restore = Puzzle.is_valid_saved_game (saved_game, /* restore finished game */ true);
+
+        uint8 size;
+        bool new_puzzle;
+        if (game_size != int.MIN)
+        {
+            settings.set_int (KEY_GRID_SIZE, game_size);
+            size = (uint8) game_size;
+            puzzle = new Puzzle (size, (uint8) colors);
+            new_puzzle = true;
+        }
+        else if (colors != 10 || (!) cli == "new")
+        {
+            size = (uint8) settings.get_int (KEY_GRID_SIZE);
+            puzzle = new Puzzle (size, (uint8) colors);
+            new_puzzle = true;
+        }
+        else if (can_restore)
+        {
+            size = (uint8) settings.get_int (KEY_GRID_SIZE);
+            puzzle = new Puzzle.restore ((!) saved_game);
+            new_puzzle = false;
+        }
+        else
+        {
+            size = (uint8) settings.get_int (KEY_GRID_SIZE);
+            puzzle = new Puzzle (size, 10);
+            new_puzzle = true;
+        }
+
+        switch ((!) cli)    // TODO add translated commands? add translations?
+        {
+            case "help":
+            case "HELP":
+                assert_not_reached ();  // should be handled by the caller
+
+            case "":
+            case "show":
+            case "status":
+                if (new_puzzle)
+                    break;
+
+                print_board (puzzle, size);
+                return Posix.EXIT_SUCCESS;
+
+            case "new": // creation already handled, need saving
+                break;
+
+            case "up":
+            case "l-up":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ true, Direction.UP, "Cannot move 
up left-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+            case "down":
+            case "l-down":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ true, Direction.DOWN, "Cannot move 
down left-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+            case "left":
+            case "l-left":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ true, Direction.LEFT, "Cannot move 
left left-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+            case "right":
+            case "l-right":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ true, Direction.RIGHT, "Cannot 
move right left-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+
+            case "r-up":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ false, Direction.UP, "Cannot move 
up right-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+            case "r-down":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ false, Direction.DOWN, "Cannot 
move down right-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+            case "r-left":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ false, Direction.LEFT, "Cannot 
move left right-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+            case "r-right":
+                if (!cli_move_tiles (puzzle, new_puzzle, /* left board */ false, Direction.RIGHT, "Cannot 
move right right-board tiles."))
+                    return Posix.EXIT_FAILURE;
+                break;
+
+            case "end":
+            case "finish":
+                if (new_puzzle || puzzle.is_solved)
+                {
+                    puzzle_is_solved_message (/* alternative message */ true);
+                    return Posix.EXIT_FAILURE;
+                }
+
+                if (puzzle.is_solved_right)
+                    puzzle.finish (/* duration */ 0);
+                else if (!puzzle.move_last_tile_if_possible ())
+                {
+                    warning ("Cannot finish automatically. If you want to give up and view the solution, use 
“solve”." + "\n");
+                    return Posix.EXIT_FAILURE;
+                }
+                break;
+
+            case "solve":
+                if (new_puzzle || puzzle.is_solved)
+                {
+                    puzzle_is_solved_message (/* alternative message */ true);
+                    return Posix.EXIT_FAILURE;
+                }
+
+                puzzle.solve ();
+                break;
+
+            default:
+                if (new_puzzle || puzzle.is_solved)
+                {
+                    puzzle_is_solved_message ();
+                    return Posix.EXIT_FAILURE;
+                }
+
+                uint8 tile_1_x;
+                uint8 tile_1_y;
+                uint8 tile_2_x;
+                uint8 tile_2_y;
+                if (!parse_cli ((!) cli, size, out tile_1_x, out tile_1_y, out tile_2_x, out tile_2_y))
+                {
+                    warning ("Cannot parse “--cli” command, aborting." + "\n");
+                    return Posix.EXIT_FAILURE;
+                }
+                if (puzzle.get_tile (tile_1_x, tile_1_y) == null
+                 && puzzle.get_tile (tile_2_x, tile_2_y) == null)
+                {
+                    warning ("Both given tiles are empty, aborting." + "\n");
+                    return Posix.EXIT_FAILURE;
+                }
+                if (!puzzle.can_switch (tile_1_x, tile_1_y, tile_2_x, tile_2_y))
+                {
+                    warning ("Cannot swap the given tiles, aborting." + "\n");
+                    print_board (puzzle, size);
+                    return Posix.EXIT_FAILURE;
+                }
+                puzzle.switch_tiles (tile_1_x, tile_1_y, tile_2_x, tile_2_y);
+                break;
+        }
+
+        print_board (puzzle, size);
+
+        settings.set_value ("saved-game", puzzle.to_variant (/* save time */ false));
+
+        return Posix.EXIT_SUCCESS;
+    }
+
+    private static bool cli_move_tiles (Puzzle puzzle, bool new_puzzle, bool left_board, Direction 
direction, string warning_string)
+    {
+        if (new_puzzle || puzzle.is_solved)
+        {
+            puzzle_is_solved_message ();
+            return false;
+        }
+        bool success;
+        switch (direction)
+        {
+            case Direction.UP:      success = puzzle.move_up (left_board);      break;
+            case Direction.DOWN:    success = puzzle.move_down (left_board);    break;
+            case Direction.LEFT:    success = puzzle.move_left (left_board);    break;
+            case Direction.RIGHT:   success = puzzle.move_right (left_board);   break;
+            default: assert_not_reached ();
+        }
+        if (!success)
+        {
+            warning (@"$warning_string\n");
+            return false;
+        }
+        return true;
+    }
+
+    private static void puzzle_is_solved_message (bool alternative_message = false)
+    {
+        if (alternative_message)
+            warning ("Puzzle is already solved! If you want to start a new one, use “new”." + "\n");
+        else
+            warning ("Puzzle is solved! If you want to start a new one, use “new”." + "\n");
+    }
+
+    private static void print_board (Puzzle puzzle, uint8 size)
+    {
+        stdout.printf ("\n");
+        for (uint8 y = 0; y < size; y++)
+        {
+            for (uint8 x = 0; x < 2 * size; x++)
+            {
+                Tile? tile = puzzle.get_tile (x, y);
+                if (tile == null)
+                    stdout.printf (" ┌─ ─┐");
+                else
+                    stdout.printf (@" ┌─$(((!) tile).north)─┐");
+                if (x == size - 1)
+                    stdout.printf ("  ");
+            }
+            stdout.printf ("\n");
+            for (uint8 x = 0; x < 2 * size; x++)
+            {
+                Tile? tile = puzzle.get_tile (x, y);
+                if (tile == null)
+                    stdout.printf ("      ");
+                else
+                    stdout.printf (@" $(((!) tile).west) · $(((!) tile).east)");
+                if (x == size - 1)
+                    stdout.printf ("  ");
+            }
+            stdout.printf ("\n");
+            for (uint8 x = 0; x < 2 * size; x++)
+            {
+                Tile? tile = puzzle.get_tile (x, y);
+                if (tile == null)
+                    stdout.printf (" └─ ─┘");
+                else
+                    stdout.printf (@" └─$(((!) tile).south)─┘");
+                if (x == size - 1)
+                    stdout.printf ("  ");
+            }
+            stdout.printf ("\n");
+        }
+        stdout.printf ("\n");
+        if (puzzle.is_solved)
+            stdout.printf ("Puzzle is solved!\n\n");
+    }
+
+    private static bool parse_cli (string cli, uint8 size,
+                               out uint8 tile_1_x, out uint8 tile_1_y,
+                               out uint8 tile_2_x, out uint8 tile_2_y)
+    {
+        tile_1_x = uint8.MAX;    //g
+        tile_1_y = uint8.MAX;   //  ar
+        tile_2_x = uint8.MAX;  //     ba
+        tile_2_y = uint8.MAX; //        ge
+
+        if (cli.length != 4)
+            return false;
+
+        char column_char = cli [0];
+        if (!is_valid_column (column_char, size, out tile_1_x))
+            return false;
+
+        column_char = cli [2];
+        if (!is_valid_column (column_char, size, out tile_2_x))
+            return false;
+
+        uint64 test;
+        if (!uint64.try_parse (cli [1].to_string (), out test))
+            return false;
+        if (test <= 0 || test > size)
+            return false;
+        tile_1_y = (uint8) test - 1;
+
+        if (!uint64.try_parse (cli [3].to_string (), out test))
+            return false;
+        if (test <= 0 || test > size)
+            return false;
+        tile_2_y = (uint8) test - 1;
+
+        return true;
+    }
+
+    private static bool is_valid_column (char column_char, uint8 size, out uint8 column)
+    {
+        switch (column_char)
+        {
+            case 'a': column = 0;           return true;
+            case 'b': column = 1;           return true;
+            case 'c': column = 2;           return size >= 3;
+            case 'd': column = 3;           return size >= 4;
+            case 'e': column = 4;           return size >= 5;
+            case 'f': column = 5;           return size == 6;
+            case 'A': column = size;        return true;
+            case 'B': column = size + 1;    return true;
+            case 'C': column = size + 2;    return size >= 3;
+            case 'D': column = size + 3;    return size >= 4;
+            case 'E': column = size + 4;    return size >= 5;
+            case 'F': column = size + 5;    return size == 6;
+            default : column = uint8.MAX;   return false;
+        }
+    }
+}
diff --git a/src/gnome-tetravex.vala b/src/gnome-tetravex.vala
index 054a1b2..104d078 100644
--- a/src/gnome-tetravex.vala
+++ b/src/gnome-tetravex.vala
@@ -26,18 +26,18 @@ private class Tetravex : Gtk.Application
     /* Translators: name of the program, as seen in the headerbar, in GNOME Shell, or in the about dialog */
     private const string PROGRAM_NAME = _("Tetravex");
 
-    private const string KEY_GRID_SIZE = "grid-size";
-
     private static bool start_paused = false;
     private static bool restore_on_start = false;
     private static int game_size = int.MIN;
     private static int colors = 10;
+    private static string? cli = null;
 
     private GLib.Settings settings;
 
     private Puzzle puzzle;
     private bool puzzle_init_done = false;
     private Label clock_label;
+    private Box clock_box;
     private History history;
 
     private PuzzleView view;
@@ -67,6 +67,12 @@ private class Tetravex : Gtk.Application
     private static string? [] remaining = new string? [1];
     private const OptionEntry [] option_entries =
     {
+        /* Translators: command-line option description, see 'gnome-tetravex --help' */
+        { "cli", 0,       OptionFlags.OPTIONAL_ARG, OptionArg.CALLBACK, (void*) _cli,   N_("Play in the 
terminal (see “--cli=help”)"),
+
+        /* Translators: in the command-line options description, text to indicate the user should give a 
command after '--cli' for playing in the terminal, see 'gnome-tetravex --help' */
+                                                                                        N_("COMMAND") },
+
         /* Translators: command-line option description, see 'gnome-tetravex --help' */
         { "colors",  'c', OptionFlags.NONE, OptionArg.INT,  ref colors,                 N_("Set number of 
colors (2-10)"),
 
@@ -92,6 +98,12 @@ private class Tetravex : Gtk.Application
         {}
     };
 
+    private bool _cli (string? option_name, string? val)
+    {
+        cli = option_name == null ? "" : (!) option_name;  // TODO report bug: should probably be val...
+        return true;
+    }
+
     private const GLib.ActionEntry[] action_entries =
     {
         { "new-game",       new_game_cb     },
@@ -165,11 +177,43 @@ private class Tetravex : Gtk.Application
 
         if (remaining [0] != null)
         {
-            /* Translators: command-line error message, displayed for an invalid CLI command; see 
'gnome-tetravex cli' */
+            /* Translators: command-line error message, displayed for an invalid CLI command; see 
'gnome-tetravex --cli new A1b2' */
             stderr.printf (N_("Failed to parse command-line arguments.\n"));
             return Posix.EXIT_FAILURE;
         }
 
+        if (cli != null)
+        {
+            if ((!) cli == "help" || (!) cli == "HELP")
+            {
+                stdout.printf ("\n" + "To play GNOME Tetravex in command-line:");
+                stdout.printf ("\n  --cli A1b2    " + "Invert two tiles, the one in A1, and the one in b2.");
+                stdout.printf ("\n                " + "An uppercase targets a tile from the initial board.");
+                stdout.printf ("\n                " + "A lowercase targets a tile in the left/final board.");
+                stdout.printf ("\n                " + "Digits specify the rows of the two tiles to invert.");
+                stdout.printf ("\n");
+                stdout.printf ("\n  --cli         " + "Show the current puzzle. Alias: “status” or “show”.");
+                stdout.printf ("\n  --cli new     " + "Create a new puzzle; for changing size, use --size.");
+                stdout.printf ("\n  --cli solve   " + "Give up with current puzzle, and view the solution.");
+                stdout.printf ("\n");
+                stdout.printf ("\n  --cli finish  " + "Finish current puzzle, automatically. Alias: “end”.");
+                stdout.printf ("\n                " + "Works for puzzles solved right or if one tile left.");
+                stdout.printf ("\n");
+                stdout.printf ("\n  --cli up      " + "Move all left-board tiles up by one.");
+                stdout.printf ("\n  --cli down    " + "Move all left-board tiles down by one.");
+                stdout.printf ("\n  --cli left    " + "Move all left-board tiles left by one.");
+                stdout.printf ("\n  --cli right   " + "Move all left-board tiles right by one.");
+                stdout.printf ("\n");
+                stdout.printf ("\n  --cli r-up    " + "Move all right-board tiles up by one.");
+                stdout.printf ("\n  --cli r-down  " + "Move all right-board tiles down by one.");
+                stdout.printf ("\n  --cli r-left  " + "Move all right-board tiles left by one.");
+                stdout.printf ("\n  --cli r-right " + "Move all right-board tiles right by one.");
+                stdout.printf ("\n\n");
+                return Posix.EXIT_SUCCESS;
+            }
+            return CLI.play_cli ((!) cli, "org.gnome.Tetravex", out settings, out saved_game, out 
can_restore, out puzzle, ref colors, ref game_size);
+        }
+
         /* Activate */
         return -1;
     }
@@ -184,7 +228,7 @@ 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);
+        can_restore = Puzzle.is_valid_saved_game (saved_game, /* restore finished game */ false);
 
         add_action_entries (action_entries, this);
         add_action (settings.create_action ("theme"));
@@ -373,19 +417,18 @@ private class Tetravex : Gtk.Application
         new_game_solve_stack.show ();
         grid.attach (new_game_solve_stack, 2, 1, 1, 1);
 
-        Box box = new Box (Orientation.HORIZONTAL, /* spacing */ 8);
+        clock_box = new Box (Orientation.HORIZONTAL, /* spacing */ 8);
         Image image = new Image.from_icon_name ("preferences-system-time-symbolic", IconSize.MENU);
         image.show ();
-        box.add (image);
+        clock_box.add (image);
         clock_label = new Label ("");
         clock_label.show ();
-        box.add (clock_label);
-        box.halign = Align.CENTER;
-        box.valign = Align.BASELINE;
-        box.set_margin_top (20);
-        box.set_margin_bottom (20);
-        box.show ();
-        grid.attach (box, 1, 1, 1, 1);
+        clock_box.add (clock_label);
+        clock_box.halign = Align.CENTER;
+        clock_box.valign = Align.BASELINE;
+        clock_box.set_margin_top (20);
+        clock_box.set_margin_bottom (20);
+        grid.attach (clock_box, 1, 1, 1, 1);
 
         undo_action   = (SimpleAction) lookup_action ("undo");
         redo_action   = (SimpleAction) lookup_action ("redo");
@@ -465,8 +508,8 @@ private class Tetravex : Gtk.Application
         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 ());
+        if (puzzle.game_in_progress)
+            settings.set_value ("saved-game", puzzle.to_variant (/* save time */ 
!puzzle.tainted_by_command_line));
         else if (!can_restore)
             settings.@set ("saved-game", "m(yyda(yyyyyyyy)ua(yyyyu))", null);
         settings.apply ();
@@ -520,14 +563,19 @@ private class Tetravex : Gtk.Application
         else
             was_paused = false;
 
-        int size = settings.get_int (KEY_GRID_SIZE);
         if (saved_game == null)
+        {
+            int size = settings.get_int (KEY_GRID_SIZE);
             puzzle = new Puzzle ((uint8) size, (uint8) colors);
+            clock_box.show ();
+        }
         else
         {
             puzzle = new Puzzle.restore ((!) saved_game);
             if (puzzle.is_solved_right)
                 solved_right_cb ();
+            if (puzzle.tainted_by_command_line)
+                clock_box.hide ();
         }
         puzzle_init_done = true;
         puzzle.tick.connect (tick_cb);
@@ -561,6 +609,9 @@ private class Tetravex : Gtk.Application
 
     private void tick_cb ()
     {
+        if (puzzle_init_done && puzzle.tainted_by_command_line)
+            return;
+
         int elapsed = 0;
         if (puzzle_init_done)
             elapsed = (int) puzzle.elapsed; // felt better when + 0.5, but as the clock is still displayed 
while the score-overlay displays the exact time, that is regularly feeling odd
@@ -603,21 +654,40 @@ private class Tetravex : Gtk.Application
 
     private void show_end_game_cb (Puzzle puzzle)
     {
-        DateTime date = new DateTime.now_local ();
-        last_history_entry = new HistoryEntry (date, puzzle.size, puzzle.elapsed, /* old history format */ 
false);
+        if (puzzle.tainted_by_command_line)
+        {
+            if (!puzzle_is_finished) // Ctrl-n has been hit before the animation finished
+                return;
 
-        if (!puzzle_is_finished) // Ctrl-n has been hit before the animation finished
-            return;
+            HistoryEntry? best_score;
+            HistoryEntry? second_score;
+            HistoryEntry? third_score;
+            HistoryEntry? worst_score;
+            history.get_fallback_scores (puzzle.size,
+                                     out best_score,
+                                     out second_score,
+                                     out third_score,
+                                     out worst_score);
+            score_overlay.display_fallback_scores (puzzle.size, best_score, second_score, third_score, 
worst_score);
+        }
+        else
+        {
+            DateTime date = new DateTime.now_local ();
+            last_history_entry = new HistoryEntry (date, puzzle.size, puzzle.elapsed, /* old history format 
*/ false);
 
-        HistoryEntry? other_score_0;
-        HistoryEntry? other_score_1;
-        HistoryEntry? other_score_2;
-        uint position = history.get_place ((!) last_history_entry,
-                                           puzzle.size,
-                                       out other_score_0,
-                                       out other_score_1,
-                                       out other_score_2);
-        score_overlay.set_score (puzzle.size, position, (!) last_history_entry, other_score_0, 
other_score_1, other_score_2);
+            if (!puzzle_is_finished) // Ctrl-n has been hit before the animation finished
+                return;
+
+            HistoryEntry? other_score_0;
+            HistoryEntry? other_score_1;
+            HistoryEntry? other_score_2;
+            uint position = history.get_place ((!) last_history_entry,
+                                               puzzle.size,
+                                           out other_score_0,
+                                           out other_score_1,
+                                           out other_score_2);
+            score_overlay.set_score (puzzle.size, position, (!) last_history_entry, other_score_0, 
other_score_1, other_score_2);
+        }
 
         new_game_solve_stack.set_visible_child_name ("new-game");
         view.hide_right_sockets ();
@@ -696,7 +766,7 @@ private class Tetravex : Gtk.Application
     private bool has_been_solved = false;
     private void solve_cb ()
     {
-        if (puzzle.elapsed < 0.2)   // security against multi-click on new-game button
+        if (!puzzle.tainted_by_command_line && puzzle.elapsed < 0.2)   // security against multi-click on 
new-game button
             return;
 
         if (puzzle.game_in_progress)
diff --git a/src/history.vala b/src/history.vala
index ca95b97..80a9de5 100644
--- a/src/history.vala
+++ b/src/history.vala
@@ -121,6 +121,59 @@ private class History : Object
         return first_entry == null;
     }
 
+    internal void get_fallback_scores (uint8          puzzle_size,
+                                   out HistoryEntry?  fallback_score_0,
+                                   out HistoryEntry?  fallback_score_1,
+                                   out HistoryEntry?  fallback_score_2,
+                                   out HistoryEntry?  fallback_score_3)
+    {
+        unowned List<HistoryEntry>? tmp_item = entries.first ();
+
+        while (tmp_item != null && ((!) tmp_item).data.size < puzzle_size)
+        { tmp_item = ((!) tmp_item).next; }
+
+        if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+        {
+            fallback_score_0 = null;
+            fallback_score_1 = null;
+            fallback_score_2 = null;
+            fallback_score_3 = null;
+            return;
+        }
+        fallback_score_0 = ((!) tmp_item).data;
+
+        tmp_item = ((!) tmp_item).next;
+        if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+        {
+            fallback_score_1 = null;
+            fallback_score_2 = null;
+            fallback_score_3 = null;
+            return;
+        }
+        fallback_score_1 = ((!) tmp_item).data;
+
+        tmp_item = ((!) tmp_item).next;
+        if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+        {
+            fallback_score_2 = null;
+            fallback_score_3 = null;
+            return;
+        }
+        fallback_score_2 = ((!) tmp_item).data;
+
+        tmp_item = ((!) tmp_item).next;
+        if (tmp_item == null || ((!) tmp_item).data.size != puzzle_size)
+        {
+            fallback_score_3 = null;
+            return;
+        }
+
+        unowned List<HistoryEntry> tmp_item_prev = (!) tmp_item;    // garbage
+        do { tmp_item_prev = (!) tmp_item; tmp_item = ((!) tmp_item).next; }
+        while (tmp_item != null && ((!) tmp_item).data.size == puzzle_size);
+        fallback_score_3 = tmp_item_prev.data;
+    }
+
     /*\
     * * loading
     \*/
diff --git a/src/meson.build b/src/meson.build
index 55c2674..390af54 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -3,6 +3,7 @@ resources = gnome.compile_resources ('resources', 'gnome-tetravex.gresource.xml'
                                      c_name: 'resources')
 
 sources = files (
+    'cli.vala',
     'config.vapi',
     'gnome-tetravex.vala',
     'history.vala',
diff --git a/src/puzzle.vala b/src/puzzle.vala
index 104664d..1666381 100644
--- a/src/puzzle.vala
+++ b/src/puzzle.vala
@@ -44,14 +44,17 @@ private class Puzzle : Object
     private Tile? [,] board;
 
     /* Game timer */
-    private Timer? clock;
+    private Timer? clock = null;    // TODO ask for Timer.do_not_start() constructor
     private uint clock_timeout;
-    public double initial_time { private get; protected construct; default = 0.0; }
+    [CCode (notify = false)] public double initial_time { private get; protected construct; default = 0.0; }
+    [CCode (notify = false)] public bool tainted_by_command_line { internal get; protected construct; }
 
     [CCode (notify = false)] internal double elapsed
     {
         get
         {
+            if (tainted_by_command_line)
+                assert_not_reached ();
             if (clock == null)
                 return 0.0;
             return initial_time + ((!) clock).elapsed ();
@@ -100,10 +103,10 @@ private class Puzzle : Object
         return true;
     }
 
-    public bool restored { private get; protected construct; default = false; }
+    [CCode (notify = false)] public bool restored { private get; protected construct; default = false; }
     internal Puzzle (uint8 size, uint8 colors)
     {
-        Object (size: size, colors: colors);
+        Object (size: size, colors: colors, tainted_by_command_line: false);
     }
 
     construct
@@ -326,16 +329,17 @@ private class Puzzle : Object
         _switch_tiles (x0, y0, x1, y1, /* delay if finished */ 0, /* undoing or redoing */ false, 
last_move_id);
     }
 
-    internal void move_up (bool left_board)
+    internal bool move_up (bool left_board)
     {
         if (!can_move_up (left_board) || last_move_id == uint.MAX)
-            return;
+            return false;
         last_move_id++;
 
         uint8 base_x = left_board ? 0 : size;
         for (uint8 y = 1; y < size; y++)
             for (uint8 x = 0; x < size; x++)
                 switch_one_of_many_tiles (base_x + x, y, base_x + x, y - 1);
+        return true;
     }
     private bool can_move_up (bool left_board)
     {
@@ -343,19 +347,26 @@ private class Puzzle : Object
         for (uint8 x = 0; x < size; x++)
             if (board [base_x + x, 0] != null)
                 return false;
-        return true;
+
+        for (uint8 x = 0; x < size; x++)
+            for (uint8 y = 1; y < size; y++)
+                if (board [base_x + x, y] != null)
+                    return true;
+
+        return false;
     }
 
-    internal void move_down (bool left_board)
+    internal bool move_down (bool left_board)
     {
         if (!can_move_down (left_board) || last_move_id == uint.MAX)
-            return;
+            return false;
         last_move_id++;
 
         uint8 base_x = left_board ? 0 : size;
         for (uint8 y = size - 1; y > 0; y--)
             for (uint8 x = 0; x < size; x++)
                 switch_one_of_many_tiles (base_x + x, y - 1, base_x + x, y);
+        return true;
     }
     private bool can_move_down (bool left_board)
     {
@@ -363,19 +374,26 @@ private class Puzzle : Object
         for (uint8 x = 0; x < size; x++)
             if (board [base_x + x, size - 1] != null)
                 return false;
-        return true;
+
+        for (uint8 x = 0; x < size; x++)
+            for (uint8 y = 0; y < size - 1; y++)
+                if (board [base_x + x, y] != null)
+                    return true;
+
+        return false;
     }
 
-    internal void move_left (bool left_board)
+    internal bool move_left (bool left_board)
     {
         if (!can_move_left (left_board) || last_move_id == uint.MAX)
-            return;
+            return false;
         last_move_id++;
 
         uint8 base_x = left_board ? 0 : size;
         for (uint8 x = 1; x < size; x++)
             for (uint8 y = 0; y < size; y++)
                 switch_one_of_many_tiles (base_x + x, y, base_x + x - 1, y);
+        return true;
     }
     private bool can_move_left (bool left_board)
     {
@@ -383,27 +401,41 @@ private class Puzzle : Object
         for (uint8 y = 0; y < size; y++)
             if (board [left_column, y] != null)
                 return false;
-        return true;
+
+        for (uint8 x = 1; x < size; x++)
+            for (uint8 y = 0; y < size; y++)
+                if (board [left_column + x, y] != null)
+                    return true;
+
+        return false;
     }
 
-    internal void move_right (bool left_board)
+    internal bool move_right (bool left_board)
     {
         if (!can_move_right (left_board) || last_move_id == uint.MAX)
-            return;
+            return false;
         last_move_id++;
 
         uint8 base_x = left_board ? 0 : size;
         for (uint8 x = size - 1; x > 0; x--)
             for (uint8 y = 0; y < size; y++)
                 switch_one_of_many_tiles (base_x + x - 1, y, base_x + x, y);
+        return true;
     }
     private bool can_move_right (bool left_board)
     {
-        uint8 right_column = left_board ? size - 1 : 2 * size - 1;
+        uint8 left_column = left_board ? 0 : size;
+        uint8 right_column = left_column + size - 1;
         for (uint8 y = 0; y < size; y++)
             if (board [right_column, y] != null)
                 return false;
-        return true;
+
+        for (uint8 x = 0; x < size - 1; x++)
+            for (uint8 y = 0; y < size; y++)
+                if (board [left_column + x, y] != null)
+                    return true;
+
+        return false;
     }
 
     internal void try_move (uint8 x, uint8 y)
@@ -459,15 +491,6 @@ private class Puzzle : Object
         return true;
     }
 
-    private enum Direction
-    {
-        NONE,
-        UP,
-        DOWN,
-        LEFT,
-        RIGHT;
-    }
-
     /*\
     * * actions
     \*/
@@ -504,6 +527,28 @@ private class Puzzle : Object
                 switch_tiles (x + size, y, x, y, duration);
     }
 
+    internal bool move_last_tile_if_possible ()
+    {
+        uint8 empty_x;
+        uint8 empty_y;
+        if (!only_one_remaining_tile (out empty_x, out empty_y))
+            return false;
+
+        for (uint8 x = size; x < 2 * size; x++)
+            for (uint8 y = 0; y < size; y++)
+                if (get_tile (x, y) != null)
+                {
+                    if (can_switch (x, y, empty_x, empty_y))
+                    {
+                        switch_tiles (x, y, empty_x, empty_y);
+                        return true;
+                    }
+                    else
+                        return false;
+                }
+        assert_not_reached ();
+    }
+
     internal bool only_one_remaining_tile (out uint8 empty_x, out uint8 empty_y)
     {
         bool empty_found = false;
@@ -531,6 +576,8 @@ private class Puzzle : Object
 
     private void start_clock ()
     {
+        if (tainted_by_command_line)
+            return;
         if (clock == null)
             clock = new Timer ();
         timeout_cb ();
@@ -538,6 +585,8 @@ private class Puzzle : Object
 
     private void stop_clock ()
     {
+        if (tainted_by_command_line)
+            return;
         if (clock == null)
             return;
         if (clock_timeout != 0)
@@ -549,6 +598,8 @@ private class Puzzle : Object
 
     private void continue_clock ()
     {
+        if (tainted_by_command_line)
+            return;
         if (clock == null)
             clock = new Timer ();
         else
@@ -558,6 +609,7 @@ private class Puzzle : Object
 
     private bool timeout_cb ()
         requires (clock != null)
+        requires (!tainted_by_command_line)
     {
         /* Notify on the next tick */
         double elapsed = ((!) clock).elapsed ();
@@ -584,11 +636,11 @@ private class Puzzle : Object
 
     private class Inversion : Object
     {
-        public uint8 x0 { internal get; protected construct; }
-        public uint8 y0 { internal get; protected construct; }
-        public uint8 x1 { internal get; protected construct; }
-        public uint8 y1 { internal get; protected construct; }
-        public uint  id { internal get; protected construct; }
+        [CCode (notify = false)] public uint8 x0 { internal get; protected construct; }
+        [CCode (notify = false)] public uint8 y0 { internal get; protected construct; }
+        [CCode (notify = false)] public uint8 x1 { internal get; protected construct; }
+        [CCode (notify = false)] public uint8 y1 { internal get; protected construct; }
+        [CCode (notify = false)] public uint  id { internal get; protected construct; }
 
         internal Inversion (uint8 x0, uint8 y0, uint8 x1, uint8 y1, uint id)
         {
@@ -697,7 +749,7 @@ private class Puzzle : Object
     * * save and restore
     \*/
 
-    internal Variant to_variant ()
+    internal Variant to_variant (bool save_time)
     {
         VariantBuilder builder = new VariantBuilder (new VariantType ("m(yyda(yyyyyyyy)ua(yyyyu))"));
         builder.open (new VariantType ("(yyda(yyyyyyyy)ua(yyyyu))"));
@@ -705,7 +757,10 @@ private class Puzzle : Object
         // board
         builder.add ("y", size);
         builder.add ("y", colors);
-        builder.add ("d", elapsed);
+        if (save_time)
+            builder.add ("d", elapsed);
+        else
+            builder.add ("d", double.MAX);
 
         // tiles
         builder.open (new VariantType ("a(yyyyyyyy)"));
@@ -753,7 +808,7 @@ private class Puzzle : Object
         public uint8 initial_y;
     }
 
-    internal static bool is_valid_saved_game (Variant maybe_variant)
+    internal static bool is_valid_saved_game (Variant maybe_variant, bool restore_finished_game)
     {
         Variant? variant = maybe_variant.get_maybe ();
         if (variant == null)
@@ -839,7 +894,15 @@ private class Puzzle : Object
 
         // TODO validate history 1/2
 
-        return true;
+        if (restore_finished_game)
+            return true;
+
+        // return false if the game is finished, true otherwise
+        for (uint8 x = board_size; x < board_size * 2; x++)
+            for (uint8 y = 0; y < board_size; y++)
+                if (current_board [x, y])
+                    return true;
+        return false;
     }
 
     internal Puzzle.restore (Variant maybe_variant)
@@ -854,7 +917,7 @@ private class Puzzle : Object
         ((!) 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);
+        Object (size: _size, colors: _colors, restored: true, initial_time: _elapsed, 
tainted_by_command_line: _elapsed == double.MAX);
 
         Variant array_variant = ((!) variant).get_child_value (3);
         board = new Tile? [size * 2, size];
@@ -887,7 +950,17 @@ private class Puzzle : Object
         game_in_progress = true;
         if (solved_on_right ())
             is_solved_right = true;
+        check_if_solved ();
     }
 
     // TODO restore history 2/2
 }
+
+private enum Direction
+{
+    NONE,
+    UP,
+    DOWN,
+    LEFT,
+    RIGHT;
+}
diff --git a/src/score-overlay.vala b/src/score-overlay.vala
index 94c0ef4..fbb55cc 100644
--- a/src/score-overlay.vala
+++ b/src/score-overlay.vala
@@ -138,6 +138,47 @@ private class ScoreOverlay : Grid
                 break;
         }
     }
+
+    internal void display_fallback_scores (uint8          puzzle_size,
+                                           HistoryEntry?  best_score,
+                                           HistoryEntry?  second_score,
+                                           HistoryEntry?  third_score,
+                                           HistoryEntry?  worst_score)
+    {
+        /* Translators: text of the score overlay, displayed after a puzzle is complete; introduces the game 
best time (in a corner case) */
+        score_0.set_place_label (_("Best time:"));
+        if (best_score != null)
+            score_0.set_value_label (HistoryEntry.get_duration_string ((!) best_score));
+        else
+            score_0.set_value_label (null);
+
+        /* Translators: text of the score overlay, displayed after a puzzle is complete; introduces the 
second best time (in a corner case) */
+        score_1.set_place_label (_("Second:"));
+        if (second_score != null)
+            score_1.set_value_label (HistoryEntry.get_duration_string ((!) second_score));
+        else
+            score_1.set_value_label (null);
+
+        /* Translators: text of the score overlay, displayed after a puzzle is complete; introduces the 
third best time (in a corner case) */
+        score_2.set_place_label (_("Third:"));
+        if (third_score != null)
+            score_2.set_value_label (HistoryEntry.get_duration_string ((!) third_score));
+        else
+            score_2.set_value_label (null);
+
+        if (worst_score != null)
+        {
+            /* Translators: text of the score overlay, displayed after a puzzle is complete; introduces the 
worst time (in a corner case) */
+            score_3.set_place_label (_("Worst time:"));
+            score_3.set_value_label (HistoryEntry.get_duration_string ((!) worst_score));
+        }
+        else
+        {
+            /* Translators: text of the score overlay, displayed after a puzzle is complete; introduces the 
fourth time (in a corner case) */
+            score_3.set_place_label (_("Fourth:"));
+            score_3.set_value_label (null);
+        }
+    }
 }
 
 [GtkTemplate (ui = "/org/gnome/Tetravex/score-overlay-entry.ui")]



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