[gnome-games] Overhaul of conflict resolution



commit 3da3d7dca0948d59f11c7d539179996ecb50a8a9
Author: Jim Ross <jimbo dimensia com>
Date:   Sun Mar 28 18:39:26 2010 -0400

    Overhaul of conflict resolution

 gnome-sudoku/src/lib/gsudoku.py |  112 +++++++--------------
 gnome-sudoku/src/lib/sudoku.py  |  219 +++++++++++++++++++++++++++++++++-----
 2 files changed, 226 insertions(+), 105 deletions(-)
---
diff --git a/gnome-sudoku/src/lib/gsudoku.py b/gnome-sudoku/src/lib/gsudoku.py
index 41d735b..e2ef7fc 100644
--- a/gnome-sudoku/src/lib/gsudoku.py
+++ b/gnome-sudoku/src/lib/gsudoku.py
@@ -96,51 +96,6 @@ class SudokuNumberGrid (gtk.AspectFrame):
         for e in self.__entries__.values():
             e.modify_bg(gtk.STATE_NORMAL, color)
 
-class ParallelDict (dict):
-    """A handy new sort of dictionary for tracking conflicts.
-
-    pd = ParallelDict()
-    pd[1] = [2, 3, 4] # 1 is linked with 2, 3 and 4
-    pd -> {1:[2, 3, 4], 2:[1], 3:[1], 4:[1]}
-    pd[2] = [1, 3, 4] # 2 is linked with 3 and 4 as well as 1
-    pd -> {1: [2, 3, 4], 2:[3, 4], 3:[1, 2], 4:[1, 2]}
-    Now for the cool part...
-    del pd[1]
-    pd -> {2: [2, 3], 3:[2], 4:[2]}
-
-    Pretty neat, no?
-    """
-    def __init__ (self, *args):
-        dict.__init__(self, *args)
-
-    def __setitem__ (self, k, v):
-        dict.__setitem__(self, k, set(v))
-        for i in v:
-            if i == k:
-                continue
-            if self.has_key(i):
-                self[i].add(k)
-            else:
-                dict.__setitem__(self, i, set([k]))
-
-    def __delitem__ (self, k):
-        v = self[k]
-        dict.__delitem__(self, k)
-        for i in v:
-            if i == k:
-                continue
-            if self.has_key(i):
-                # Make sure we have a reference to i. If we don't
-                # something has gone wrong... but according to bug
-                # 385937 this has gone wrong at least once, so we'd
-                # better check for it.
-                if k in self[i]:
-                    self[i].remove(k)
-                if not self[i]:
-                    # If k was the last value in the list of values
-                    # for i, then we delete i from our dictionary
-                    dict.__delitem__(self, i)
-
 class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
 
     __gsignals__ = {
@@ -368,7 +323,6 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
     @simple_debug
     def setup_grid (self, grid, group_size):
         self.doing_initial_setup = True
-        self.__error_pairs__ = ParallelDict()
         if isinstance(grid, sudoku.SudokuGrid):
             self.grid = sudoku.InteractiveSudoku(grid.grid, group_size = grid.group_size)
         else:
@@ -411,16 +365,21 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
         if self.grid.check_for_completeness():
             self.emit('puzzle-finished')
 
-    def complain_conflicts (self, x, y, value):
-        '''set error highlights on [x, y] and all related box.
+    def highlight_conflicts (self, x, y):
+        '''highlight any squares that conflict with position x,y.
 
-        We think the error is caused by `value`. But the box at [x, y] could
-        have a different value.'''
-        self.__entries__[x, y].set_error_highlight(True)
-        conflicts = self.grid.find_conflicts(x, y, value)
-        for conflict in conflicts:
-            self.__entries__[conflict].set_error_highlight(True)
-        self.__error_pairs__[(x, y)] = conflicts
+        Conflict resolution is taken care of completely within
+        the InteractiveGrid class.  A list of conflicting cells
+        are stored in InteractiveGrid.conflicts
+        '''
+        # Return if there are no conflicts for this cell
+        if not self.grid.conflicts.has_key((x, y)):
+            return
+        # Highlight the current cell
+        self.__entries__[(x, y)].set_error_highlight(True)
+        # Then highlight any cells that conflict with it
+        for coord in self.grid.conflicts[(x, y)]:
+            self.__entries__[coord].set_error_highlight(True)
 
     @simple_debug
     def add_value (self, x, y, val, trackers = []):
@@ -452,13 +411,10 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
                 if v:
                     self.__entries__[(x, y)].set_color(self.get_tracker_color(k))
                     self.trackers[k].append((x, y, val))
-        # Add value to our underlying sudoku grid -- this will raise
-        # an error if the value is out of bounds with the current
-        # rules.
-        try:
-            self.grid.add(x, y, val, True)
-        except sudoku.ConflictError, err:
-            self.complain_conflicts(err.x, err.y, err.value)
+        # Add it to the underlying grid
+        self.grid.add(x, y, val, True)
+        # Highlight any conflicts that the new value creates
+        self.highlight_conflicts(x, y)
         # Draw our entry
         self.__entries__[(x, y)].queue_draw()
 
@@ -469,9 +425,10 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
         If do_removal, remove it from our underlying grid as well.
         """
         e = self.__entries__[(x, y)]
-        if do_removal and self.grid and self.grid._get_(x, y):
+        # Always call the grid's remove() for proper conflict resolution
+        if self.grid:
             self.grid.remove(x, y)
-        self.remove_error_highlight(x, y)
+            self.remove_error_highlight()
         # remove trackers
         for t in self.trackers_for_point(x, y):
             remove = []
@@ -484,18 +441,18 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
             e.set_value(0)
         e.unset_color()
 
-    def remove_error_highlight (self, x, y):
-        '''remove error highlight from [x, y] and also all errors caused by it'''
-        if self.__error_pairs__.has_key((x, y)):
-            entry = self.__entries__[(x, y)]
-            entry.set_error_highlight(False)
-            errors_removed = self.__error_pairs__[(x, y)]
-            del self.__error_pairs__[(x, y)]
-            for coord in errors_removed:
-                # If we're not an error by some other pairing...
-                if not self.__error_pairs__.has_key(coord):
-                    linked_entry = self.__entries__[coord]
-                    linked_entry.set_error_highlight(False)
+    def remove_error_highlight (self):
+        '''remove error highlight from [x, y] and also all errors caused by it
+
+        Conflict resolution is now handled within the InteractiveSudoku class.
+        If any conflicts were cleared on the last remove() then they are
+        stored in grid.cleared_conflicts
+        '''
+        if not self.grid.cleared_conflicts:
+            return
+        for coord in self.grid.cleared_conflicts:
+            linked_entry = self.__entries__[coord]
+            linked_entry.set_error_highlight(False)
 
     @simple_debug
     def auto_fill (self):
@@ -608,6 +565,9 @@ class SudokuGameDisplay (SudokuNumberGrid, gobject.GObject):
 
     def add_tracker (self, x, y, tracker, val = None):
         self.__entries__[(x, y)].set_color(self.get_tracker_color(tracker))
+        # Highlight the conflicts when opening a saved game
+        if self.grid.conflicts.has_key((x, y)):
+            self.__entries__[(x, y)].set_error_highlight(True)
         if not val:
             val = self.grid._get_(x, y)
         self.trackers[tracker].append((x, y, val))
diff --git a/gnome-sudoku/src/lib/sudoku.py b/gnome-sudoku/src/lib/sudoku.py
index fb9c09a..c05d2a8 100644
--- a/gnome-sudoku/src/lib/sudoku.py
+++ b/gnome-sudoku/src/lib/sudoku.py
@@ -60,7 +60,52 @@ class ConflictError (ValueError):
 class AlreadySetError (ValueError):
     pass
 
-class SudokuGrid:
+class ParallelDict (dict):
+    """A handy new sort of dictionary for tracking conflicts.
+
+    pd = ParallelDict()
+    pd[1] = [2, 3, 4] # 1 is linked with 2, 3 and 4
+    pd -> {1:[2, 3, 4], 2:[1], 3:[1], 4:[1]}
+    pd[2] = [1, 3, 4] # 2 is linked with 3 and 4 as well as 1
+    pd -> {1: [2, 3, 4], 2:[3, 4], 3:[1, 2], 4:[1, 2]}
+    Now for the cool part...
+    del pd[1]
+    pd -> {2: [2, 3], 3:[2], 4:[2]}
+
+    Pretty neat, no?
+    """
+    def __init__ (self, *args):
+        dict.__init__(self, *args)
+
+    def __setitem__ (self, k, v):
+        dict.__setitem__(self, k, set(v))
+        for i in v:
+            if i == k:
+                continue
+            if self.has_key(i):
+                self[i].add(k)
+            else:
+                dict.__setitem__(self, i, set([k]))
+
+    def __delitem__ (self, k):
+        v = self[k]
+        dict.__delitem__(self, k)
+        for i in v:
+            if i == k:
+                continue
+            if self.has_key(i):
+                # Make sure we have a reference to i. If we don't
+                # something has gone wrong... but according to bug
+                # 385937 this has gone wrong at least once, so we'd
+                # better check for it.
+                if k in self[i]:
+                    self[i].remove(k)
+                if not self[i]:
+                    # If k was the last value in the list of values
+                    # for i, then we delete i from our dictionary
+                    dict.__delitem__(self, i)
+
+class SudokuGrid(object):
     def __init__ (self, grid = False, verbose = False, group_size = 9):
         self.grid = []
         self.cols = []
@@ -124,6 +169,10 @@ class SudokuGrid:
                 #is clicked multiple times, which causes this exception:
                 #raise AlreadySetError
                 return
+        # Always store the value in the underlying grid
+        self._set_(x, y, val)
+        # But don't add it to the solution hints(rows/cols/boxes) if there is
+        # a conflict
         if val in self.rows[y]:
             raise ConflictError(TYPE_ROW, (x, y), val)
         if val in self.cols[x]:
@@ -135,13 +184,12 @@ class SudokuGrid:
         self.rows[y].add(val)
         self.cols[x].add(val)
         self.boxes[box].add(val)
-        self._set_(x, y, val)
 
     def remove (self, x, y):
         val = self._get_(x, y)
-        self.rows[y].remove(val)
-        self.cols[x].remove(val)
-        self.boxes[self.box_by_coords[(x, y)]].remove(val)
+        self.rows[y].discard(val)
+        self.cols[x].discard(val)
+        self.boxes[self.box_by_coords[(x, y)]].discard(val)
         self._set_(x, y, 0)
 
     def _get_ (self, x, y):
@@ -165,7 +213,10 @@ class SudokuGrid:
         for y, row in enumerate(grid):
             for x, cell in enumerate(row):
                 if cell:
-                    self.add(x, y, cell)
+                    try:
+                        self.add(x, y, cell)
+                    except ConflictError:
+                        pass
 
     def __repr__ (self):
         s = "<Grid\n       "
@@ -183,29 +234,6 @@ class SudokuGrid:
                     possibilities[(x, y)] = self.possible_values(x, y)
         return possibilities
 
-    def find_conflicts (self, x, y, val, conflict_type = None):
-        '''Find all squares that conflict with value val at position x,y.
-
-        If conflict_type is specified, we only find conflicts of given
-        type (ROW, COLUMN OR BOX).
-        '''
-        if conflict_type == TYPE_ROW:
-            coords = self.row_coords[y]
-        elif conflict_type == TYPE_COLUMN:
-            coords = self.col_coords[x]
-        elif conflict_type == TYPE_BOX:
-            coords = self.box_coords[self.box_by_coords[(x, y)]]
-        else:
-            coords = (self.row_coords[y]
-                      + self.col_coords[x]
-                      + self.box_coords[self.box_by_coords[(x, y)]]
-                      )
-        conflicting_coordinates = []
-        for x, y in coords:
-            if self._get_(x, y) == val:
-                conflicting_coordinates.append((x, y))
-        return conflicting_coordinates
-
     def to_string (self):
         """Output our grid as a string."""
         return " ".join([" ".join([str(x) for x in row]) for row in self.grid])
@@ -477,6 +505,8 @@ class InteractiveSudoku (SudokuSolver):
     solving."""
     def __init__ (self, grid = False, verbose = False, group_size = 9):
         SudokuSolver.__init__(self, grid, verbose, group_size)
+        self.conflicts = ParallelDict()
+        self.cleared_conflicts = []
 
     def to_string (self):
         return self.virgin.to_string() + '\n' + SudokuSolver.to_string(self)
@@ -508,6 +538,137 @@ class InteractiveSudoku (SudokuSolver):
     def is_changed (self):
         return (self.grid != self.virgin.grid)
 
+    def add (self, x, y, val, force = False):
+        '''Add a value to the grid.
+
+        The main feature of this method is conflict resolution.  When conflicts
+        are found they are stored in the conflicts ParallelDict.  A cell that
+        is in conflict is stored in the underlying grid(SudokuGrid.grid), but
+        it has all of its solution hints cleared(SudokuGrid.rows/cols/boxes).
+        Care must be taken so that solution hints from the original
+        grid(SudokuSolver.virgin) are not cleared.
+        '''
+        # First just add it to SudokuGrid
+        no_exception = True
+        try:
+            super(InteractiveSudoku, self).add(x, y, val, force)
+        except ConflictError:
+            no_exception = False
+
+        # Find any cells that conflict with the new value for this cell
+        coords = set([])
+        coords.update(self.row_coords[y],
+                      self.col_coords[x],
+                      self.box_coords[self.box_by_coords[(x, y)]])
+        coords.discard((x, y))
+        conflicting_coordinates = []
+        for xx, yy in coords:
+            if self._get_(xx, yy) == val:
+                conflicting_coordinates.append((xx, yy))
+        # Store the conflicts for access
+        if conflicting_coordinates:
+            self.conflicts[(x, y)] = conflicting_coordinates
+        # Resume when there are no conflicts
+        else:
+            return
+        # But when we do have conflicts, the values from cols/rows/boxes need
+        # to be removed so the hinting doesn't consider them. We must be
+        # chaste with the virgin though.
+        try:
+            if no_exception and not self.virgin._get_(x, y):
+                self.rows[y].discard(val)
+                self.cols[x].discard(val)
+                self.boxes[self.box_by_coords[(x, y)]].discard(val)
+            for xx, yy in conflicting_coordinates:
+                if self.virgin._get_(xx, yy):
+                    continue
+                if not val in self.virgin.rows[yy]:
+                    self.rows[yy].discard(val)
+                if not val in self.virgin.cols[xx]:
+                    self.cols[xx].discard(val)
+                if not val in self.virgin.box_coords[self.box_by_coords[(xx, yy)]]:
+                    self.boxes[self.box_by_coords[(xx, yy)]].discard(val)
+        # This class can be used before the virgin is created.  Pass through
+        # for the initialization phase
+        except AttributeError:
+            pass
+
+    def remove (self, x, y):
+        '''Remove a value from the grid.
+
+        The main feature of this method is conflict resolution.  All
+        conflicting cells are checked to see if they are actually
+        conflict-free.  A list of conflict-free cells are stored in
+        InteractiveSudoku.cleared_conflicts.  The cleared_conflicts list is
+        cleared for each meaningful call to remove(), so it must be processed
+        before another remove() call.
+        All solution hints(SudokuGrid.rows/cols/boxes) are reinstated for
+        conflict-free cells.
+        '''
+        # Grab the value that we're clearing.  Skip out if its nothing
+        val = self._get_(x, y)
+        if not val:
+            return
+        # Pop the conflicts resolved by this removal
+        self.cleared_conflicts = []
+        errors_removed = []
+        if self.conflicts.has_key((x, y)):
+            errors_removed = self.conflicts[(x, y)]
+            del self.conflicts[(x, y)]
+        # If there are no conflicts for this cell then just remove it in from
+        # the grid
+        else :
+            super(InteractiveSudoku, self).remove(x, y)
+            return
+        # Grid clearance flags
+        if val in self.rows[y]:
+            clear_row = True
+        else:
+            clear_row = False
+        if val in self.cols[x]:
+            clear_col = True
+        else:
+            clear_col = False
+        if val in self.boxes[self.box_by_coords[(x, y)]]:
+            clear_box = True
+        else:
+            clear_box = False
+        # Scroll through the conflicts
+        for coord in errors_removed:
+            # If it is not an error by some other pairing, append it to a list
+            # of conflicts that were actually cleared by this removal.
+            if not self.conflicts.has_key(coord):
+                self.cleared_conflicts.append(coord)
+            # When a conflict remains, we need to correct the rows, cols, and
+            # boxes arrays properly
+            else:
+                if clear_row and coord in self.row_coords[y]:
+                    clear_row = False
+                if clear_col and coord in self.col_coords[x]:
+                    clear_col = False
+                if clear_box and coord in self.box_coords[self.box_by_coords[(x, y)]]:
+                    clear_box = False
+
+        # Clear the rows, cols, and boxes if we need to.
+        if clear_row:
+            self.rows[y].remove(val)
+        if clear_col:
+            self.cols[x].remove(val)
+        if clear_box:
+            self.boxes[self.box_by_coords[(x, y)]].remove(val)
+        # Clear the cell
+        self._set_(x, y, 0)
+
+        # Scroll through the cleared conflicts and commit them to ensure they
+        # are represented in the grid properly.  It is possible for add() to do
+        # subsequent remove()s, so hold onto the cleared conflict list for the
+        # caller.
+        hold_conflicts = self.cleared_conflicts
+        for xx, yy in self.cleared_conflicts:
+            self.add(xx, yy, val, True)
+        self.cleared_conflicts = hold_conflicts
+
+
 class DifficultyRating:
 
     very_hard = _('Very Hard')



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