[gnome-sudoku/vala-port] vala port
- From: Thomas Hindoe Paaboel Andersen <thomashpa src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-sudoku/vala-port] vala port
- Date: Fri, 7 Jun 2013 19:00:52 +0000 (UTC)
commit f935f123faa7bc95c7dd6270e2c29216aae5f77c
Author: Thomas Hindoe Paaboel Andersen <phomes gmail com>
Date: Fri Jun 7 22:07:55 2013 +0200
vala port
by Christopher Baines
configure.ac | 22 +-
data/Makefile.am | 7 +-
data/gnome-sudoku-menu.ui | 65 ++
data/gnome-sudoku.desktop.in.in | 3 +-
data/gnome-sudoku.ui | 319 +++++++++
data/org.gnome.gnome-sudoku.gschema.xml.in | 5 +
data/print_games.ui | 326 ---------
data/select_game.ui | 134 ----
data/tracker.ui | 104 ---
src/Makefile.am | 50 ++-
src/config.vapi | 8 +
src/gnome-sudoku | 23 -
src/gnome-sudoku.gresource.xml.in | 13 +
src/gnome-sudoku.vala | 501 ++++++++++++++
src/lib/Makefile.am | 31 -
src/lib/__init__.py | 1 -
src/lib/colors.py | 85 ---
src/lib/dancer.py | 137 ----
src/lib/defaults.py | 58 --
src/lib/defs.py.in | 2 -
src/lib/dialog_swallower.py | 77 ---
src/lib/game_selector.py | 282 --------
src/lib/gnome_sudoku.py | 22 -
src/lib/gsudoku.py | 817 -----------------------
src/lib/gtk_goodies/Makefile.am | 7 -
src/lib/gtk_goodies/Undo.py | 474 -------------
src/lib/gtk_goodies/__init__.py | 1 -
src/lib/gtk_goodies/dialog_extras.py | 236 -------
src/lib/main.py | 995 ----------------------------
src/lib/number_box.py | 801 ----------------------
src/lib/pausable.py | 65 --
src/lib/printing.py | 215 ------
src/lib/saver.py | 285 --------
src/lib/simple_debug.py | 45 --
src/lib/sudoku.py | 912 -------------------------
src/lib/sudoku_maker.py | 607 -----------------
src/lib/sudoku_thumber.py | 161 -----
src/lib/timer.py | 87 ---
src/lib/tracker_info.py | 208 ------
src/main.vala | 85 +++
src/number-picker.vala | 56 ++
src/sudoku-board.vala | 512 ++++++++++++++
src/sudoku-game.vala | 156 +++++
src/sudoku-generator.vala | 413 ++++++++++++
src/sudoku-solver.vala | 872 ++++++++++++++++++++++++
src/sudoku-store.vala | 109 +++
src/sudoku-view.vala | 819 +++++++++++++++++++++++
47 files changed, 3995 insertions(+), 7218 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 3d8f133..19b2c5f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4,21 +4,29 @@ AM_SILENT_RULES([yes])
AM_MAINTAINER_MODE([enable])
GNOME_MAINTAINER_MODE_DEFINES
-GLIB_GSETTINGS
+AM_PROG_VALAC([0.16.0])
+AM_PROG_CC_C_O
-AM_PATH_PYTHON([2.4],,AC_MSG_ERROR([Python not found]))
+GLIB_GSETTINGS
dnl ###########################################################################
dnl Dependencies
dnl ###########################################################################
-PYGOBJECT_REQUIRED=2.28.3
+GTK_REQUIRED=3.4.0
-PKG_CHECK_MODULES(PYGOBJECT, [
- pygobject-3.0 >= $PYGOBJECT_REQUIRED
+PKG_CHECK_MODULES(GNOME_SUDOKU, [
+ gtk+-3.0 >= $GTK_REQUIRED
])
dnl ###########################################################################
+dnl GResources
+dnl ###########################################################################
+
+GLIB_COMPILE_RESOURCES=`$PKG_CONFIG --variable glib_compile_resources gio-2.0`
+AC_SUBST(GLIB_COMPILE_RESOURCES)
+
+dnl ###########################################################################
dnl Internationalization
dnl ###########################################################################
@@ -45,7 +53,5 @@ data/icons/HighContrast/Makefile
data/gnome-sudoku.desktop.in
help/Makefile
src/Makefile
-src/lib/Makefile
-src/lib/defs.py
-src/lib/gtk_goodies/Makefile
+src/gnome-sudoku.gresource.xml
])
diff --git a/data/Makefile.am b/data/Makefile.am
index f3dc54d..5f1d490 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -20,11 +20,8 @@ puzzle_DATA = \
hard \
very_hard
-uidir = $(datadir)/gnome-sudoku
-ui_DATA = \
- print_games.ui \
- select_game.ui \
- tracker.ui
+dist_noinst_DATA = \
+ gnome-sudoku.ui
man_MANS = gnome-sudoku.6
diff --git a/data/gnome-sudoku-menu.ui b/data/gnome-sudoku-menu.ui
new file mode 100644
index 0000000..9dc0515
--- /dev/null
+++ b/data/gnome-sudoku-menu.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <menu id="sudoku-menu">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_New Game</attribute>
+ <attribute name="action">app.new-game</attribute>
+ <attribute name="accel"><Primary>n</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Reset</attribute>
+ <attribute name="action">app.reset</attribute>
+ <attribute name="accel"><Primary>r</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Undo</attribute>
+ <attribute name="action">app.undo</attribute>
+ <attribute name="accel"><Primary>u</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Redo</attribute>
+ <attribute name="action">app.redo</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Print</attribute>
+ <attribute name="action">app.print</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Print Multiple Sudoku's...</attribute>
+ <attribute name="action">app.print-multiple</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Show Possible Numbers</attribute>
+ <attribute name="action">app.possible-numbers</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Warn About Unfillable Squares</attribute>
+ <attribute name="action">app.unfillable-squares</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Help</attribute>
+ <attribute name="action">app.help</attribute>
+ <attribute name="accel">F1</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_About</attribute>
+ <attribute name="action">app.about</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Quit</attribute>
+ <attribute name="action">app.quit</attribute>
+ <attribute name="accel"><Primary>q</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/data/gnome-sudoku.desktop.in.in b/data/gnome-sudoku.desktop.in.in
index f94e956..ddbfd60 100644
--- a/data/gnome-sudoku.desktop.in.in
+++ b/data/gnome-sudoku.desktop.in.in
@@ -1,14 +1,13 @@
[Desktop Entry]
_Name=Sudoku
_Comment=Test your logic skills in this number grid puzzle
-_Keywords=game;board;tiles;japanese;
Exec=gnome-sudoku
Icon=gnome-sudoku
Terminal=false
Type=Application
Categories=GNOME;GTK;Game;LogicGame;
X-GNOME-Bugzilla-Bugzilla=GNOME
-X-GNOME-Bugzilla-Product=gnome-sudoku
+X-GNOME-Bugzilla-Product=gnome-games
X-GNOME-Bugzilla-Component=BugBuddyBugs
X-GNOME-Bugzilla-Version= VERSION@
StartupNotify=true
diff --git a/data/gnome-sudoku.ui b/data/gnome-sudoku.ui
new file mode 100644
index 0000000..5fe83d8
--- /dev/null
+++ b/data/gnome-sudoku.ui
@@ -0,0 +1,319 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkApplicationWindow" id="sudoku_app">
+ <property name="title" translatable="yes">Sudoku</property>
+ <property name="hide_titlebar_when_maximized">True</property>
+ <child>
+ <object class="GtkBox" id="main_box">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="start_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="new_game_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">New Game</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="gravity" value="west"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="new_game_box">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="easy_grid">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="easy_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Easy</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="medium_grid">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="medium_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Medium</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hard_grid">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="hard_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Hard</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="very_hard_grid">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="very_hard_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Very
Hard</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">20</property>
+ </packing>
+ </child>
+ </object> <!-- End of new game box -->
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ <property name="padding">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="saved_game_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Saved Game</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="gravity" value="west"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object> <!-- End of new start_box -->
+ </child>
+ <child>
+ <object class="GtkBox" id="game_box">
+ <property name="visible">False</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkBox" id="controls_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="number_picker_box">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="back_button">
+ <property name="label" translatable="yes">Back</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="hint_button">
+ <property name="label" translatable="yes">Hint</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="help_button">
+ <property name="label" translatable="yes">Help</property>
+ <property name="use_action_appearance">False</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object> <!-- End of controls_box -->
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="padding">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="grid_box">
+ <property name="visible">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object> <!-- End of game_box -->
+ </child>
+ <child>
+ <object class="GtkBox" id="help_box">
+ <property name="visible">False</property>
+ <property name="spacing">2</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkExpander" id="naked_singles">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox" id="naked_single_items">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="naked_singles_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Naked Singles</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkExpander" id="hidden_singles">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox" id="hidden_single_items">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="hidden_singles_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Hidden Singles</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkExpander" id="naked_subsets">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox" id="naked_subset_items">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel" id="naked_subsets_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Naked Subsets</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object> <!-- End of help_box -->
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/data/org.gnome.gnome-sudoku.gschema.xml.in b/data/org.gnome.gnome-sudoku.gschema.xml.in
index 61c58ce..fb22252 100644
--- a/data/org.gnome.gnome-sudoku.gschema.xml.in
+++ b/data/org.gnome.gnome-sudoku.gschema.xml.in
@@ -101,5 +101,10 @@
<summary>Number of puzzles to print on a page</summary>
<description>Number of puzzles to print on a page</description>
</key>
+ <key name="fullscreen" type="b">
+ <default>false</default>
+ <_summary>A flag to enable fullscreen mode</_summary>
+ <_description>A flag to enable fullscreen mode</_description>
+ </key>
</schema>
</schemalist>
diff --git a/src/Makefile.am b/src/Makefile.am
index 5318775..38ffad3 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,8 +1,48 @@
-SUBDIRS = lib
+bin_PROGRAMS = gnome-sudoku
-#################################################################
+BUILT_SOURCES = gnome-sudoku-resources.c
-## Executable
-dist_bin_SCRIPTS = gnome-sudoku
+gnome_sudoku_SOURCES = \
+ config.vapi \
+ main.vala \
+ gnome-sudoku.vala \
+ sudoku-board.vala \
+ sudoku-game.vala \
+ sudoku-generator.vala \
+ sudoku-store.vala \
+ sudoku-solver.vala \
+ logical-sudoku-solver.vala \
+ sudoku-view.vala \
+ number-picker.vala \
+ $(BUILT_SOURCES)
--include $(top_srcdir)/git.mk
+gnome_sudoku_CFLAGS = \
+ -DPKGDATADIR=\"@datadir@/gnome-sudoku\" \
+ -DLOCALEDIR=\"@localedir \" \
+ -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \
+ $(GTK_CFLAGS) \
+ $(GEE_CFLAGS) \
+ $(GMODULE_CFLAGS) \
+ $(WARN_CFLAGS)
+
+gnome_sudoku_LDADD = \
+ $(GTK_LIBS) \
+ $(GEE_LIBS) \
+ $(GMODULE_LIBS)
+
+gnome_sudoku_VALAFLAGS = \
+ --pkg posix \
+ --pkg gtk+-3.0 \
+ --pkg gee-1.0 \
+ --pkg gmodule-2.0 \
+ --vapidir $(top_srcdir)/libgames-support \
+ --pkg GnomeGamesSupport-1.0
+
+gnome-sudoku-resources.c: gnome-sudoku.gresource.xml $(shell $(GLIB_COMPILE_RESOURCES)
--generate-dependencies gnome-sudoku.gresource.xml)
+ $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir) --generate-source
gnome-sudoku.gresource.xml
+
+EXTRA_DIST = \
+ gnome_sudoku.gresource.xml
+
+DISTCLEANFILES = \
+ Makefile.in
diff --git a/src/config.vapi b/src/config.vapi
new file mode 100644
index 0000000..aae449c
--- /dev/null
+++ b/src/config.vapi
@@ -0,0 +1,8 @@
+[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")]
+namespace Config
+{
+ public const string PKGDATADIR;
+ public const string LOCALEDIR;
+ public const string GETTEXT_PACKAGE;
+ public const string VERSION;
+}
diff --git a/src/gnome-sudoku.gresource.xml.in b/src/gnome-sudoku.gresource.xml.in
new file mode 100644
index 0000000..f8c08ab
--- /dev/null
+++ b/src/gnome-sudoku.gresource.xml.in
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/gnome-sudoku/ui">
+ <file alias="gnome-sudoku.ui" preprocess="xml-stripblanks">@top_srcdir@/data/gnome-sudoku.ui</file>
+ <file alias="gnome-sudoku-menu.ui"
preprocess="xml-stripblanks">@top_srcdir@/data/gnome-sudoku-menu.ui</file>
+ </gresource>
+ <gresource prefix="/org/gnome/gnome-sudoku/puzzles">
+ <file alias="easy">@top_srcdir@/data/easy</file>
+ <file alias="medium">@top_srcdir@/data/medium</file>
+ <file alias="hard">@top_srcdir@/data/hard</file>
+ <file alias="very_hard">@top_srcdir@/data/very_hard</file>
+ </gresource>
+</gresources>
diff --git a/src/gnome-sudoku.vala b/src/gnome-sudoku.vala
new file mode 100644
index 0000000..233eb9e
--- /dev/null
+++ b/src/gnome-sudoku.vala
@@ -0,0 +1,501 @@
+using Gtk;
+using Gee;
+
+public class Sudoku : Gtk.Application
+{
+ private GLib.Settings settings;
+ private Builder builder;
+
+ private ApplicationWindow window;
+ private CheckMenuItem fullscreen_menu;
+
+ // The current game and view, if they exist
+ private SudokuGame game;
+ private SudokuView view;
+
+ // The start box (contains the new game previews)
+ private Box start_box;
+ private Box game_box; // Holds the grid and controls boxes
+ private Box grid_box; // Holds the view
+ private Box help_box;
+
+ private Box controls_box; // Holds the controls (including the number picker)
+ private NumberPicker number_picker;
+
+ private SudokuView easy_preview;
+ private SudokuView medium_preview;
+ private SudokuView hard_preview;
+ private SudokuView very_hard_preview;
+
+ private SudokuStore sudoku_store;
+
+ // Help Stuff
+ private Box naked_single_items;
+ private Box hidden_single_items;
+ private Box naked_subset_items;
+
+ private const GLib.ActionEntry action_entries[] =
+ {
+ {"new-game", new_game_cb },
+ {"reset", reset_cb },
+ {"undo", undo_cb },
+ {"redo", redo_cb },
+ {"print", print_cb },
+ {"print-multiple", print_multiple_cb },
+ {"possible-numbers", possible_numbers_cb, null, "false", possible_numbers_changed },
+ {"unfillable-squares", unfillable_squares_cb, null, "false", unfillable_squares_changed },
+ {"help", help_cb },
+ {"about", about_cb },
+ {"quit", quit_cb }
+ };
+
+ public Sudoku ()
+ {
+ Object (application_id: "org.gnome.gnome-sudoku", flags: ApplicationFlags.FLAGS_NONE);
+ }
+
+ protected override void startup()
+ {
+ base.startup ();
+ add_action_entries (action_entries, this);
+ }
+
+ protected override void activate () {
+ settings = new GLib.Settings ("org.gnome.gnome-sudoku");
+
+ builder = new Builder ();
+ try
+ {
+ builder.add_from_resource ("/org/gnome/gnome-sudoku/ui/gnome-sudoku.ui");
+ builder.add_from_resource ("/org/gnome/gnome-sudoku/ui/gnome-sudoku-menu.ui");
+ }
+ catch (GLib.Error e)
+ {
+ GLib.warning ("Could not load UI: %s", e.message);
+ }
+ window = (ApplicationWindow) builder.get_object ("sudoku_app");
+
+ if (settings.get_boolean ("fullscreen"))
+ window.fullscreen ();
+
+ add_window (window);
+
+ set_app_menu (builder.get_object ("sudoku-menu") as MenuModel);
+
+ fullscreen_menu = (CheckMenuItem) builder.get_object ("toggle_fullscreen_imagemenuitem");
+
+ start_box = (Box) builder.get_object ("start_box");
+ game_box = (Box) builder.get_object ("game_box");
+ grid_box = (Box) builder.get_object ("grid_box");
+ help_box = (Box) builder.get_object ("help_box");
+
+ naked_single_items = (Box) builder.get_object ("naked_single_items");
+ hidden_single_items = (Box) builder.get_object ("hidden_single_items");
+
+ controls_box = (Box) builder.get_object ("number_picker_box");
+
+ var back_button = (Button) builder.get_object ("back_button");
+ back_button.clicked.connect (() => {
+ undo_cb();
+ });
+
+ var hint_button = (Button) builder.get_object ("hint_button");
+ hint_button.clicked.connect (() => {
+ view.hint ();
+ });
+
+ var help_button = (Button) builder.get_object ("help_button");
+ help_button.clicked.connect (() => {
+ help_box.visible = !help_box.visible;
+ });
+
+ sudoku_store = new SudokuStore ();
+ //SudokuGenerator gen = new SudokuGenerator();
+
+ var easy_grid = (Box) builder.get_object ("easy_grid");
+ var medium_grid = (Box) builder.get_object ("medium_grid");
+ var hard_grid = (Box) builder.get_object ("hard_grid");
+ var very_hard_grid = (Box) builder.get_object ("very_hard_grid");
+
+ SudokuBoard easy_board = sudoku_store.get_random_easy_board ();
+ //gen.make_symmetric_puzzle(Random.int_range(0, 4));
+ //gen.generate (DifficultyRating.easy_range);
+ easy_preview = new SudokuView (new SudokuGame (easy_board), true);
+ easy_preview.show ();
+ easy_grid.pack_start (easy_preview);
+
+ easy_grid.button_press_event.connect ((event) => {
+ if (event.button == 1)
+ start_game (easy_board);
+
+ return false;
+ });
+
+ SudokuBoard medium_board = sudoku_store.get_random_medium_board ();
+ //gen.make_symmetric_puzzle(Random.int_range(0, 4));
+ // gen.generate (DifficultyRating.medium_range);
+ medium_preview = new SudokuView (new SudokuGame (medium_board), true);
+ medium_preview.show ();
+ medium_grid.pack_start (medium_preview);
+
+ medium_grid.button_press_event.connect ((event) => {
+ if (event.button == 1)
+ start_game (medium_board);
+
+ return false;
+ });
+
+ SudokuBoard hard_board = sudoku_store.get_random_hard_board ();
+ //gen.make_symmetric_puzzle(Random.int_range(0, 4));
+ //gen.generate (DifficultyRating.hard_range);
+ hard_preview = new SudokuView (new SudokuGame (hard_board), true);
+ hard_preview.show ();
+ hard_grid.pack_start (hard_preview);
+
+ hard_grid.button_press_event.connect ((event) => {
+ if (event.button == 1)
+ start_game (hard_board);
+
+ return false;
+ });
+
+ SudokuBoard very_hard_board = sudoku_store.get_random_very_hard_board ();
+ //gen.make_symmetric_puzzle(Random.int_range(0, 4));
+ //gen.generate (DifficultyRating.very_hard_range);
+ very_hard_preview = new SudokuView (new SudokuGame (very_hard_board), true);
+ very_hard_preview.show ();
+ very_hard_grid.pack_start (very_hard_preview);
+
+ very_hard_grid.button_press_event.connect ((event) => {
+ if (event.button == 1)
+ start_game (very_hard_board);
+
+ return false;
+ });
+
+ show_start ();
+
+ builder.connect_signals (this);
+
+ window.show ();
+ }
+
+ private void start_game (SudokuBoard board)
+ {
+ SudokuBoard completed_board = board.clone ();
+
+ SudokuRater rater = new SudokuRater(ref completed_board);
+ DifficultyRating rating = rater.get_difficulty ();
+ rating.pretty_print ();
+
+ bool show_possibilities = false;
+ bool show_warnings = false;
+
+ if (view != null) {
+ show_possibilities = view.show_possibilities;
+ show_warnings = view.show_warnings;
+
+ grid_box.remove (view);
+ controls_box.remove (number_picker);
+ }
+
+ game_box.visible = true;
+ start_box.visible = false;
+
+ game = new SudokuGame (board);
+
+ if (game.timer.elapsed() <= 0.000002)
+ {
+ game.timer.start ();
+ }
+ else
+ {
+ game.timer.continue ();
+ }
+
+ view = new SudokuView (game);
+
+ view.show_possibilities = show_possibilities;
+ view.show_warnings = show_warnings;
+
+ view.show ();
+ grid_box.pack_start (view);
+
+ LogicalSudokuSolver logical_solver = new LogicalSudokuSolver(ref game.board);
+ update_help (logical_solver);
+
+ number_picker = new NumberPicker(ref game.board);
+ controls_box.pack_start (number_picker);
+
+ view.cell_focus_in_event.connect ((row, col) => {
+ // Only enable the NumberPicker for unfixed cells
+ number_picker.sensitive = !game.board.is_fixed[row, col];
+ });
+
+ view.cell_value_changed_event.connect ((row, col) => {
+ update_help (logical_solver);
+ });
+
+
+ number_picker.number_picked.connect ((number) => {
+ view.set_cell_value (view.selected_x, view.selected_y, number);
+ });
+
+ game.board.completed.connect (() => {
+ view.dance ();
+
+ double time = game.timer.elapsed ();
+
+ for (var i = 0; i < game.board.rows; i++)
+ {
+ for (var j = 0; j < game.board.cols; j++)
+ {
+ view.can_focus = false;
+ }
+ }
+
+ var dialog = new MessageDialog(null, DialogFlags.DESTROY_WITH_PARENT, MessageType.INFO,
ButtonsType.NONE, "Well done, you completed the puzzle in %f seconds", time);
+
+ dialog.add_button ("Same difficulty again", 0);
+ dialog.add_button ("New difficulty", 1);
+
+ dialog.response.connect ((response_id) => {
+ switch (response_id)
+ {
+ case 0:
+ start_game (sudoku_store.get_random_board (rating.get_catagory ()));
+ break;
+ case 1:
+ show_start ();
+ break;
+ }
+ dialog.destroy ();
+ });
+
+ dialog.show ();
+ });
+ }
+
+ private void update_help (LogicalSudokuSolver logical_solver)
+ {
+ foreach (Widget w in naked_single_items.get_children())
+ {
+ naked_single_items.remove (w);
+ }
+
+ ArrayList<Cell?> naked_singles = logical_solver.get_naked_singles ();
+
+ foreach (Cell? cell in naked_singles)
+ {
+ var event_box = new Gtk.EventBox ();
+
+ var label = new Gtk.Label ("%d is the only possiblility in %d, %d".printf (cell.val,
cell.coord.col, cell.coord.row));
+
+ event_box.enter_notify_event.connect ((event) => {
+ view.cell_grab_focus (cell.coord.row, cell.coord.col);
+ return true;
+ });
+
+ label.use_markup = true;
+ event_box.add (label);
+ naked_single_items.add (event_box);
+ event_box.show ();
+ label.show ();
+ }
+
+ foreach (Widget w in hidden_single_items.get_children())
+ {
+ hidden_single_items.remove (w);
+ }
+
+ foreach (HiddenSingle? hidden_single in logical_solver.get_hidden_singles ())
+ {
+ var event_box = new Gtk.EventBox ();
+
+ string description = "%d should go in %d, %d because its the only place it can go in the
associated ".printf (hidden_single.cell.val, hidden_single.cell.coord.col, hidden_single.cell.coord.row);
+ if (hidden_single.row && hidden_single.col && hidden_single.block)
+ {
+ description += "row, column and block";
+ }
+ else if (hidden_single.row && hidden_single.col)
+ {
+ description += "row and column";
+ }
+ else if (hidden_single.col && hidden_single.block)
+ {
+ description += "column and block";
+ }
+ else if (hidden_single.row && hidden_single.block)
+ {
+ description += "row and block";
+ }
+ else if (hidden_single.row)
+ {
+ description += "row";
+ }
+ else if (hidden_single.col)
+ {
+ description += "column";
+ }
+ else if (hidden_single.block)
+ {
+ description += "block";
+ }
+
+ var label = new Gtk.Label (description);
+
+ event_box.enter_notify_event.connect ((event) => {
+ view.cell_grab_focus (hidden_single.cell.coord.row, hidden_single.cell.coord.col);
+ return true;
+ });
+
+ label.enter_notify_event.connect ((event) => {
+ view.selected_x = hidden_single.cell.coord.col;
+ view.selected_y = hidden_single.cell.coord.row;
+ return true;
+ });
+
+ label.use_markup = true;
+ event_box.add (label);
+ hidden_single_items.add (event_box);
+ event_box.show ();
+ label.show ();
+ }
+ }
+
+ private void show_start ()
+ {
+ game_box.visible = false;
+ start_box.visible = true;
+ }
+
+ public void new_game_cb ()
+ {
+ show_start ();
+ }
+
+ public void reset_cb ()
+ {
+ game.reset ();
+ }
+
+ public void undo_cb ()
+ {
+ game.undo ();
+ }
+
+ public void redo_cb ()
+ {
+ game.redo ();
+ }
+
+ public void print_cb ()
+ {
+ stdout.printf ("TODO: Print\n");
+ }
+
+ public void print_multiple_cb ()
+ {
+ stdout.printf ("TODO: Print multiple\n");
+ }
+
+ public void possible_numbers_cb ()
+ {
+ view.show_possibilities = !view.show_possibilities;
+ }
+
+ private void possible_numbers_changed (SimpleAction action, Variant state)
+ {
+ view.show_possibilities = (bool) state;
+ }
+
+ public void unfillable_squares_cb ()
+ {
+ view.show_warnings = !view.show_warnings;
+ }
+
+ private void unfillable_squares_changed (SimpleAction action, Variant state)
+ {
+ view.show_warnings = (bool) state;
+ }
+
+ public void quit_cb ()
+ {
+ window.destroy ();
+ }
+
+ public bool sudoku_app_window_state_event_cb (Widget widget, Gdk.EventWindowState event)
+ {
+ if ((event.changed_mask & Gdk.WindowState.FULLSCREEN) != 0)
+ {
+ bool is_fullscreen = (event.new_window_state & Gdk.WindowState.FULLSCREEN) != 0;
+ settings.set_boolean ("fullscreen", is_fullscreen);
+ fullscreen_menu.active = is_fullscreen;
+ }
+
+ return false;
+ }
+
+ public void toggle_hints_cb (Widget widget)
+ {
+ view.show_hints = !view.show_hints;
+ }
+
+ public void toggle_warnings_cb (Widget widget)
+ {
+ view.show_warnings = !view.show_warnings;
+ }
+
+ public void hint_cb (Widget widget)
+ {
+ view.hint ();
+ }
+
+ public void clear_top_notes_cb (Widget widget)
+ {
+ view.clear_top_notes ();
+ }
+
+ public void clear_bottom_notes_cb (Widget widget)
+ {
+ view.clear_bottom_notes ();
+ }
+
+ public void toggle_tracker_cb (Widget widget)
+ {
+ stdout.printf ("TODO: Toggle tracker\n");
+ }
+
+ public void help_cb ()
+ {
+ try
+ {
+ show_uri (window.get_screen (), "ghelp:gnome-sudoku", get_current_event_time ());
+ }
+ catch (GLib.Error e)
+ {
+ GLib.warning ("Unable to open help: %s", e.message);
+ }
+ }
+
+ private const string[] authors = { "Robert Ancell <robert ancell gmail com>", "Christopher Baines
<cbaines8 gmail com>" };
+ private const string[] artists = { "Place Holder <place holder com>" };
+
+ public void about_cb ()
+ {
+ show_about_dialog (window,
+ "program-name", _("Sudoku"),
+ "logo-icon-name", "gnome-sudoku",
+ "version", Config.VERSION,
+ "comments", _("GNOME Sudoku is a simple Sudoku generator and player. Sudoku
is a Japanese logic puzzle.\n\nGNOME Sudoku is a part of GNOME Games."),
+ "copyright", "Copyright 2010-2011 Robert Ancell <robert ancell gmail com>",
+ "license-type", License.GPL_2_0,
+ "wrap-license", false,
+ "authors", authors,
+ "artists", artists,
+ "translator-credits", _("translator-credits"),
+ "website", "http://www.gnome.org/projects/gnome-games/",
+ "website-label", _("GNOME Games web site")
+ );
+ }
+}
diff --git a/src/main.vala b/src/main.vala
new file mode 100644
index 0000000..7a0dfdd
--- /dev/null
+++ b/src/main.vala
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 Robert Ancell <robert ancell gmail com>
+ *
+ * This program 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.
+ *
+ * This program 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 program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+private static bool show_version;
+
+private static const OptionEntry[] options =
+{
+ { "version", 'v', 0, OptionArg.NONE, ref show_version,
+ /* Help string for command line --version flag */
+ N_("Show release version"), null},
+ { null }
+};
+
+public static int main (string[] args)
+{
+ //SudokuGenerator.gen_stats ();
+ //SudokuRater.gen_python_test ();
+
+ //SudokuGenerator gen = new SudokuGenerator ();
+ //SudokuBoard board = gen.make_symmetric_puzzle(GLib.Random.int_range(0, 4));
+ //board.get_string ();
+ //stdout.printf("\n");
+
+ //SudokuBoard test_board = board.clone ();
+ //SudokuSolver test_solver = new SudokuSolver (ref test_board);
+
+ /*if (test_solver.quick_has_unique_solution ())
+ stdout.printf("Unique solution\n");
+ else
+ stdout.printf("No unique solution\n");
+
+
+ SudokuSolver solver = new SudokuSolver (ref board);
+ foreach (SudokuBoard solved_board in solver)
+ {
+ solved_board.get_string ();
+ stdout.printf("\n");
+ }*/
+
+ //return 0;
+
+ Gtk.init (ref args);
+
+ var c = new OptionContext (/* Arguments and description for --help text */
+ _("[FILE] - Play Sudoku"));
+ c.add_main_entries (options, Config.GETTEXT_PACKAGE);
+ c.add_group (Gtk.get_option_group (true));
+ try
+ {
+ c.parse (ref args);
+ }
+ catch (Error e)
+ {
+ stderr.printf ("%s\n", e.message);
+ stderr.printf (/* Text printed out when an unknown command-line argument provided */
+ _("Run '%s --help' to see a full list of available command line options."), args[0]);
+ stderr.printf ("\n");
+ return Posix.EXIT_FAILURE;
+ }
+ if (show_version)
+ {
+ /* Note, not translated so can be easily parsed */
+ stderr.printf ("gnome-sudoku %s\n", Config.VERSION);
+ return Posix.EXIT_SUCCESS;
+ }
+
+ var app = new Sudoku ();
+
+ return app.run ();
+}
diff --git a/src/number-picker.vala b/src/number-picker.vala
new file mode 100644
index 0000000..9ce3a38
--- /dev/null
+++ b/src/number-picker.vala
@@ -0,0 +1,56 @@
+private class NumberPicker : Gtk.Grid
+{
+ private SudokuBoard board;
+
+ public signal void number_picked (int number);
+
+ public NumberPicker (ref SudokuBoard board, bool show_clear = true) {
+ this.board = board;
+
+ for (var col = 0; col < board.block_cols; col++)
+ {
+ for (var row = 0; row < board.block_rows; row++)
+ {
+ int n = col + row * board.block_cols + 1;
+
+ var button = new Gtk.Button ();
+ button.focus_on_click = false;
+ this.attach (button, col, row, 1, 1);
+
+ var label = new Gtk.Label ("<big>%d</big>".printf (n));
+ label.use_markup = true;
+ button.add (label);
+ label.show ();
+
+ button.clicked.connect (() => {
+ number_picked(n);
+ });
+
+ if (n == 5)
+ button.grab_focus ();
+
+ button.show ();
+ }
+ }
+
+ if (show_clear)
+ {
+ var button = new Gtk.Button ();
+ button.focus_on_click = false;
+ this.attach (button, 0, 4, 3, 1);
+
+ var label = new Gtk.Label ("<big>Clear</big>");
+ label.use_markup = true;
+ button.add (label);
+ label.show ();
+
+ button.clicked.connect (() => {
+ number_picked(0);
+ });
+
+ button.show ();
+ }
+
+ this.show ();
+ }
+}
diff --git a/src/sudoku-board.vala b/src/sudoku-board.vala
new file mode 100644
index 0000000..1a85260
--- /dev/null
+++ b/src/sudoku-board.vala
@@ -0,0 +1,512 @@
+using Gee;
+
+public class SudokuBoard
+{
+ /* 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 */
+ 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 */
+ private bool[,,] possible_in_block; /* if specific value is possible in specific block */
+
+ /* Number of rows in one block */
+ private int _block_rows;
+ public int block_rows
+ {
+ get { return _block_rows; }
+ }
+
+ /* Number of columns in one block */
+ private int _block_cols;
+ public int block_cols
+ {
+ get { return _block_cols; }
+ }
+
+ /* Number of rows in board */
+ private int _rows;
+ public int rows
+ {
+ get { return _rows; }
+ }
+
+ /* Number of columns in board */
+ private int _cols;
+ public int cols
+ {
+ get { return _cols; }
+ }
+
+ /* Maximum possible val on board. 9 for 3x3 sudoku*/
+ public int max_val
+ {
+ get { return block_rows * block_cols; }
+ }
+
+ public bool broken
+ {
+ get { return broken_coords.size != 0; }
+ }
+
+ /* the number of filled squares on the board */
+ private int _filled;
+ public int filled
+ {
+ get { return _filled; }
+ }
+
+ public bool complete
+ {
+ get { return _filled == _cols * _rows && !broken; }
+ }
+
+ public signal void completed ();
+
+ /* The set of coordinates on the board which are invalid */
+ public Gee.Set<Coord?> broken_coords;
+
+ /* The list of coordinates for each column on the board */
+ public Gee.List<Gee.List<Coord?>> coords_for_col;
+
+ /* The list of coordinates for each row on the board */
+ public Gee.List<Gee.List<Coord?>> coords_for_row;
+
+ /* The map from the coordinate of a box, to the list of coordinates in that box, for each box on the
board */
+ public Map<Coord?, Gee.List<Coord?>> coords_for_block;
+
+ public SudokuBoard (int block_rows = 3, int block_cols = 3)
+ {
+ _rows = _cols = block_rows * block_cols;
+ _block_rows = block_rows;
+ _block_cols = block_cols;
+ cells = new int[_rows, _cols];
+ is_fixed = new bool[_rows, _cols];
+ possible_in_row = new bool[_rows, _cols];
+ possible_in_col = new bool[_cols, _rows];
+ possible_in_block = new bool[_block_rows, _block_cols, _block_rows * _block_cols];
+
+ for (var l1 = 0; l1 < _rows; l1++)
+ {
+ for (var l2 = 0; l2 < _cols; l2++)
+ {
+ cells[l1, l2] = 0;
+ is_fixed[l1, l2] = false;
+ possible_in_row[l1, l2] = true;
+ possible_in_col[l2, l1] = true;
+ }
+ }
+ for (var l1 = 0; l1 < _block_rows; l1++)
+ {
+ for (var l2 = 0; l2 < _block_cols; l2++)
+ {
+ for (var l3 = 0; l3 < max_val; l3++)
+ possible_in_block[l1, l2, l3] = true;
+ }
+ }
+
+ broken_coords = new HashSet<Coord?>((GLib.HashFunc) Coord.hash, (GLib.EqualFunc) Coord.equal);
+
+ coords_for_col = new ArrayList<Gee.List<Coord?>> ();
+ for (int col = 0; col < _cols; col++)
+ {
+ coords_for_col.add (new ArrayList<Coord?> ((GLib.EqualFunc) Coord.equal));
+ for (int row = 0; row < _rows; row++)
+ {
+ coords_for_col.get (col).add (Coord(row, col));
+ }
+ coords_for_col[col] = coords_for_col[col].read_only_view;
+ }
+ coords_for_col = coords_for_col.read_only_view;
+
+ coords_for_row = new ArrayList<Gee.List<Coord?>> ();
+ for (int row = 0; row < _rows; row++)
+ {
+ coords_for_row.add (new ArrayList<Coord?> ((GLib.EqualFunc) Coord.equal));
+ for (int col = 0; col < _cols; col++)
+ {
+ coords_for_row.get (row).add (Coord(row, col));
+ }
+ coords_for_row[row] = coords_for_row[row].read_only_view;
+ }
+ coords_for_row = coords_for_row.read_only_view;
+
+ coords_for_block = new HashMap<Coord?, Gee.List<Coord?>> ((GLib.HashFunc) Coord.hash,
(GLib.EqualFunc) Coord.equal);
+ for (int col = 0; col < _block_cols; col++)
+ {
+ for (int row = 0; row < _block_rows; row++)
+ {
+ coords_for_block.set (Coord(row, col), new ArrayList<Coord?> ((GLib.EqualFunc) Coord.equal));
+ }
+ }
+ for (int col = 0; col < _cols; col++)
+ {
+ for (int row = 0; row < _rows; row++)
+ {
+ coords_for_block.get(Coord(row / _block_rows, col / _block_cols)).add(Coord(row, col));
+ }
+ }
+ for (int col = 0; col < _block_cols; col++)
+ {
+ for (int row = 0; row < _block_rows; row++)
+ {
+ coords_for_block[Coord(row, col)] = coords_for_block[Coord(row, col)].read_only_view;
+ }
+ }
+ coords_for_block = coords_for_block.read_only_view;
+ }
+
+ public SudokuBoard clone ()
+ {
+ SudokuBoard board = new SudokuBoard (_block_rows , _block_cols);
+ board.cells = cells;
+ board.is_fixed = is_fixed;
+ board.possible_in_row = possible_in_row;
+ board.possible_in_col = possible_in_col;
+ board.possible_in_block = possible_in_block;
+ board._filled = _filled;
+ board.broken_coords.add_all (broken_coords);
+
+ return board;
+ }
+
+ public void set_from_string (string s, string delimiter = "", string empty_value = "0")
+ {
+ //stdout.printf("Processing %s\n", s);
+
+ int number_of_cells = _cols * _rows;
+
+ string[] cells = s.split (delimiter, number_of_cells);
+
+ //stdout.printf("Cells %d %d\n", number_of_cells, cells.length);
+ for (int i = 0; i < number_of_cells; i++)
+ {
+ string cell = cells[i];
+ //stdout.printf("Cell %d: %s\n", i, cell);
+
+ if (cell != empty_value)
+ {
+ int val = int.parse(cell);
+ //stdout.printf("Cell val: %d\n", val);
+
+ assert (val >= 1 && val <= max_val);
+
+ insert (i / _cols, i % _cols, val, true);
+ }
+ }
+ }
+
+ public bool is_possible (int row, int col, int val)
+ {
+ val--;
+ return (possible_in_row[row, val] && possible_in_col[col, val] && possible_in_block [row /
_block_cols, col / _block_rows, val]);
+ }
+
+ public int count_possibilities (int row, int col)
+ {
+ return get_possibilities(row, col).length;
+ }
+
+ public int[] get_possibilities (int row, int col)
+ {
+ if (cells [row, col] != 0)
+ return new int[0];
+
+ var possibilities = new int[9];
+ var count = 0;
+
+ for (var l = 1; l <= max_val; l++)
+ {
+ if (is_possible (row, col, l)) {
+ possibilities[count] = l;
+ count++;
+ }
+ }
+ return possibilities[0:count];
+ }
+
+ public bool[] get_possibilities_as_bool_array (int row, int col)
+ {
+ var possibilities = new bool[max_val];
+
+ for (var l = 1; l <= max_val; l++)
+ {
+ possibilities[l - 1] = is_possible (row, col, l);
+ }
+
+ return possibilities;
+ }
+
+ public Coord get_block_for(int row, int col)
+ {
+ return Coord(row / _block_rows, col / _block_cols);
+ }
+
+ public void insert (int row, int col, int val, bool is_fixed = false)
+ {
+ /* This should not happen when coded properly ;) */
+ assert (val > 0);
+ assert (val <= max_val);
+
+ /* Cant insert in to a fixed cell, unless you know what you are doing */
+ if (!is_fixed)
+ assert (!this.is_fixed[row, col]);
+
+ // If the cell has a previous value, remove it before continuing
+ if (cells[row, col] != 0)
+ remove(row, col, is_fixed);
+
+ cells[row, col] = val;
+ this.is_fixed[row, col] = is_fixed;
+ _filled++;
+
+ if (!possible_in_row[row, val - 1]) // If val was not possible in this row
+ {
+ mark_breakages_for(coords_for_row[row], val); // Mark the breakages
+ }
+
+ if (!possible_in_col[col, val - 1]) // If val was not possible in this col
+ {
+ mark_breakages_for(coords_for_col[col], val); // Mark the breakages
+ }
+
+ if (!possible_in_block[row / _block_cols, col / _block_rows, val - 1]) // If val was not possible in
this block
+ {
+ mark_breakages_for(coords_for_block[Coord(row / _block_cols, col / _block_rows)], val); // Mark
the breakages
+ }
+
+ // Then just mark it as not possible
+ val--;
+ possible_in_row[row, val] = false;
+ possible_in_col[col, val] = false;
+ possible_in_block[row / _block_cols, col / _block_rows, val] = false;
+
+ if (complete)
+ completed();
+ }
+
+ public void set (int row, int col, int val)
+ {
+ if (val == 0)
+ {
+ remove (row, col);
+ }
+ else if (val > 0 && val <= max_val)
+ {
+ insert (row, col, val);
+ }
+ else
+ {
+ assert_not_reached();
+ }
+ }
+
+ public int get (int row, int col)
+ {
+ return cells[row, col];
+ }
+
+ public void remove (int row, int col, bool is_fixed = false)
+ {
+ /* You can't remove an empty cell */
+ if (cells[row, col] == 0)
+ return;
+
+ /* You can't remove an fixed cell */
+ if (!is_fixed)
+ assert (!this.is_fixed[row, col]);
+
+ int previous_val = cells[row, col];
+ cells[row, col] = 0;
+
+ if (broken_coords.contains(Coord(row, col))) // If this cell was broken
+ {
+ // Remove all the related breakages in the related sets of cells
+ remove_breakages_for(coords_for_row[row], previous_val);
+ remove_breakages_for(coords_for_col[col], previous_val);
+ remove_breakages_for(coords_for_block[Coord(row / _block_rows, col / _block_cols)],
previous_val);
+ broken_coords.remove(Coord(row, col));
+
+ // Re-mark all the breakages,
+ mark_breakages_for(coords_for_row[row], previous_val);
+ mark_breakages_for(coords_for_col[col], previous_val);
+ mark_breakages_for(coords_for_block[Coord(row / _block_rows, col / _block_cols)], previous_val);
+
+ // and update the possibilities accordingly
+ possible_in_row[row, previous_val - 1] = get_occurances(coords_for_row[row], previous_val).size
== 0;
+ possible_in_col[col, previous_val - 1] = get_occurances(coords_for_col[col], previous_val).size
== 0;
+ possible_in_block[row / _block_cols, col / _block_rows, previous_val - 1] =
get_occurances(coords_for_block[Coord(row / _block_rows, col / _block_cols)], previous_val).size == 0;
+ }
+ else // Not previously broken, so just mark as a possible value
+ {
+ previous_val--;
+
+ possible_in_row[row, previous_val] = true;
+ possible_in_col[col, previous_val] = true;
+ possible_in_block[row / _block_cols, col / _block_rows, previous_val] = true;
+ }
+
+ _filled--;
+ }
+
+ public Set<Coord?> get_occurances(Gee.List<Coord?> coords, int val)
+ {
+ Set<Coord?> occurances = new HashSet<Coord?>((GLib.HashFunc) Coord.hash, (GLib.EqualFunc)
Coord.equal);
+ foreach (Coord coord in coords)
+ {
+ if (cells[coord.row, coord.col] == val) {
+ occurances.add (coord);
+ }
+ }
+ return occurances;
+ }
+
+ public bool row_contains(int row, int val)
+ {
+ return get_occurances(coords_for_row[row], val).size != 0;
+ }
+
+ public bool col_contains(int col, int val)
+ {
+ return get_occurances(coords_for_col[col], val).size != 0;
+ }
+
+ public bool block_contains(Coord block, int val)
+ {
+ return get_occurances(coords_for_block[block], val).size != 0;
+ }
+
+ private void remove_breakages_for(Gee.List<Coord?> coords, int val)
+ {
+ foreach (Coord coord in coords)
+ {
+ if (cells[coord.row, coord.col] == val && broken_coords.contains(coord)) {
+ broken_coords.remove(coord);
+ }
+ }
+ }
+
+ /* returns if val is possible in coords */
+ private void mark_breakages_for(Gee.List<Coord?> coords, int val)
+ {
+ Set<Coord?> occurances = get_occurances(coords, val);
+ if (occurances.size != 1)
+ {
+ broken_coords.add_all(occurances);
+ }
+ }
+
+ public void to_initial_state ()
+ {
+ for (var l1 = 0; l1 < _rows; l1++)
+ {
+ for (var l2 = 0; l2 < _cols; l2++)
+ {
+ if (!is_fixed[l1, l2])
+ remove (l1, l2);
+ }
+ }
+ }
+
+ public void print (int indent = 0) {
+ for (var l1 = 0; l1 < 9; l1++)
+ {
+ for (int i = 0; i < indent; i++)
+ {
+ stdout.printf(" ");
+ }
+ for (var l2 = 0; l2 < 9; l2++)
+ {
+ if (cells[l1,l2] != 0)
+ stdout.printf ("%d ", cells[l1,l2]);
+ else
+ stdout.printf (" ");
+ }
+ stdout.printf ("\n");
+ }
+ stdout.flush ();
+ }
+
+ public void get_string () {
+ stdout.printf ("[ ");
+ for (var l1 = 0; l1 < 9; l1++)
+ {
+ stdout.printf ("[ ");
+ for (var l2 = 0; l2 < 9; l2++)
+ {
+ stdout.printf ("%d", cells[l1,l2]);
+ if (l2 != 8)
+ stdout.printf (",");
+ }
+ stdout.printf (" ]");
+ if (l1 != 8)
+ stdout.printf (",");
+ }
+ stdout.printf (" ]");
+ }
+
+ public int[,] get_cells()
+ {
+ return cells;
+ }
+
+ public HashMap<Coord?, ArrayList<int>> calculate_open_squares () {
+ var possibilities = new HashMap<Coord?, ArrayList<int>> ((GLib.HashFunc) Coord.hash,
(GLib.EqualFunc) Coord.equal);
+ for (var l1 = 0; l1 < _rows; l1++)
+ {
+ for (var l2 = 0; l2 < _cols; l2++)
+ {
+ if (cells[l1, l2] == 0)
+ {
+ ArrayList<int> possArrayList = new ArrayList<int> ();
+ int[] possArray = get_possibilities (l1, l2);
+ foreach (int i in possArray) {
+ possArrayList.add (i);
+ }
+ possibilities[Coord(l1, l2)] = possArrayList;
+ }
+ }
+ }
+ return possibilities;
+ }
+}
+
+public struct Coord
+{
+ public int row;
+ public int col;
+
+ public Coord(int row, int col)
+ {
+ this.row = row;
+ this.col = col;
+ }
+
+ public static int hash (Coord coord) {
+ return (coord.row * 33) ^ coord.col;
+ }
+
+ public static bool equal (Coord a, Coord b) {
+ return ((a.row == b.row) && (a.col == b.col));
+ }
+}
+
+public struct Cell
+{
+ public Coord coord;
+ public int val;
+
+ public Cell(Coord coord, int val)
+ {
+ this.coord = coord;
+ this.val = val;
+ }
+
+ public static int hash (Cell cell) {
+ return (Coord.hash(cell.coord) * 33) ^ cell.val;
+ }
+
+ public static bool equal (Cell a, Cell b) {
+ return (Coord.equal(a.coord, b.coord) && (a.val == b.val));
+ }
+}
diff --git a/src/sudoku-game.vala b/src/sudoku-game.vala
new file mode 100644
index 0000000..5558ed0
--- /dev/null
+++ b/src/sudoku-game.vala
@@ -0,0 +1,156 @@
+using Gee;
+
+public class SudokuGame
+{
+ public SudokuBoard board;
+ public GLib.Timer timer;
+
+ private struct UndoItem
+ {
+ public int row;
+ public int col;
+ public int val;
+ }
+
+ public signal void cell_changed (int row, int col, int old_val, int new_val);
+
+ private SList<UndoItem?> undostack;
+ private SList<UndoItem?> redostack;
+
+ public SudokuGame (SudokuBoard board)
+ {
+ this.board = board;
+ timer = new Timer();
+ undostack = null;
+ redostack = null;
+ }
+
+ public void insert (int row, int col, int val)
+ {
+ var old_val = board[row, col];
+ update_undo (row, col, old_val, val);
+ board.insert (row, col, val);
+ cell_changed (row, col, old_val, val);
+ }
+
+ public void remove (int row, int col)
+ {
+ int old_val = board[row, col];
+ update_undo (row, col, old_val, 0);
+ board.remove (row, col);
+ cell_changed (row, col, old_val, 0);
+ }
+
+ public void undo ()
+ {
+ apply_stack (ref undostack, ref redostack);
+ }
+
+ public void redo ()
+ {
+ apply_stack (ref redostack, ref undostack);
+ }
+
+ public void reset ()
+ {
+ timer.reset();
+ var count = board.filled;
+ for (var l1 = 0; l1 < board.rows; l1++)
+ {
+ for (var l2 = 0; l2 < board.cols; l2++)
+ {
+ if (!board.is_fixed[l1, l2])
+ remove (l1, l2);
+ }
+ }
+ count -= board.filled;
+ add_to_stack (ref undostack, -1, -1, count);
+ }
+
+ public void hint (ref int row, ref int col)
+ {
+ var total_pos = new int [board.rows, board.cols];
+ var min = board.max_val + 1;
+ var count = 0;
+ for (var l1 = 0; l1 < board.rows; l1++)
+ {
+ for (var l2 = 0; l2 < board.cols; l2++)
+ {
+ total_pos [l1, l2] = board.count_possibilities (l1, l2);
+ if (total_pos [l1, l2] < min && total_pos [l1, l2] > 0)
+ {
+ min = total_pos [l1, l2];
+ count = 0;
+ }
+ if (total_pos [l1, l2] == min)
+ {
+ count++;
+ }
+ }
+ }
+ if (count == 0)
+ return;
+ count = Random.int_range (1, count + 1);
+ for (var l1 = 0; l1 < board.rows; l1++)
+ {
+ for (var l2 = 0; l2 < board.cols; l2++)
+ {
+ if (total_pos[l1, l2] == min)
+ {
+ count--;
+ if (count == 0)
+ {
+ row = l1;
+ col = l2;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ public void cell_changed_cb (int row, int col, int old_val, int new_val)
+ {
+ cell_changed (row, col, old_val, new_val);
+ }
+
+ public void update_undo (int row, int col, int old_val, int new_val)
+ {
+ add_to_stack (ref undostack, row, col, old_val);
+ redostack = null;
+ }
+
+ private void add_to_stack (ref SList<UndoItem?> stack, int r, int c, int v)
+ {
+ UndoItem step = { r, c, v };
+ stack.prepend (step);
+ }
+
+ private void apply_stack (ref SList<UndoItem?> from, ref SList<UndoItem?> to)
+ {
+ if (from == null)
+ return;
+
+ /* Undoing change of single cell */
+ if (from.data.row >= 0 && from.data.col >= 0)
+ {
+ int old_val = board [from.data.row, from.data.col];
+ add_to_stack (ref to, from.data.row, from.data.col, old_val);
+ board.remove (from.data.row, from.data.col);
+ if (from.data.val != 0) {
+ board.insert (from.data.row, from.data.col, from.data.val);
+ }
+ cell_changed (from.data.row, from.data.col, old_val, from.data.val);
+ from.remove (from.data);
+ }
+ /* Undoing reset action */
+ else
+ {
+ var num = from.data.val;
+ from.remove (from.data);
+ for (var l = 0; l < num; l++)
+ apply_stack (ref from, ref to);
+ add_to_stack (ref to, -1, -1, num);
+ }
+ }
+}
diff --git a/src/sudoku-generator.vala b/src/sudoku-generator.vala
new file mode 100644
index 0000000..2bc635b
--- /dev/null
+++ b/src/sudoku-generator.vala
@@ -0,0 +1,413 @@
+public class SudokuGenerator
+{
+ public int clues = 40;
+
+ public SudokuBoard start_board = new SudokuBoard(3, 3);
+ private SudokuBoard temp_start_board = new SudokuBoard(3, 3);
+
+ public struct RatedSudoku {
+
+ public SudokuBoard board;
+ public DifficultyRating diff;
+
+ public RatedSudoku(SudokuBoard board, DifficultyRating diff)
+ {
+ this.board = board;
+ this.diff = diff;
+ }
+ }
+
+ public SudokuGenerator() {
+ int solution = 0;
+ generate_start_board(-1, -1, -1, -1, ref solution);
+ }
+
+ public static void gen_stats ()
+ {
+ for (int i = 22; i < 77; i++)
+ {
+ stdout.printf("%d, ", i);
+ for (int repeat = 0; repeat < 50; repeat++)
+ {
+ stdout.printf("repeat %d\n", repeat);
+ SudokuGenerator gen = new SudokuGenerator();
+ gen.clues = i;
+
+
+ RatedSudoku? rated_sudoku = gen.make_unique_puzzle(Random.int_range(0, 4), true);
+ while (true) {
+ if (rated_sudoku == null)
+ gen.make_unique_puzzle(Random.int_range(0, 4), true);
+ else
+ break;
+ }
+
+ /*SudokuBoard board = gen.make_symmetric_puzzle (Random.int_range(0, 4));
+ while (true)
+ {
+ SudokuBoard test_board = board.clone ();
+
+ SudokuSolver test_solver = new SudokuSolver(ref test_board);
+ if (test_solver.quick_has_unique_solution ())
+ break;
+ }*/
+
+ stdout.printf("%f, ", rated_sudoku.diff.rating);
+ }
+ stdout.printf("\n");
+ }
+ }
+
+ public SudokuBoard generate (float[] difficulty_range, bool symmetric = true)
+ {
+ int count = 0;
+
+ do {
+ float skew = 0.0f;
+
+ clues = get_cells_for ((float) Random.double_range((double) difficulty_range[0], (double)
difficulty_range[1]));
+
+ if (clues < 17)
+ clues = 17;
+
+ if (clues > 60)
+ clues = 60;
+
+ if (count >= 10 && count % 10 == 0)
+ {
+ int solution = 0;
+ generate_start_board(-1, -1, -1, -1, ref solution);
+ }
+
+ //stdout.printf("Generating with %d clues\n", clues);
+
+ SudokuBoard puzzle;
+
+ if (symmetric) {
+ RatedSudoku rated_sudoku = make_unique_puzzle (GLib.Random.int_range(0, 3));
+
+ if (rated_sudoku.board != null && rated_sudoku.diff.in_range(difficulty_range))
+ return rated_sudoku.board;
+ } else {
+ puzzle = make_puzzle_by_boxes (skew);
+ }
+
+ count++;
+ } while (count < 100);
+
+ stdout.printf ("Puzzle not found..\n");
+
+ return new SudokuBoard(3, 3);
+ }
+
+ private int get_cells_for (float difficulty)
+ {
+ return (int) ((-22.3275863216 * difficulty) + 44.9652207631);
+ }
+
+ private void generate_start_board (int row, int col, int no, int filled, ref int solution)
+ {
+ if (filled == -1)
+ filled = temp_start_board.filled;
+
+ if (filled == temp_start_board.rows * temp_start_board.cols)
+ {
+ solution++;
+ for (var y = 0; y < 9; y++)
+ {
+ for (var x = 0; x < 9; x++)
+ {
+ start_board.insert (x, y, temp_start_board[x, y]);
+ }
+ }
+ return;
+ }
+
+ if (row == -1 || col == -1)
+ {
+ for (var l1 = 0; l1 < temp_start_board.rows; l1++)
+ {
+ for (var l2 = 0;l2 < temp_start_board.cols; l2++)
+ {
+ if (temp_start_board[l1, l2] == 0)
+ {
+ generate_start_board (l1, l2, no, filled, ref solution);
+ return;
+ }
+ }
+ }
+ }
+
+ if (no == -1)
+ {
+ bool[] values_tried = new bool[temp_start_board.max_val];
+ int values_tried_count = 0;
+ while (values_tried_count < temp_start_board.max_val)
+ {
+ // Find a value that has not been tried yet
+ int l1 = -1;
+ do {
+ l1 = GLib.Random.int_range(0, temp_start_board.max_val) + 1;
+ } while (values_tried[l1]);
+
+ values_tried_count++;
+
+ if (temp_start_board.is_possible (row, col, l1))
+ {
+ generate_start_board (row, col, l1, filled, ref solution);
+
+ if (solution > 0)
+ return;
+ }
+ }
+ return;
+ }
+
+ temp_start_board.insert (row, col, no);
+ generate_start_board (-1, -1, -1, filled + 1, ref solution);
+ temp_start_board.remove (row, col);
+ }
+
+ public SudokuBoard make_symmetric_puzzle (int line)
+ {
+ SudokuBoard new_puzzle = new SudokuBoard(3, 3);
+
+ while (new_puzzle.filled < clues) {
+ int row = GLib.Random.int_range(0, 9);
+ int col = GLib.Random.int_range(0, 9);
+
+ if (!new_puzzle.is_fixed[row, col]) {
+
+ Coord reflection = reflect(row, col, line);
+
+ new_puzzle.insert(row, col, start_board[row, col], true);
+
+ if (reflection.row == row && reflection.col == col)
+ continue; // Skip as the reflection is itself
+
+ new_puzzle.insert(reflection.row, reflection.col, start_board[reflection.row,
reflection.col], true);
+ }
+
+ }
+ return new_puzzle;
+ }
+
+ private Coord reflect(int row, int col, int line)
+ {
+ if (line == 0) // Vertical, through the middle
+ {
+ return Coord(row, 8 - col);
+ }
+ else if (line == 1) // From bottom right to top left
+ {
+ return Coord(8 - col, 8 - row);
+ }
+ else if (line == 2) // Horizontal through the middle
+ {
+ return Coord(8 - row, col);
+ }
+ else if (line == 3) // From top left to bottom right
+ {
+ return Coord(8 - row, 8 - col);
+ }
+ else
+ {
+ return Coord(row, col);
+ }
+ }
+
+ /* Make a puzzle paying attention to evenness of clue
+ distribution.
+
+ If skew_by is 0, we distribute our clues as evenly as possible
+ across boxes. If skew by is 1.0, we make the distribution of
+ clues as uneven as possible. In other words, if we had 27
+ boxes for a 9x9 grid, a skew_by of 0 would put exactly 3 clues
+ in each 3x3 grid whereas a skew_by of 1.0 would completely
+ fill 3 3x3 grids with clues.
+
+ We believe this skewing may have something to do with how
+ difficult a puzzle is to solve. By toying with the ratios,
+ this method may make it considerably easier to generate
+ difficult or easy puzzles.
+ */
+ public SudokuBoard make_puzzle_by_boxes (float skew_by = 0.0f) {
+ SudokuBoard new_puzzle = new SudokuBoard(3, 3);
+
+ // Number of total boxes
+ int nboxes = 9; // TODO: Fix
+ // If no max is given, we calculate one based on our skew_by --
+ // a skew_by of 1 will always produce full squares, 0 will
+ // produce the minimum fullness, and between between in
+ // proportion to its betweenness.
+ int max_squares = clues / nboxes;
+ max_squares += (int) ((nboxes - max_squares) * skew_by);
+ int clued = 0;
+ // nclues will be a list of the number of clues we want per
+ // box, counting from the top left, along, and wraping.
+ // _ _ _
+ // |0|1|2|
+ // |3|4|5|
+ // |6|7|8|
+ // - - -
+
+ int[] nclues = new int[nboxes];
+ for (int n = 0; n < nboxes; n++) {
+ // Make sure we'll have enough clues to fill our target
+ // number, regardless of our calculation of the current max
+
+ // TODO: The following code (directly translated from python) does nothing?
+ // int minimum = (this.clues - clued) / (nboxes - n);
+ //if (max_squares < minimum) {
+ // cls = minimum
+ //} else {
+ // cls = int(max_squares)
+ //}
+ int clues = max_squares;
+ if (clues > (this.clues - clued)) {
+ clues = this.clues - clued;
+ }
+ nclues[n] = clues;
+
+ clued += clues;
+ if (skew_by != 0) {
+ // Reduce our number of squares proportionally to
+ // skewiness.
+ max_squares = (int) GLib.Math.round(max_squares * skew_by);
+ }
+ }
+
+ // shuffle ourselves... probably badly (TODO)
+ for (int i=0; i<nboxes; i++) {
+ int from = GLib.Random.int_range(0, nboxes);
+ int to = GLib.Random.int_range(0, nboxes);
+
+ int temp = nclues[to];
+ nclues[to] = nclues[from];
+ nclues[from] = temp;
+ }
+
+ for (int i = 0; i < nboxes; i++) {
+ while (nclues[i] > 0) {
+ int base_x = (i % new_puzzle.block_cols) * new_puzzle.block_cols;
+ int base_y = (i / new_puzzle.block_rows) * new_puzzle.block_rows;
+
+ int x = GLib.Random.int_range(base_x, base_x + new_puzzle.block_rows);
+ int y = GLib.Random.int_range(base_y, base_y + new_puzzle.block_cols);
+
+ if (!new_puzzle.is_fixed[x, y]) {
+ new_puzzle.insert(x, y, start_board[x, y], true);
+ nclues[i]--;
+ }
+ }
+ }
+ return new_puzzle;
+ }
+
+ public RatedSudoku? make_unique_puzzle (int line = -1, bool strict_number_of_clues = false)
+ {
+ SudokuBoard board;
+ DifficultyRating diff;
+
+ if (line != -1)
+ {
+ int old_clues = clues;
+ clues = 4;
+ board = make_symmetric_puzzle (line);
+ clues = old_clues;
+ //stdout.printf("made a board with %d clues\n", board.filled);
+ }
+ else
+ {
+ board = make_puzzle_by_boxes ();
+ }
+
+ while (true)
+ {
+ SudokuBoard solved_board = board.clone ();
+ SudokuRater solver = new SudokuRater(ref solved_board);
+ if (solver.has_unique_solution())
+ {
+ //stdout.printf("unique solution found\n");
+ diff = solver.get_difficulty();
+ break;
+ }
+ //stdout.printf("no unique solution found\n");
+
+ //board.print ();
+
+ //stdout.printf("possible solutions\n");
+
+ /*SudokuBoard solved_board_two = board.clone ();
+ SudokuRater solver_two = new SudokuRater(ref solved_board_two);
+
+ int solutions = solver_two.quick_count_solutions ();
+ if (solutions < 5)
+ {
+ foreach (SudokuBoard solution in solver_two)
+ {
+ solution.print ();
+ stdout.printf("\n");
+ }
+ }
+ else
+ {
+ stdout.printf("have %d solutions\n", solutions);
+ }*/
+
+ for (int i = 0; i < solver.breadcrumbs.size; i++)
+ {
+ Guess guess = solver.breadcrumbs[i];
+ //stdout.printf("crumb (%d, %d) %d\n", guess.col, guess.row, guess.val);
+ }
+
+ // Otherwise...
+ Guess crumb = solver.breadcrumbs[solver.breadcrumbs.size - 1];
+ // stdout.printf("got crumb (%d, %d) %d\n", crumb.col, crumb.row, crumb.val);
+
+ board.insert (crumb.row, crumb.col, start_board[crumb.row, crumb.col], true);
+
+ Coord reflection = reflect(crumb.row, crumb.col, line);
+
+ board.insert (reflection.row, reflection.col, start_board[reflection.row, reflection.col], true);
+
+ //stdout.printf("Not unique, filling in (%d, %d) and (%d, %d)\n", crumb.row, crumb.col,
reflection.row, reflection.col);
+
+ if (strict_number_of_clues && board.filled > clues)
+ {
+ //stdout.printf("failed to get the number of clues, instead got %d\n", board.filled);
+ return null;
+ }
+ //stdout.printf("trying again\n");
+ }
+
+ // make sure we have the proper number of clues
+ if (strict_number_of_clues)
+ {
+ //stdout.printf("checking the number of clues\n");
+ bool changed = false;
+ while (board.filled < clues)
+ {
+ int row = -1;
+ int col = -1;
+ do
+ {
+ row = Random.int_range(0, 9);
+ col = Random.int_range(0, 9);
+ }
+ while (board[row, col] == 0);
+
+ board.insert (row, col, start_board[row, col], true);
+
+ Coord reflection = reflect(row, col, line);
+ board.insert (reflection.row, reflection.col, start_board[reflection.row, reflection.col],
true);
+
+ changed = true;
+ }
+ if (changed)
+ diff = new SudokuRater(ref board).get_difficulty();
+ }
+ //stdout.printf("returning rated puzzle\n");
+ return RatedSudoku(board, diff);
+ }
+}
diff --git a/src/sudoku-solver.vala b/src/sudoku-solver.vala
new file mode 100644
index 0000000..f732ce3
--- /dev/null
+++ b/src/sudoku-solver.vala
@@ -0,0 +1,872 @@
+using Gee;
+
+protected errordomain SudokuError {
+ UNSOLVABLE_PUZZLE,
+ CONFLICT_ERROR,
+ ALREADY_SET_ERROR
+}
+
+public class SudokuSolver
+{
+ protected SudokuBoard board;
+
+ private ParallelDict conflicts;
+ protected GuessList guesses;
+ public BreadcrumbTrail breadcrumbs;
+ protected int backtraces = 0;
+ private Guess current_guess;
+
+ private bool count_solutions = false;
+ private int break_at = 100;
+
+ private ArrayList<Guess> trail = new ArrayList<Guess> ();
+ private ArrayList<string> trailDetails = new ArrayList<string> ();
+
+ private int debug_indent = 0;
+
+ protected bool solved;
+
+ public SudokuSolver (ref SudokuBoard board)
+ {
+ this.board = board;
+ this.conflicts = new ParallelDict();
+ this.guesses = new GuessList();
+ this.breadcrumbs = new BreadcrumbTrail();
+ this.solved = false;
+ }
+
+ /* Check if current SudokuBoard has at least one solution or not */
+ public bool quick_has_solution ()
+ {
+ int solutions = 0;
+ count_solutions = false;
+ quick_solve (-1, -1, -1, -1, ref solutions);
+ return (solutions > 0);
+ }
+
+ public bool quick_has_unique_solution ()
+ /*Check if current SudokuBoard has unique solution or not*/
+ {
+ int solutions = 0;
+ count_solutions = false;
+ quick_solve (-1, -1, -1, -1, ref solutions);
+ return (solutions == 1);
+ }
+
+ /* Check if current SudokuBoard has more than one solution or not */
+ public bool quick_has_many_solution ()
+ {
+ int solutions = 0;
+ count_solutions = false;
+ quick_solve (-1, -1, -1, -1, ref solutions);
+ return (solutions > 1);
+ }
+
+ /* Check if current SudokuBoard has more than one solution or not */
+ public int quick_count_solutions (int break_at = 100)
+ {
+ int solutions = 0;
+ count_solutions = true;
+ this.break_at = break_at;
+ quick_solve (-1, -1, -1, -1, ref solutions);
+ return solutions;
+ }
+
+ private void quick_solve (int row, int col, int no, int filled, ref int solution)
+ {
+ if (filled == -1)
+ filled = board.filled;
+
+ if (filled == board.rows * board.cols)
+ {
+ solution++;
+ return;
+ }
+
+ if (row == -1 || col == -1)
+ {
+ for (var l1 = 0; l1 < board.rows; l1++)
+ {
+ for (var l2 = 0;l2 < board.cols; l2++)
+ {
+ if (board[l1, l2] == 0)
+ {
+ quick_solve (l1, l2, no, filled, ref solution);
+ return;
+ }
+ }
+ }
+ }
+
+ if (no == -1)
+ {
+ for (var l1 = 1; l1 <= board.max_val; l1++)
+ {
+ if (board.is_possible (row, col, l1))
+ {
+ quick_solve (row, col, l1, filled, ref solution);
+ /* Break at solutions == 2 as we don't need exact count of possible solutions */
+ if ((!count_solutions && solution > 1) || (solution > break_at))
+ return;
+ }
+ }
+ return;
+ }
+
+ board.insert (row, col, no);
+ quick_solve (-1, -1, -1, filled + 1, ref solution);
+ board.remove (row, col);
+ }
+
+
+
+ public Iterator iterator ()
+ {
+ return new Iterator(this);
+ }
+
+ public bool has_unique_solution ()
+ {
+ Iterator sf = iterator ();
+
+ if (sf.next() == true && sf.next() == false)
+ return true;
+ else
+ return false;
+ }
+
+ public class Iterator {
+ private SudokuSolver solver;
+ private SudokuBoard solution;
+
+ public Iterator(SudokuSolver solver) {
+ this.solver = solver;
+ }
+
+ public bool next() {
+ if (!solver.solved)
+ {
+ solver.auto_fill ();
+ try
+ {
+ while (!solver.guess_least_open_square());
+ }
+ catch (SudokuError e)
+ {
+ solution = null;
+ return false;
+ }
+
+ solver.solved = true;
+ solution = solver.board;
+
+ return true;
+ } else {
+ while (solver.breadcrumbs.size != 0)
+ {
+ solver.unwrap_guess(solver.breadcrumbs[solver.breadcrumbs.size -1]);
+ try
+ {
+ while (!solver.guess_least_open_square());
+ }
+ catch (SudokuError e)
+ {
+ solution = null;
+ return false;
+ }
+ solution = solver.board;
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ public SudokuBoard? get() {
+ return solution;
+ }
+ }
+
+ /*
+ * Fills the board looking at the possibilities, then starts guessing using guess_least_open_square
+ */
+ public SudokuBoard solve () {
+ auto_fill();
+ try {
+ while (!guess_least_open_square());
+ } catch (SudokuError e) {
+
+ }
+ solved = true;
+ return board;
+ }
+
+ /*
+ * This method uses fill_must_fills, then fill_deterministically to fill the board
+ */
+ private ArrayList<Cell?> auto_fill () {
+ ArrayList<Cell?> changed = new ArrayList<Cell?> ();
+ try {
+ changed.add_all (fill_must_fills ());
+ } catch ( SudokuError e ) {
+ return changed;
+ }
+ changed.add_all (fill_deterministically ());
+ return changed;
+ }
+
+ /*
+ * This method looks at each cell on the board, and if it has one possible value, inserts it
+ */
+ protected ArrayList<Cell?> fill_deterministically () {
+ HashMap<Coord?, ArrayList<int>> poss = board.calculate_open_squares ();
+
+ ArrayList<Cell?> changed = new ArrayList<Cell?> ();
+ foreach (Coord coord in poss.keys) {
+ ArrayList<int> choices = poss[coord];
+
+ if (choices.size != 1)
+ continue;
+
+ int val = choices[0];
+
+ insert (coord.row, coord.col, val);
+ changed.add ( Cell(coord, val));
+ }
+ return changed;
+ }
+
+ /*
+ * This method looks at each set of cells on the board, and then calls fill_must_fills_for on them, it
returns the changes it has made as an array
+ */
+ protected ArrayList<Cell?> fill_must_fills () throws SudokuError {
+ ArrayList<Cell?> changed = new ArrayList<Cell?> ();
+
+ for (int col = 0; col < board.cols; col++) {
+ changed.add_all(fill_must_fills_for(board.coords_for_col.get(col)));
+ }
+
+ for (int row = 0; row < board.rows; row++) {
+ changed.add_all(fill_must_fills_for(board.coords_for_row.get (row)));
+ }
+
+ int blocks_across = board.cols / board.block_cols;
+ int blocks_down = board.rows / board.block_rows;
+
+ for (int block_down = 0; block_down < blocks_down; block_down++) {
+ for (int block_across = 0; block_across < blocks_across; block_across++) {
+ changed.add_all(fill_must_fills_for(board.coords_for_block.get (Coord(block_across,
block_down))));
+ }
+ }
+ return changed;
+ }
+
+ /*
+ * This method looks at the list of coords its been given, it returns the changes it has made as an array
+ */
+ private ArrayList<Cell?> fill_must_fills_for (Gee.List<Coord?> coords) throws SudokuError {
+ bool skip_set = false;
+ foreach (Coord coord in coords) {
+ if (conflicts.contains(coord)) {
+ skip_set = true;
+ break;
+ }
+ }
+
+ if (skip_set)
+ return new ArrayList<Cell?> ();
+
+ // This list holds the changes that are made to the board
+ ArrayList<Cell?> changed = new ArrayList<Cell?> ((GLib.EqualFunc) Coord.equal);
+
+ // Maps the number needed to a cell that can hold it
+ var needs = new HashMap<int, Coord?> (null, null, (GLib.EqualFunc) Coord.equal);
+ for (int i=1; i <= board.max_val; i++)
+ needs[i] = null;
+
+ // Look at each of the coords in the list
+ foreach (Coord coord in coords) {
+
+ int val = board[coord.row, coord.col];
+ if (val != 0) { // If it contains a value
+ if (needs.has_key(val)) { // If the value that it contains is still down as being needed
+ needs.unset(val); // Remove it
+ }
+ } else { // If its empty
+ // Find out what can go in it
+ int[] possibilities = board.get_possibilities(coord.row, coord.col);
+
+ // For each value it can hold
+ foreach (int possibility in possibilities) {
+ // If its needed
+ if (needs.has_key(possibility)) {
+ if (needs[possibility] == null) { // If it has no candidate, put the current coord in
+ needs[possibility] = coord;
+ } else { // Else, as this value can go in two cells (either coord or
needs[possibility]),
+ // we cant deal with it, so remove it
+ needs.unset(possibility);
+ }
+ }
+ }
+ }
+ }
+
+ if (needs.size != 0) { // If this block needs any values
+ foreach (int n in needs.keys) { // Foreach value n, that this block needs
+ if (needs[n] == null) { // If n is needed, but there is no candidate, panic?!?
+ //stdout.printf("but its null, oh noes\n");
+ throw new SudokuError.UNSOLVABLE_PUZZLE("Missing a %d in\n", n);
+ }
+ else
+ {
+ int val = board[needs[n].row, needs[n].col]; // Get the value currently in the cell
+ // FIXME: Not sure why the val == n is here, the python code is a bit flakey with the
exceptions in add here...
+ if (val == 0 || val == n) { // If this cell is empty
+ insert(needs[n].row, needs[n].col, n); // Insert the value
+ changed.add (Cell(needs[n], n));
+ } else { // Else, we need val in here, but its already occupied?!?
+ //stdout.printf("%d, %d must be two values at once! %d and %d\n", needs[n].col,
needs[n].row, val, n);
+ throw new SudokuError.UNSOLVABLE_PUZZLE("%d, %d must be two values at once! %d and
%d", needs[n].col, needs[n].row, val, n);
+ }
+ }
+ }
+ }
+
+ return changed;
+ }
+
+ /*
+ * Guesses the least open cell (cell with the smallest number of possibilitites) on the board.
+ * It returns true if there are no open squares, or false ...
+ */
+ protected virtual bool guess_least_open_square () throws SudokuError
+ {
+ HashMap<Coord?, ArrayList<int>> poss = board.calculate_open_squares ();
+
+ // if there are no open squares, we're done!
+ if (poss.keys.size == 0) {
+ return true;
+ }
+
+ // Find the square with the least possibilties
+ MapIterator<Coord?, ArrayList<int>> iter = poss.map_iterator ();
+ iter.first ();
+ Coord least_coord = iter.get_key ();
+ ArrayList<int> least_coord_possibilties = iter.get_value ();
+
+ foreach (Coord coord in poss.keys) {
+ if (poss[coord].size > least_coord_possibilties.size)
+ {
+ continue;
+ }
+ else if (poss[coord].size < least_coord_possibilties.size)
+ {
+ least_coord = coord;
+ least_coord_possibilties = poss[coord];
+ }
+ else if (coord.col < least_coord.col)
+ {
+ least_coord = coord;
+ least_coord_possibilties = poss[coord];
+ }
+ else if (coord.col == least_coord.col && coord.row < least_coord.row)
+ {
+ least_coord = coord;
+ least_coord_possibilties = poss[coord];
+ }
+ }
+
+ ArrayList<int> possible_values = new ArrayList<int> ();
+ Guess[] guesses_for_coord = guesses.guesses_for (least_coord.row, least_coord.col);
+
+ // Remove all possibilties already guessed
+ foreach (int possibility in least_coord_possibilties)
+ {
+ bool found = false;
+ foreach (Guess guess in guesses_for_coord)
+ {
+ if (guess.val == possibility)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ possible_values.add (possibility);
+ }
+
+ if (possible_values.size == 0)
+ {
+ if (breadcrumbs.size != 0)
+ {
+ backtraces += 1;
+ unwrap_guess( breadcrumbs.get (breadcrumbs.size - 1) );
+ debug_indent++;
+ return guess_least_open_square();
+ }
+ else
+ {
+ throw new SudokuError.UNSOLVABLE_PUZZLE ("Unsolvable");
+ }
+ }
+
+ // Pick a random number to guess (from the list of possible numbers)
+ int guess = possible_values.get (Random.int_range (0, possible_values.size));
+
+ Guess guess_obj = new Guess(least_coord.row, least_coord.col, guess);
+
+ if (breadcrumbs.size != 0) {
+ breadcrumbs.get (breadcrumbs.size -1).children.add (guess_obj);
+ }
+
+ current_guess = null; // reset (we're tracked via guess.get_child())
+
+ insert (least_coord.row, least_coord.col, guess);
+ current_guess = guess_obj; // (All deterministic additions
+ // get added to our
+ // consequences)
+ guesses.add (guess_obj);
+
+ trail.add (guess_obj);
+ trailDetails.add ("+");
+
+ breadcrumbs.add (guess_obj);
+ ArrayList<Cell?> fills = auto_fill ();
+
+ bool contains_empty = false;
+
+ Collection<ArrayList<int>> possibilties_left = board.calculate_open_squares().values;
+ foreach (ArrayList<int> i in possibilties_left) {
+ if (i.size == 0)
+ contains_empty = true;
+ }
+
+ if (contains_empty) {
+ trailDetails.add ("Guess leaves us with impossible squares.");
+ unwrap_guess (guess_obj);
+ debug_indent++;
+ return guess_least_open_square();
+ }
+
+ return false;
+ }
+
+ private void unwrap_guess (Guess guess)
+ {
+ trail.add (guess);
+ trailDetails.add ("-");
+
+ if (board[guess.row, guess.col] != 0) {
+ board.remove (guess.row, guess.col);
+ }
+ foreach (Coord coord in guess.consequences.keys) {
+ if (board[coord.row, coord.col] != 0) {
+ board.remove (coord.row, coord.col);
+ }
+ }
+
+ foreach (Guess child in guess.children) {
+ unwrap_guess (child);
+ if (child in guesses)
+ guesses.remove (child);
+ }
+
+ if (guess in breadcrumbs) {
+ breadcrumbs.remove (guess);
+ }
+ }
+
+ protected virtual void insert (int row, int col, int val)
+ {
+ if (current_guess != null)
+ current_guess.add_consequence(row, col, val);
+ board.insert (row, col, val);
+ }
+}
+
+class SudokuRater : SudokuSolver {
+
+ private bool guessing;
+ private bool fake_add;
+ private ArrayList<Cell?> fake_additions;
+ private ArrayList<Cell?> add_me_queue;
+ private HashSet<Cell?> filled;
+ private HashMap<int, HashSet<Cell?>> fill_must_fillables;
+ private HashMap<int, HashSet<Cell?>> elimination_fillables;
+ private int tier;
+
+ public SudokuRater (ref SudokuBoard board) {
+ base(ref board);
+ guessing = false;
+ fake_add = false;
+ fake_additions = new ArrayList<Cell?> ();
+ filled = new HashSet<Cell?> ((GLib.HashFunc) Cell.hash, (GLib.EqualFunc) Cell.equal);
+ fill_must_fillables = new HashMap<int, HashSet<Cell?>> ();
+ elimination_fillables = new HashMap<int, HashSet<Cell?>> ();
+ tier = 0;
+ }
+
+ protected override void insert (int row, int col, int val)
+ {
+ if (!fake_add) {
+ if (!guessing) {
+ scan_fillables();
+ foreach (Cell delayed_cell in add_me_queue)
+ {
+ if (board[delayed_cell.coord.row, delayed_cell.coord.col] == 0) {
+ base.insert(delayed_cell.coord.row, delayed_cell.coord.col, delayed_cell.val);
+ }
+ }
+ if (board[row, col] == 0)
+ base.insert(row, col, val);
+ tier += 1;
+ }
+ else
+ {
+ base.insert(row, col, val);
+ }
+ }
+ else
+ {
+ fake_additions.add (Cell(Coord(row, col), val));
+ }
+ }
+
+ private void scan_fillables () {
+ fake_add = true;
+ // this will now tell us how many squares at current
+ // difficulty could be filled at this moment.
+ fake_additions = new ArrayList<Cell?> ();
+ try {
+ fill_must_fills();
+ } catch (SudokuError e) {
+ }
+ fill_must_fillables[tier] = new HashSet<Cell?> ((GLib.HashFunc) Cell.hash, (GLib.EqualFunc)
Cell.equal);
+ foreach (Cell cell in fake_additions) {
+ if (!filled.contains(cell))
+ fill_must_fillables[tier].add (cell);
+ }
+
+ add_me_queue = fake_additions;
+ fake_additions = new ArrayList<Cell?> ();
+
+ try {
+ fill_deterministically();
+ } catch (SudokuError e) {
+ }
+
+ elimination_fillables[tier] = new HashSet<Cell?> ((GLib.HashFunc) Cell.hash, (GLib.EqualFunc)
Cell.equal);
+ foreach (Cell cell in fake_additions) {
+ if (!filled.contains(cell))
+ elimination_fillables[tier].add (cell);
+ }
+
+ filled.add_all(fill_must_fillables[tier]);
+ filled.add_all(elimination_fillables[tier]);
+
+ add_me_queue.add_all(fake_additions);
+ fake_add = false;
+ }
+
+ protected override bool guess_least_open_square () throws SudokuError
+ {
+ guessing = true;
+ return base.guess_least_open_square();
+ }
+
+ public DifficultyRating get_difficulty () {
+ if (!solved)
+ solve();
+ int clues = 0;
+ for (int row = 0; row<board.rows; row++)
+ for (int col = 0; col<board.cols; col++)
+ if (board.is_fixed[row,col])
+ clues++;
+
+ int numbers_added = (board.rows * board.cols) - clues;
+
+ DifficultyRating rating = new DifficultyRating(fill_must_fillables,
+ elimination_fillables,
+ guesses,
+ backtraces,
+ numbers_added);
+ return rating;
+ }
+
+ public static void gen_python_test () {
+ stdout.printf("import sudoku\n\n");
+
+ for (int repeat = 0; repeat < 20; repeat++)
+ {
+ SudokuGenerator gen = new SudokuGenerator();
+ gen.clues = Random.int_range(17, 60);
+
+ SudokuBoard board = gen.make_symmetric_puzzle (Random.int_range(0, 4));
+
+ stdout.printf("diff = sudoku.SudokuRater(");
+ board.get_string ();
+ stdout.printf(").difficulty()\n");
+
+ SudokuRater rater = new SudokuRater(ref board);
+ DifficultyRating diff = rater.get_difficulty ();
+
+ stdout.printf("print diff.value, %f\n\n", diff.rating);
+ }
+ }
+}
+
+public class Guess {
+ private int _row;
+ public int row
+ {
+ get { return _row; }
+ }
+
+ private int _col;
+ public int col
+ {
+ get { return _col; }
+ }
+
+ private int _val;
+ public int val
+ {
+ get { return _val; }
+ }
+
+ public ArrayList<Guess> children;
+
+ public HashMap<Coord?, int> consequences;
+
+ public Guess(int row, int col, int val) {
+ _row = row;
+ _col = col;
+ _val = val;
+ consequences = new HashMap<Coord?, int> ((GLib.HashFunc) Coord.hash, (GLib.EqualFunc) Coord.equal);
+ children = new ArrayList<Guess> ();
+ }
+
+ public void add_consequence (int row, int col, int val)
+ {
+ consequences.set (Coord(row, col), val);
+ }
+}
+
+public class GuessList : ArrayList<Guess> {
+
+ public Guess[] guesses_for (int row, int col) {
+ Guess[] guesses = {};
+
+ foreach (Guess guess in this) {
+ if (guess.row == row && guess.col == col) {
+ guesses += guess;
+ }
+ }
+
+ return guesses;
+ }
+
+ public Guess[] remove_children (Guess guess) {
+ Guess[] removed = {};
+
+ foreach (Guess g in guess.children) {
+ if (this.contains(g)) {
+ removed += g;
+ this.remove(g);
+ }
+ }
+
+ return removed;
+ }
+
+ public Guess[] remove_guesses_for ( int row, int col) {
+ Guess[] removed = {};
+
+ foreach (Guess guess in this) {
+ if (guess.row == row && guess.col == col) {
+ removed += guess;
+ this.remove(guess);
+ }
+ }
+
+ return removed;
+ }
+}
+
+public class BreadcrumbTrail : GuessList {
+
+ public new void append(Guess guess) {
+ if (guesses_for(guess.row, guess.col).length != 0) {
+ // "We already have crumbs on %s, %s" % (guess.x, guess.y))
+ } else {
+ add(guess);
+ }
+ }
+}
+
+public class ParallelDict {
+ private HashMap<Coord?, HashSet<Coord?>> map = new HashMap<Coord?, HashSet<Coord?>> ();
+
+ public void set (Coord k, HashSet<Coord?> v)
+ {
+ map.set (k, v);
+ foreach (Coord i in v)
+ {
+ if (i == k)
+ continue;
+ if (map.has_key(i)) {
+ map[k].add (i);
+ } else {
+ HashSet<Coord?> kSet = new HashSet<Coord?> ();
+ kSet.add (k);
+ map.set (i, kSet);
+ }
+ }
+ }
+
+ public void unset (Coord k)
+ {
+ HashSet<Coord?> v = map[k];
+ map.unset (k);
+ foreach (Coord i in v)
+ {
+ if (i == k)
+ continue;
+ if (map.has_key(i))
+ {
+ if (k in map[i])
+ map[i].remove(k);
+ if (map[i].size == 0)
+ map.unset (i);
+ // If k was the last value in the list of values
+ // for i, then we delete i from our dictionary
+ }
+ }
+ }
+
+ public bool contains (Coord key)
+ {
+ return map.has_key (key);
+ }
+}
+
+public enum DifficultyCatagory {
+ EASY,
+ MEDIUM,
+ HARD,
+ VERY_HARD
+}
+
+public class DifficultyRating {
+
+ public const float[] VERY_HARD_RANGE = { 0.75f, 10 };
+ public const float[] HARD_RANGE = { 0.6f, 0.75f };
+ public const float[] MEDIUM_RANGE = { 0.45f, 0.6f };
+ public const float[] EASY_RANGE = { -10, 0.45f };
+
+ HashMap<int, HashSet<Cell?>> fill_must_fillables;
+ HashMap<int, HashSet<Cell?>> elimination_fillables;
+ GuessList guesses;
+ int backtraces;
+ int squares_filled;
+
+ float elimination_ease;
+ float fillable_ease;
+
+ float instant_fill_fillable;
+ float instant_elimination_fillable;
+ float proportion_instant_elimination_fillable;
+ float proportion_instant_fill_fillable;
+
+ public float rating;
+
+ public DifficultyRating (HashMap<int, HashSet<Cell?>> fill_must_fillables, HashMap<int, HashSet<Cell?>>
elimination_fillables, GuessList guesses, int backtraces, int squares_filled )
+ {
+ this.fill_must_fillables = fill_must_fillables;
+ this.elimination_fillables = elimination_fillables;
+
+ this.guesses = guesses;
+ this.backtraces = backtraces;
+ this.squares_filled = squares_filled;
+
+ if (fill_must_fillables.size != 0)
+ instant_fill_fillable = (float) fill_must_fillables[0].size;
+ else
+ instant_fill_fillable = 0.0f;
+
+ if (elimination_fillables.size != 0)
+ instant_elimination_fillable = (float) elimination_fillables[0].size;
+ else
+ instant_elimination_fillable = 0.0f;
+
+ proportion_instant_elimination_fillable = instant_elimination_fillable / squares_filled;
+ // some more numbers that may be crazy...
+ proportion_instant_fill_fillable = instant_fill_fillable / squares_filled;
+ elimination_ease = add_with_diminishing_importance(count_values(elimination_fillables));
+ fillable_ease = add_with_diminishing_importance(count_values(fill_must_fillables));
+ rating = calculate();
+ }
+
+ private int[] count_values (HashMap<int, HashSet<Cell?>> map) {
+ TreeMap<int, HashSet<Cell?>> sortedMap = new TreeMap<int, HashSet<Cell?>> ();
+ foreach (int key in map.keys) {
+ sortedMap[key] = map[key];
+ }
+ int[] array = new int[map.size];
+ int p = 0;
+ foreach (int i in sortedMap.keys) {
+ array[p] = sortedMap[i].size;
+ p++;
+ }
+ return array;
+ }
+
+ private float calculate () {
+ return 1 - (((float)fillable_ease) / squares_filled) - (((float)elimination_ease / squares_filled))
+ (guesses.size / squares_filled) + (backtraces / squares_filled);
+ }
+
+ delegate int DiminshBy(int a);
+
+ private static int diminsh_by_one (int a) {
+ return a + 1;
+ }
+
+ public bool in_range (float[] range) {
+ return rating >= range[0] && rating < range[1];
+ }
+
+ public DifficultyCatagory get_catagory ()
+ {
+ if (in_range(EASY_RANGE))
+ return DifficultyCatagory.EASY;
+ else if (in_range(MEDIUM_RANGE))
+ return DifficultyCatagory.MEDIUM;
+ else if (in_range(HARD_RANGE))
+ return DifficultyCatagory.HARD;
+ else if (in_range(VERY_HARD_RANGE))
+ return DifficultyCatagory.VERY_HARD;
+ else
+ assert_not_reached();
+ }
+
+ static float add_with_diminishing_importance (int[] array, DiminshBy diminish_by = diminsh_by_one) {
+ float sum = 0;
+ for (int i = 0; i < array.length; i++)
+ {
+ sum += ((float) array[i]) / diminish_by(i);
+ }
+ return sum;
+ }
+
+ public void pretty_print () {
+ stdout.printf ("Number of moves instantly fillable by elimination: %f\n",
instant_elimination_fillable);
+ stdout.printf ("Percentage of moves instantly fillable by elimination: %f\n",
proportion_instant_elimination_fillable * 100);
+ stdout.printf ("Number of moves instantly fillable by filling: %f\n", instant_fill_fillable);
+ stdout.printf ("Percentage of moves instantly fillable by filling: %f\n",
proportion_instant_fill_fillable * 100);
+ stdout.printf ("Number of guesses made: %d\n", guesses.size);
+ stdout.printf ("Number of backtraces: %d\n", backtraces);
+ stdout.printf ("Ease by filling: %f\n", fillable_ease);
+ stdout.printf ("Ease by elimination: %f\n", elimination_ease);
+ stdout.printf ("Calculated difficulty: %f\n", rating);
+ }
+}
diff --git a/src/sudoku-store.vala b/src/sudoku-store.vala
new file mode 100644
index 0000000..8477c35
--- /dev/null
+++ b/src/sudoku-store.vala
@@ -0,0 +1,109 @@
+using Gee;
+
+class SudokuStore
+{
+ private ArrayList<SudokuBoard> easy_boards = new ArrayList<SudokuBoard> ();
+ private ArrayList<SudokuBoard> medium_boards = new ArrayList<SudokuBoard> ();
+ private ArrayList<SudokuBoard> hard_boards = new ArrayList<SudokuBoard> ();
+ private ArrayList<SudokuBoard> very_hard_boards = new ArrayList<SudokuBoard> ();
+
+ public SudokuStore () {
+ try {
+ { // Easy boards
+ var file = File.new_for_uri ("resource:///org/gnome/gnome-sudoku/puzzles/easy");
+
+ var dis = new DataInputStream (file.read ());
+
+ string line;
+ // Read lines until end of file (null) is reached
+ while ((line = dis.read_line (null)) != null) {
+ SudokuBoard board = new SudokuBoard();
+ board.set_from_string(line[0:161], " ");
+
+ easy_boards.add(board);
+ }
+ }
+
+ { // Medium boards
+ var file = File.new_for_uri ("resource:///org/gnome/gnome-sudoku/puzzles/medium");
+
+ var dis = new DataInputStream (file.read ());
+
+ string line;
+ // Read lines until end of file (null) is reached
+ while ((line = dis.read_line (null)) != null) {
+ SudokuBoard board = new SudokuBoard();
+ board.set_from_string(line[0:161], " ");
+
+ medium_boards.add(board);
+ }
+ }
+
+ { // Hard boards
+ var file = File.new_for_uri ("resource:///org/gnome/gnome-sudoku/puzzles/hard");
+
+ var dis = new DataInputStream (file.read ());
+
+ string line;
+ // Read lines until end of file (null) is reached
+ while ((line = dis.read_line (null)) != null) {
+ SudokuBoard board = new SudokuBoard();
+ board.set_from_string(line[0:161], " ");
+
+ hard_boards.add(board);
+ }
+ }
+
+ { // Very hard boards
+ var file = File.new_for_uri ("resource:///org/gnome/gnome-sudoku/puzzles/very_hard");
+
+ var dis = new DataInputStream (file.read ());
+
+ string line;
+ // Read lines until end of file (null) is reached
+ while ((line = dis.read_line (null)) != null) {
+ SudokuBoard board = new SudokuBoard();
+ board.set_from_string(line[0:161], " ");
+
+ very_hard_boards.add(board);
+ }
+ }
+ } catch (Error e) {
+ error ("%s", e.message);
+ }
+ }
+
+ public SudokuBoard get_random_easy_board()
+ {
+ return easy_boards[Random.int_range(0, easy_boards.size)];
+ }
+
+ public SudokuBoard get_random_medium_board()
+ {
+ return medium_boards[Random.int_range(0, medium_boards.size)];
+ }
+
+ public SudokuBoard get_random_hard_board()
+ {
+ return hard_boards[Random.int_range(0, hard_boards.size)];
+ }
+
+ public SudokuBoard get_random_very_hard_board()
+ {
+ return very_hard_boards[Random.int_range(0, very_hard_boards.size)];
+ }
+
+ public SudokuBoard get_random_board(DifficultyCatagory catagory)
+ {
+ if (catagory == DifficultyCatagory.EASY)
+ return get_random_easy_board();
+ else if (catagory == DifficultyCatagory.MEDIUM)
+ return get_random_medium_board();
+ else if (catagory == DifficultyCatagory.HARD)
+ return get_random_hard_board();
+ else if (catagory == DifficultyCatagory.VERY_HARD)
+ return get_random_very_hard_board();
+ else
+ assert_not_reached();
+ }
+}
diff --git a/src/sudoku-view.vala b/src/sudoku-view.vala
new file mode 100644
index 0000000..753672a
--- /dev/null
+++ b/src/sudoku-view.vala
@@ -0,0 +1,819 @@
+using Gtk;
+using Gdk;
+
+private class SudokuCellView : Gtk.DrawingArea
+{
+ private Pango.Layout layout;
+
+ private double size_ratio = 2;
+
+ private Gtk.Window? popup = null;
+
+ private SudokuGame game;
+
+ private int _row;
+ public int row
+ {
+ get { return _row; }
+ set { _row = value; }
+ }
+
+ private int _col;
+ public int col
+ {
+ get { return _col; }
+ set { _col = value; }
+ }
+
+ public int value
+ {
+ get
+ {
+ return game.board [_row, _col];
+ }
+ set
+ {
+ if (is_fixed)
+ {
+ string text = "%d".printf (game.board [_row, _col]);
+ layout = create_pango_layout (text);
+ layout.set_font_description (style.font_desc);
+ return;
+ }
+ if (value == 0)
+ {
+ string text = "";
+ layout = create_pango_layout (text);
+ layout.set_font_description (style.font_desc);
+ if (game.board [_row, _col] != 0)
+ game.remove (_row, _col);
+ return;
+ }
+ if (value == game.board [_row, _col])
+ {
+ string text = "%d".printf (value);
+ layout = create_pango_layout (text);
+ layout.set_font_description (style.font_desc);
+ return;
+ }
+ game.insert (_row, _col, value);
+ }
+ }
+
+ public bool is_fixed
+ {
+ get
+ {
+ return game.board.is_fixed[_row, _col];
+ }
+ }
+
+ public string top_notes { set; get; default = ""; }
+ public string bottom_notes { set; get; default = ""; }
+
+ private bool _show_possibilities;
+ public bool show_possibilities
+ {
+ get { return _show_possibilities; }
+ set {
+ _show_possibilities = value;
+ queue_draw ();
+ }
+ }
+
+ private bool _warn_about_unfillable_squares = false;
+ public bool warn_about_unfillable_squares
+ {
+ get { return _warn_about_unfillable_squares; }
+ set {
+ _warn_about_unfillable_squares = value;
+ queue_draw ();
+ }
+ }
+
+ private bool _selected;
+ public bool selected
+ {
+ get { return _selected; }
+ set { _selected = value; }
+ }
+
+ private bool _highlight;
+ public bool highlight
+ {
+ get { return _highlight; }
+ set { _highlight = value; }
+ }
+
+ private bool _invalid;
+ public bool invalid
+ {
+ get { return _invalid; }
+ set { _invalid = value; }
+ }
+
+ private RGBA _background_color;
+ public RGBA background_color
+ {
+ get { return _background_color; }
+ set
+ {
+ _background_color = value;
+ }
+ }
+
+ public SudokuCellView (int row, int col, ref SudokuGame game, bool small = false)
+ {
+ this.game = game;
+ this._row = row;
+ this._col = col;
+
+ if (is_fixed)
+ {
+ _background_color = { 0.8, 0.8, 0.8, 0.0 };
+ }
+ else
+ {
+ _background_color = { 1.0, 1.0, 1.0, 0.0 };
+ }
+
+ set_can_focus(true);
+ can_focus = true;
+
+ style.font_desc.set_size (Pango.SCALE * 13);
+ this.value = game.board [_row, _col];
+
+ if (small)
+ {
+ size_ratio = 1;
+ }
+
+ events = EventMask.EXPOSURE_MASK | EventMask.BUTTON_PRESS_MASK | EventMask.KEY_PRESS_MASK;
+ focus_out_event.connect (focus_out_cb);
+ game.cell_changed.connect (cell_changed_cb);
+ }
+
+ public override void get_preferred_width (out int minimal_width, out int natural_width)
+ {
+ int width, height, side;
+ layout.get_size (out width, out height);
+ side = width > height ? width : height;
+ minimal_width = natural_width = (int) (size_ratio * side) / Pango.SCALE;
+ }
+
+ public override void get_preferred_height (out int minimal_height, out int natural_height)
+ {
+ int width, height, side;
+ layout.get_size (out width, out height);
+ side = width > height ? width : height;
+ minimal_height = natural_height = (int) (size_ratio * side) / Pango.SCALE;
+ }
+
+ public override bool button_press_event (Gdk.EventButton event)
+ {
+ if (event.button != 1)
+ return false;
+
+ if (!is_focus)
+ {
+ grab_focus ();
+ return false;
+ }
+
+ if (event.y / get_allocated_height () < 0.25)
+ {
+ show_note_editor (0);
+ }
+ else
+ {
+ if (event.y / get_allocated_height () > 0.75)
+ {
+ if (!_show_possibilities)
+ {
+ show_note_editor (1);
+ }
+ }
+ else
+ {
+ show_number_picker ();
+ }
+ }
+
+ return false;
+ }
+
+ private bool number_pick { get; set; default = false; }
+
+ private void show_number_picker ()
+ {
+ if (popup != null)
+ return;
+ if (is_fixed)
+ return;
+ number_pick = true;
+ popup = new Gtk.Window (Gtk.WindowType.POPUP);
+ popup.decorated = false;
+ popup.resizable = false;
+ popup.skip_taskbar_hint = true;
+ popup.skip_pager_hint = true;
+ popup.set_type_hint (Gdk.WindowTypeHint.DIALOG);
+
+ var number_picker = new NumberPicker(ref game.board, value != 0);
+ number_picker.number_picked.connect ((o, number) => {
+ value = number;
+ if (number == 0)
+ {
+ notify_property("value");
+ }
+ hide_popup ();
+ });
+ popup.add (number_picker);
+
+ int x, y, width, height;
+ get_window ().get_origin (out x, out y);
+ popup.show ();
+ popup.get_size (out width, out height);
+ popup.focus_out_event.connect (() => { hide_popup (); return true; });
+ var screen = popup.get_screen ();
+ var popup_x = x - (width - get_allocated_width ()) / 2;
+ var popup_y = y - (height - get_allocated_height ()) / 2;
+ if (popup_x < 0)
+ popup_x = 0;
+ else if (popup_x + width > screen.get_width ())
+ popup_x = screen.get_width () - width;
+ if (popup_y < 0)
+ popup_y = 0;
+ else if (popup_y + height > screen.get_height ())
+ popup_y = screen.get_height () - height;
+ popup.move (popup_x, popup_y);
+ }
+
+ private void hide_popup ()
+ {
+ number_pick = false;
+ if (popup == null)
+ return;
+ popup.destroy ();
+ popup = null;
+ }
+
+ private bool editing_notes { get; set; default = false; }
+
+ private void show_note_editor (int top)
+ {
+ if (popup != null)
+ return;
+ if (is_fixed)
+ return;
+
+ editing_notes = true;
+ // TODO: This probably should be changed to Gtk.WindowType.POPUP
+ popup = new Gtk.Window (Gtk.WindowType.TOPLEVEL);
+ popup.decorated = false;
+ popup.resizable = false;
+ popup.skip_taskbar_hint = true;
+ popup.skip_pager_hint = true;
+ popup.set_type_hint (Gdk.WindowTypeHint.DIALOG);
+ popup.set_size_request (get_allocated_width (), -1);
+
+ var entry = new Gtk.Entry ();
+ entry.has_frame = false;
+ if (top == 0)
+ entry.set_text (top_notes);
+ else
+ entry.set_text (bottom_notes);
+ popup.add (entry);
+
+ entry.focus_out_event.connect (() => {
+ hide_note_editor (entry, top);
+ return true;
+ });
+
+ entry.activate.connect (() => { hide_note_editor (entry, top); });
+
+ int x, y, height;
+
+ get_window ().get_origin (out x, out y);
+ popup.show_all ();
+ popup.get_size (null, out height);
+ popup.move (x, y + top * (get_allocated_height () - height));
+ }
+
+ private void hide_note_editor (Gtk.Entry entry, int top)
+ {
+ editing_notes = false;
+ if (top == 0)
+ top_notes = entry.get_text ();
+ else
+ bottom_notes = entry.get_text ();
+ hide_popup ();
+ }
+
+ private bool focus_out_cb (Gtk.Widget widget, Gdk.EventFocus event)
+ {
+ if (editing_notes || number_pick) return false;
+ if (popup != null)
+ hide_popup ();
+ return false;
+ }
+
+ /* Key mapping function to help convert Gdk.keyval_name string to numbers */
+ private int key_map_keypad (string key_name)
+ {
+ /* Compared with "0" to make sure, actual "0" is not misinterpreted as parse error in int.parse() */
+ if (key_name == "KP_0" || key_name == "0")
+ return 0;
+ if (key_name == "KP_1")
+ return 1;
+ if (key_name == "KP_2")
+ return 2;
+ if (key_name == "KP_3")
+ return 3;
+ if (key_name == "KP_4")
+ return 4;
+ if (key_name == "KP_5")
+ return 5;
+ if (key_name == "KP_6")
+ return 6;
+ if (key_name == "KP_7")
+ return 7;
+ if (key_name == "KP_8")
+ return 8;
+ if (key_name == "KP_9")
+ return 9;
+ return -1;
+ }
+
+ public override bool key_press_event (Gdk.EventKey event)
+ {
+ string k_name = Gdk.keyval_name (event.keyval);
+ int k_no = int.parse (k_name);
+ /* If k_no is 0, there might be some error in parsing, crosscheck with keypad values. */
+ if (k_no == 0)
+ k_no = key_map_keypad (k_name);
+ if (k_no >= 1 && k_no <= 9)
+ {
+ value = k_no;
+ return true;
+ }
+ if (k_no == 0 || k_name == "BackSpace" || k_name == "Delete")
+ {
+ value = 0;
+ notify_property("value");
+ return true;
+ }
+
+ if (k_name == "space" || k_name == "Return" || k_name == "KP_Enter")
+ {
+ show_number_picker ();
+ return true;
+ }
+
+ return false;
+ }
+
+ public override bool draw (Cairo.Context c)
+ {
+ StyleContext styleContext = get_style_context ();
+
+ // Draw the background
+ if (_selected)
+ {
+ RGBA color = styleContext.get_background_color (StateFlags.SELECTED);
+ c.set_source_rgb (color.red, color.green, color.blue);
+ }
+ else
+ {
+ c.set_source_rgb (_background_color.red, _background_color.green, _background_color.blue);
+ }
+
+ c.rectangle (0, 0, get_allocated_width (), get_allocated_height ());
+ c.fill ();
+
+ int glyph_width, glyph_height;
+ layout.get_pixel_size (out glyph_width, out glyph_height);
+ if (game.board.broken_coords.contains(Coord(row, col)))
+ {
+ c.set_source_rgb (1.0, 0.0, 0.0);
+ }
+ else if (_selected)
+ {
+ RGBA color = styleContext.get_color (StateFlags.SELECTED);
+ c.set_source_rgb (color.red, color.green, color.blue);
+ }
+ else
+ {
+ c.set_source_rgb (0.0, 0.0, 0.0);
+ }
+
+ if (value != 0)
+ {
+ int width, height;
+ layout.get_size (out width, out height);
+ height /= Pango.SCALE;
+
+ double scale = ((double) get_allocated_height () / size_ratio) / height;
+ c.move_to ((get_allocated_width () - glyph_width * scale) / 2, (get_allocated_height () -
glyph_height * scale) / 2);
+ c.save ();
+ c.scale (scale, scale);
+ Pango.cairo_update_layout (c, layout);
+ Pango.cairo_show_layout (c, layout);
+ c.restore ();
+ }
+ else if (_show_possibilities)
+ {
+ double possibility_size = get_allocated_height () / (size_ratio * 2);
+ c.set_font_size (possibility_size);
+ c.set_source_rgb (0.0, 0.0, 0.0);
+
+ bool[] possibilities = game.board.get_possibilities_as_bool_array(row, col);
+
+ int height = get_allocated_height () / game.board.block_cols;
+ int width = get_allocated_height () / game.board.block_rows;
+
+ int num = 0;
+ for (int row = 0; row < game.board.block_rows; row++)
+ {
+ for (int col = 0; col < game.board.block_cols; col++)
+ {
+ num++;
+
+ if (possibilities[num - 1])
+ {
+ c.move_to (col * width, (row * height) + possibility_size);
+ c.show_text ("%d".printf(num));
+ }
+ }
+ }
+ }
+
+ if (is_fixed)
+ return false;
+
+ // Draw the notes
+ double note_size = get_allocated_height () / (size_ratio * 2);
+ c.set_font_size (note_size);
+
+ c.move_to (0, note_size);
+
+ c.set_source_rgb (0.0, 0.0, 0.0);
+ c.show_text (top_notes);
+
+ c.move_to (0, (get_allocated_height () - 3));
+ if (_warn_about_unfillable_squares)
+ {
+ c.set_source_rgb (1.0, 0.0, 0.0);
+ c.show_text ("None");
+ }
+ else
+ {
+ c.set_source_rgb (0.0, 0.0, 0.0);
+ //if (_show_possibilities)
+ //{
+ // c.show_text (get_possibility_string (game.board.get_possibilities(_row, _col)));
+ //}
+ //else
+ //{
+ c.show_text (bottom_notes);
+ //}
+ }
+
+ return false;
+ }
+
+ private static string get_possibility_string (int[] possibilities) {
+ var builder = new StringBuilder ();
+ foreach (int a in possibilities) {
+ builder.append (@"$a ");
+ }
+ builder.truncate ((possibilities.length * 2) - 1);
+ return builder.str;
+ }
+
+ public void cell_changed_cb (int row, int col, int old_val, int new_val)
+ {
+ if (row == this.row && col == this.col)
+ {
+ this.value = new_val;
+ if (new_val == 0)
+ {
+ notify_property("value");
+ }
+ }
+ }
+
+ public void hint ()
+ {
+ show_hint ();
+ Timeout.add (200, remove_hint);
+ Timeout.add (400, show_hint);
+ Timeout.add (600, remove_hint);
+ Timeout.add (800, show_hint);
+ Timeout.add (1000, remove_hint);
+ }
+
+ private bool show_hint ()
+ {
+ background_color = { 1.0, 0.0, 0.0, 0.0 };
+ queue_draw ();
+ return false;
+ }
+
+ private bool remove_hint ()
+ {
+ background_color = { 1.0, 1.0, 1.0, 0.0 };
+ queue_draw ();
+ return false;
+ }
+}
+
+public class SudokuView : Gtk.AspectFrame
+{
+ private SudokuGame game;
+ private SudokuCellView[,] cells;
+
+ public signal void cell_focus_in_event (int row, int col);
+ public signal void cell_focus_out_event (int row, int col);
+ public signal void cell_value_changed_event (int row, int col);
+
+ private bool previous_board_broken_state = false;
+
+ private Gtk.EventBox box;
+ private Gtk.Table table;
+
+ private int dance_step;
+
+ private const RGBA[] dance_colors = { {0.8, 0.0, 0.0, 0.0},
+ {0.9372549019607843, 0.1607843137254902, 0.1607843137254902, 0.0},
+ {0.9607843137254902, 0.4745098039215686, 0.0, 0.0},
+ {0.9882352941176471, 0.6862745098039216, 0.2431372549019607, 0.0},
+ {0.9882352941176471, 0.9137254901960784, 0.3098039215686274, 0.0},
+ {0.5411764705882353, 0.8862745098039215, 0.2039215686274509, 0.0},
+ {0.4509803921568627, 0.8235294117647058, 0.0862745098039215, 0.0},
+ {0.4470588235294118, 0.6235294117647059, 0.8117647058823529, 0.0},
+ {0.2039215686274509, 0.3960784313725497, 0.6431372549019608, 0.0},
+ {0.6784313725490196, 0.4980392156862745, 0.6588235294117647, 0.0},
+ {0.4588235294117647, 0.3137254901960784, 0.4823529411764706, 0.0}
+ };
+
+ private int _selected_x = 0;
+ public int selected_x
+ {
+ get { return _selected_x; }
+ set {
+ cells[selected_y, selected_x].selected = false;
+ cells[selected_y, selected_x].queue_draw ();
+ _selected_x = value;
+ cells[selected_y, selected_x].selected = true;
+ }
+ }
+
+ private int _selected_y = 0;
+ public int selected_y
+ {
+ get { return _selected_y; }
+ set {
+ cells[selected_y, selected_x].selected = false;
+ cells[selected_y, selected_x].queue_draw ();
+ _selected_y = value;
+ cells[selected_y, selected_x].selected = true;
+ }
+ }
+
+ public SudokuView (SudokuGame game, bool preview = false)
+ {
+ shadow_type = Gtk.ShadowType.NONE;
+
+ /* Use an EventBox to be able to set background */
+ box = new Gtk.EventBox ();
+ box.modify_bg (Gtk.StateType.NORMAL, box.style.black);
+ add (box);
+ box.show ();
+
+ this.obey_child = false;
+
+ set_game (game, preview);
+ }
+
+ public void set_game (SudokuGame game, bool preview = false)
+ {
+ if (table != null)
+ {
+ box.remove (table);
+ }
+
+ this.game = game;
+
+ table = new Gtk.Table (game.board.rows, game.board.cols, false);
+ table.row_spacing = 1;
+ table.column_spacing = 1;
+ for (var row = game.board.block_rows; row < game.board.rows; row += game.board.block_rows)
+ {
+ table.set_row_spacing (row - 1, 2);
+ }
+ for (var col = game.board.block_cols; col < game.board.cols; col += game.board.block_cols)
+ {
+ table.set_col_spacing (col - 1, 2);
+ }
+ table.border_width = 2;
+
+ cells = new SudokuCellView[game.board.rows, game.board.cols];
+ for (var row = 0; row < game.board.rows; row++)
+ {
+ for (var col = 0; col < game.board.cols; col++)
+ {
+ var cell = new SudokuCellView (row, col, ref this.game, preview);
+ var cell_row = row;
+ var cell_col = col;
+
+ if (!preview)
+ {
+ cell.focus_out_event.connect (() => {
+ cell_focus_out_event(cell_row, cell_col);
+ return false;
+ });
+
+ cell.focus_in_event.connect (() => {
+ this.selected_x = cell_col;
+ this.selected_y = cell_row;
+ cell_focus_in_event(cell_row, cell_col);
+ return false;
+ });
+
+ cell.notify["value"].connect((s, p)=> {
+ /* The board needs redrawing if it was/is broken, or if the possibilities are being
displayed */
+ if (_show_possibilities || _show_warnings || game.board.broken ||
previous_board_broken_state) {
+ for (var i = 0; i < game.board.rows; i++)
+ {
+ for (var j = 0; j < game.board.cols; j++)
+ {
+ if (_show_warnings && cells[i,j].value == 0 &&
game.board.count_possibilities (cells[i,j].row, cells[i,j].col) == 0) {
+ if (!cells[i,j].warn_about_unfillable_squares) {
+ cells[i,j].warn_about_unfillable_squares = true;
+ cells[i,j].hint ();
+ }
+ }
+ else
+ {
+ cells[i,j].warn_about_unfillable_squares = false;
+ }
+ }
+ }
+ previous_board_broken_state = game.board.broken;
+ }
+ cell_value_changed_event(cell_row, cell_col);
+ });
+ }
+
+ cells[row, col] = cell;
+ cell.show ();
+ table.attach_defaults (cell, col, col+1, row, row+1);
+
+ }
+ }
+ box.add (table);
+ table.show ();
+ }
+
+ private bool _show_highlights = false;
+ public bool show_highlights
+ {
+ get { return _show_highlights; }
+ set { _show_highlights = value; }
+ }
+
+ private bool _show_hints = false;
+ public bool show_hints
+ {
+ get { return _show_hints; }
+ set { _show_hints = value; }
+ }
+
+ private bool _show_warnings = false;
+ public bool show_warnings
+ {
+ get { return _show_warnings; }
+ set {
+ _show_warnings = value;
+ for (var i = 0; i < game.board.rows; i++)
+ {
+ for (var j = 0; j < game.board.cols; j++)
+ {
+ if (_show_warnings && cells[i,j].value == 0 && game.board.count_possibilities
(cells[i,j].row, cells[i,j].col) == 0)
+ {
+ cells[i,j].warn_about_unfillable_squares = true;
+ cells[i,j].hint ();
+ }
+ else
+ {
+ cells[i,j].warn_about_unfillable_squares = false;
+ }
+ }
+ }
+ }
+ }
+
+ private bool _show_possibilities = false;
+ public bool show_possibilities
+ {
+ get { return _show_possibilities; }
+ set {
+ _show_possibilities = value;
+ for (var i = 0; i < game.board.rows; i++)
+ for (var j = 0; j < game.board.cols; j++)
+ cells[i,j].show_possibilities = value;
+ }
+ }
+
+ public void set_cell_value (int x, int y, int value) {
+ cells[y, x].value = value;
+ if (value == 0)
+ {
+ cells[y, x].notify_property("value");
+ }
+ }
+
+ public void hint ()
+ {
+ int row=0, col=0;
+ game.hint (ref row, ref col);
+ cells [row, col].hint ();
+ }
+
+ public bool dance () {
+ if (dance_step < 90)
+ {
+ for (var j = 0; j < game.board.cols; j++)
+ {
+ RGBA color = dance_colors[dance_step % dance_colors.length];
+ dance_step++;
+ for (var i = 0; i < game.board.rows; i++)
+ cells[i,j].background_color = color;
+ }
+ }
+ else
+ {
+ for (var i = 0; i < game.board.rows; i++)
+ {
+ RGBA color = dance_colors[dance_step % dance_colors.length];
+ dance_step++;
+ for (var j = 0; j < game.board.cols; j++)
+ cells[i,j].background_color = color;
+ }
+ }
+
+ if (dance_step > 180)
+ dance_step = 0;
+
+ if (dance_step >= 0)
+ Timeout.add (200, dance);
+ queue_draw ();
+
+ return false;
+ }
+
+ public void stop_dance ()
+ {
+ dance_step = -1;
+ for (var j = 0; j < game.board.cols; j++)
+ {
+ for (var i = 0; i < game.board.rows; i++)
+ {
+ if (cells[i,j].is_fixed)
+ {
+ cells[i,j].background_color = { 0.8, 0.8, 0.8, 0.0 };
+ }
+ else
+ {
+ cells[i,j].background_color = { 1.0, 1.0, 1.0, 0.0 };
+ }
+ }
+ }
+ }
+
+ private RGBA get_next_color (RGBA color)
+ {
+ return dance_colors[Random.int_range(0, dance_colors.length)];
+ }
+
+ public void clear_top_notes ()
+ {
+ for (var i = 0; i < game.board.rows; i++)
+ for (var j = 0; j < game.board.cols; j++)
+ cells[i,j].top_notes = "";
+ queue_draw ();
+ }
+
+ public void clear_bottom_notes ()
+ {
+ for (var i = 0; i < game.board.rows; i++)
+ for (var j = 0; j < game.board.cols; j++)
+ cells[i,j].bottom_notes = "";
+ queue_draw ();
+ }
+
+ public void cell_grab_focus(int row, int col)
+ {
+ cells[row, col].grab_focus ();
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]