[gnome-sudoku] Warn when the solution, if any, is violated



commit 47bc730d65276015d7161b7c46c34a6849a3ad3a
Author: Steven Elliott <selliott512 gmail com>
Date:   Sun Jul 17 15:19:51 2022 -0400

    Warn when the solution, if any, is violated

 lib/qqwing-wrapper.cpp | 38 +++++++++++++++++++++-
 lib/qqwing-wrapper.h   |  1 +
 lib/qqwing.vapi        |  1 +
 lib/sudoku-board.vala  | 51 +++++++++++++++++++++++++++++
 src/gnome-sudoku.vala  |  3 ++
 src/sudoku-view.vala   | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 180 insertions(+), 1 deletion(-)
---
diff --git a/lib/qqwing-wrapper.cpp b/lib/qqwing-wrapper.cpp
index 3c6bc36..f056a55 100644
--- a/lib/qqwing-wrapper.cpp
+++ b/lib/qqwing-wrapper.cpp
@@ -29,6 +29,10 @@
 #include <glib.h>
 #include <qqwing.hpp>
 
+// Constants
+
+const int BOARD_SIZE = 81;
+
 /*
  * Generate a symmetric puzzle of specified difficulty.
  * The result must be freed with g_free().
@@ -37,7 +41,6 @@ int* qqwing_generate_puzzle(int difficulty)
 {
     int i = 0;
     const int MAX_ITERATIONS = 1000;
-    const int BOARD_SIZE = 81;
     qqwing::SudokuBoard board;
     static std::once_flag flag;
 
@@ -67,6 +70,39 @@ int* qqwing_generate_puzzle(int difficulty)
     return copy;
 }
 
+/*
+ * Solve a given puzzle in place. If true is returned the puzzle will be solved.
+ * If false is returned the puzzle will be unchanged.
+ */
+gboolean qqwing_solve_puzzle(int* puzzle)
+{
+    qqwing::SudokuBoard board;
+
+    if (!board.setPuzzle(puzzle))
+    {
+        // This can happen when there is no solution.
+        g_warning("Failed to solve puzzle: the puzzle could not be set.");
+        return FALSE;
+    }
+    if (!board.hasUniqueSolution())
+    {
+        // The multiple solution case.
+        g_warning("Failed to solve puzzle: the puzzle does not have a unique solution.");
+        return FALSE;
+    }
+    if (!board.solve())
+    {
+        // This should not happen given the above checks.
+        g_warning("Failed to solve puzzle: the call to solve() failed.");
+        return FALSE;
+    }
+
+    // Valid. Copy and return true.
+    const int* solution = board.getSolution();
+    std::copy(solution, &solution[BOARD_SIZE], puzzle);
+    return TRUE;
+}
+
 /*
  * Count the number of solutions of a puzzle
  * but return 2 if there are multiple.
diff --git a/lib/qqwing-wrapper.h b/lib/qqwing-wrapper.h
index a546e8d..384e8cb 100644
--- a/lib/qqwing-wrapper.h
+++ b/lib/qqwing-wrapper.h
@@ -27,6 +27,7 @@
 G_BEGIN_DECLS
 
 int *qqwing_generate_puzzle(int difficulty);
+gboolean qqwing_solve_puzzle(int* puzzle);
 int qqwing_count_solutions_limited(int *puzzle);
 void qqwing_print_stats(int *puzzle);
 char *qqwing_get_version(void);
diff --git a/lib/qqwing.vapi b/lib/qqwing.vapi
index f29205b..05ff5bc 100644
--- a/lib/qqwing.vapi
+++ b/lib/qqwing.vapi
@@ -23,6 +23,7 @@
 namespace QQwing {
     [CCode (array_length=false)]
     int[] generate_puzzle (int difficulty);
+    bool solve_puzzle([CCode (array_length = false)] int[] puzzle);
     int count_solutions_limited ([CCode (array_length = false)] int[] puzzle);
     void print_stats ([CCode (array_length = false)] int[] puzzle);
     string get_version ();
diff --git a/lib/sudoku-board.vala b/lib/sudoku-board.vala
index 3b61700..661bfa3 100644
--- a/lib/sudoku-board.vala
+++ b/lib/sudoku-board.vala
@@ -26,6 +26,7 @@ public class SudokuBoard : Object
     /* Implemented in such a way that it can be extended for other sizes ( like 2x3 sudoku or 4x4 sudoku ) 
instead of normal 3x3 sudoku. */
 
     protected int[,] cells;                     /* stores the value of the cells */
+    protected int[,] solution;                  /* stores the solution, if any, null otherwise */
     public bool[,] is_fixed;                    /* if the value at location is fixed or not */
     private bool[,] possible_in_row;            /* if specific value is possible in specific row */
     private bool[,] possible_in_col;            /* if specific value is possible in specific col */
@@ -172,6 +173,7 @@ public class SudokuBoard : Object
     {
         SudokuBoard board = new SudokuBoard (block_rows , block_cols);
         board.cells = cells;
+        board.solution = solution;
         board.is_fixed = is_fixed;
         board.possible_in_row = possible_in_row;
         board.possible_in_col = possible_in_col;
@@ -364,6 +366,31 @@ public class SudokuBoard : Object
             fixed--;
     }
 
+    public void set_solution (int row, int col, int val)
+    {
+        solution[row, col] = val;
+    }
+
+    public int get_solution (int row, int col)
+    {
+        return solution[row, col];
+    }
+
+    public void solve ()
+    {
+        int[] solution_1d = convert_2d_to_1d(cells);
+
+        if (QQwing.solve_puzzle (solution_1d))
+            solution = convert_1d_to_2d(solution_1d);
+        else
+            solution = null;
+    }
+
+    public bool solved ()
+    {
+        return solution != null;
+    }
+
     public int count_solutions_limited ()
     {
         return QQwing.count_solutions_limited ((int[]) cells);
@@ -521,6 +548,30 @@ public class SudokuBoard : Object
 
         return s;
     }
+
+    // Convert a 2D array to a 1D array. The 2D array is assumed to have
+    // dimensions rows, cols.
+    private int[] convert_2d_to_1d(int[,] ints_2d)
+    {
+        int[] ints_1d = new int[rows * cols];
+        int i = 0;
+        for (int row = 0; row < rows; row++)
+            for (int col = 0; col < cols; col++)
+                ints_1d[i++] = ints_2d[row, col];
+        return ints_1d;
+    }
+
+    // Convert a 1D array to a 2D array. The 1D array is assumed to have
+    // length rows * cols.
+    private int[,] convert_1d_to_2d(int[] ints_1d)
+    {
+        int[,] ints_2d = new int[rows, cols];
+        int i = 0;
+        for (int row = 0; row < rows; row++)
+            for (int col = 0; col < cols; col++)
+                ints_2d[row, col] = ints_1d[i++];
+        return ints_2d;
+    }
 }
 
 public enum House
diff --git a/src/gnome-sudoku.vala b/src/gnome-sudoku.vala
index ec9f8e5..84c29b7 100644
--- a/src/gnome-sudoku.vala
+++ b/src/gnome-sudoku.vala
@@ -310,6 +310,9 @@ public class Sudoku : Gtk.Application
 
     private void start_game (SudokuBoard board)
     {
+        if (current_game_mode == GameMode.PLAY)
+            board.solve();
+
         if (game != null)
         {
             game.paused_changed.disconnect (paused_changed_cb);
diff --git a/src/sudoku-view.vala b/src/sudoku-view.vala
index 1c0da5e..656e81b 100644
--- a/src/sudoku-view.vala
+++ b/src/sudoku-view.vala
@@ -36,6 +36,10 @@ private class SudokuCellView : DrawingArea
 
     private bool initialized_earmarks;
 
+    // Whether the control keys are pressed.
+    private bool left_control;
+    private bool right_control;
+
     public int value
     {
         get { return game.board [row, col]; }
@@ -236,6 +240,10 @@ private class SudokuCellView : DrawingArea
         {
             popover.destroy ();
             popover = null;
+
+            // Destroying a popover means that this type of warning is now possible.
+            if (warn_incorrect_solution())
+                queue_draw ();
         }
     }
 
@@ -284,10 +292,16 @@ private class SudokuCellView : DrawingArea
     {
         key_controller = new EventControllerKey (this);
         key_controller.key_pressed.connect (on_key_pressed);
+        key_controller.key_released.connect (on_key_release);
     }
 
     private inline bool on_key_pressed (EventControllerKey _key_controller, uint keyval, uint keycode, 
ModifierType state)
     {
+        if (keyval == Gdk.Key.Control_L)
+            left_control = true;
+        if (keyval == Gdk.Key.Control_R)
+            right_control = true;
+
         if (game.mode == GameMode.PLAY && (is_fixed || game.paused))
             return false;
         string k_name = keyval_name (keyval);
@@ -347,6 +361,25 @@ private class SudokuCellView : DrawingArea
         return false;
     }
 
+    private inline void on_key_release (EventControllerKey _key_controller, uint keyval, uint keycode, 
ModifierType state)
+    {
+        bool control_released = false;
+        if (keyval == Gdk.Key.Control_L)
+        {
+            left_control = false;
+            control_released = true;
+        }
+        if (keyval == Gdk.Key.Control_R)
+        {
+            right_control = false;
+            control_released = true;
+        }
+
+        // Releasing a control means that this type of warning is now possible.
+        if (control_released && warn_incorrect_solution())
+            queue_draw ();
+    }
+
     public override bool draw (Cairo.Context c)
     {
         RGBA background_color;
@@ -358,6 +391,45 @@ private class SudokuCellView : DrawingArea
             background_color = highlight_color;
         else
             background_color = free_cell_color;
+
+        // Highlight the cell if the value or earmarks are inconsistent with
+        // a known solution, if any.
+        if (warn_incorrect_solution())
+        {
+            bool cell_error = false;
+            int solution = game.board.get_solution (row, col);
+            if (value != 0)
+            {
+                // Check value against the solution.
+                cell_error = value != solution;
+            }
+            else
+            {
+                // Check earmarks against the solution.
+                var marks = game.board.get_earmarks (row, col);
+                bool earmarked = false;
+                bool solution_found = false;
+                for (int num = 1; num <= marks.length; num++)
+                {
+                    if (marks[num - 1])
+                    {
+                        earmarked = true;
+                        if (num == solution)
+                            solution_found = true;
+                    }
+                }
+                if (earmarked && !solution_found)
+                    cell_error = true;
+            }
+
+            // Make the error cell more red by reducing the other colors to 60%.
+            if (cell_error)
+            {
+                background_color.green *= 0.6;
+                background_color.blue  *= 0.6;
+            }
+        }
+
         c.set_source_rgba (background_color.red, background_color.green, background_color.blue, 
background_color.alpha);
         c.rectangle (0, 0, get_allocated_width (), get_allocated_height ());
         c.fill();
@@ -489,6 +561,21 @@ private class SudokuCellView : DrawingArea
     {
         game.board.disable_all_earmarks (row, col);
     }
+
+    // Return true if the user is to be warned when the value or earmarks are
+    // inconsistent with the known solution, and it is ok for the user to be
+    // warned.
+    private bool warn_incorrect_solution()
+    {
+        // In the following popovers are checked so that the solution of the cell
+        // is not revealed to the user as the user enters candidate numbers for
+        // the cell using the earmark picker. Similarly don't reveal the solution
+        // while earmarks are being entered with the control key.
+        return _show_warnings &&                                  // show warnings?
+               (popover == null) && (earmark_popover == null) && // popovers gone?
+               (!left_control) && (!right_control) &&             // control keys not pressed?
+               game.board.solved();                               // solution exists?
+    }
 }
 
 public const RGBA fixed_cell_color = {0.8, 0.8, 0.8, 1.0};


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