Package: xword
Version: 1.0-3
Severity: wishlist
Tags: patch

xword assumes that crosswords it reads are American-style, i.e. that any
square that has a black square above it or to its left is at the beginning of
a new solution, which isn't true of British-style crosswords (see
http://en.wikipedia.org/wiki/Crossword#Types_of_grid). Hence it gets very
confused when loading a British-style crossword.

Here is a series of patches that add support for British-style layouts, plus
one fix for keypress handling and and one patch that adds a test suite. These
are taken from my git repository at (http://rhydd.org/git/xword/).
>From fdfb5a210da7f4596323330e72b1fc190611f40e Mon Sep 17 00:00:00 2001
From: Dafydd Harries <d...@rhydd.org>
Date: Sun, 3 May 2009 16:39:40 +0300
Subject: [PATCH 1/6] handle British-style crossword layouts

When allocating numbers to cells, allocate a new number to a cell if the cell:

 - is white;
 - has not already been allocated a number;
 - and it it followed by white cell either to the right or downwards.

When allocating a number, allocate the same number to consecutive white cells
to the right or downwards. Previously, a new number was always allocated if
the cell above or to the left was black.
---
 xword |   61 ++++++++++++++++++++++++++++++++++++-------------------------
 1 files changed, 36 insertions(+), 25 deletions(-)

diff --git a/xword b/xword
index 1fde0fa..8da2492 100755
--- a/xword
+++ b/xword
@@ -263,35 +263,39 @@ class Puzzle:
         number = 1
         for y in range(self.height):
             for x in range(self.width):
-                is_fresh_x = self.is_black(x-1, y)
-                is_fresh_y = self.is_black(x, y-1)
+                new_across = False
+                new_down = False
 
                 if not self.is_black(x, y):
-                    if is_fresh_x:
-                        self.across_map[x, y] = number
-                        if self.is_black(x+1, y):
-                            self.across_clues[number] = ''
-                        else:
-                            self.across_clues[number] = self.clues.pop(0)
-                    else: self.across_map[x, y] = self.across_map[x-1, y]
+                    if ((x, y) not in self.across_map and
+                            not self.is_black(x+1, y)):
+                        new_across = True
+                        self.across_clues[number] = self.clues.pop(0)
+
+                        for x_ in range(x, self.width):
+                            if self.is_black(x_, y):
+                                break
+
+                            self.across_map[x_, y] = number
                     
-                    if is_fresh_y:
-                        self.down_map[x, y] = number
-                        if self.is_black(x, y+1): # see April 30, 2006 puzzle
-                            self.down_clues[number] = ''
-                        else:
-                            self.down_clues[number] = self.clues.pop(0)
-                    else: self.down_map[x, y] = self.down_map[x, y-1]
-
-                    if is_fresh_x or is_fresh_y:
-                        self.is_across[number] = is_fresh_x
-                        self.is_down[number] = is_fresh_y
+                    if ((x, y) not in self.down_map and
+                            # see April 30, 2006 puzzle
+                            not self.is_black(x, y+1)):
+                        new_down = True
+                        self.down_clues[number] = self.clues.pop(0)
+
+                        for y_ in range(y, self.height):
+                            if self.is_black(x, y_):
+                                break
+
+                            self.down_map[x, y_] = number
+
+                    if new_across or new_down:
+                        self.is_across[number] = new_across
+                        self.is_down[number] = new_down
                         self.number_map[number] = (x, y)
                         self.number_rev_map[x, y] = number
                         number += 1
-                else:
-                    self.across_map[x, y] = 0
-                    self.down_map[x, y] = 0
         self.max_number = number-1
 
     def hashcode(self):
@@ -996,8 +1000,15 @@ class PuzzleController:
                 self.do_update('box-update', xp, yp)
 
             self.do_update('title-update')
-            self.do_update('across-update', self.puzzle.number(x, y, ACROSS))
-            self.do_update('down-update', self.puzzle.number(x, y, DOWN))
+
+            if (x, y) not in self.puzzle.mode_maps[self.mode]:
+                self.switch_mode()
+
+            if (x, y) in self.puzzle.across_map:
+                self.do_update('across-update', self.puzzle.number(x, y, 
ACROSS))
+
+            if (x, y) in self.puzzle.down_map:
+                self.do_update('down-update', self.puzzle.number(x, y, DOWN))
 
     def select_word(self, mode, n):
         if mode <> self.mode: self.switch_mode()
-- 
1.6.2.4

>From 5a2ae0ef3d6c64384863a37df5b371df49c3884f Mon Sep 17 00:00:00 2001
From: Dafydd Harries <d...@rhydd.org>
Date: Sun, 3 May 2009 16:45:38 +0300
Subject: [PATCH 2/6] when switching modes, jump to the first clue of the other 
mode, not clue 1

In US-style crosswords, both across and down modes have a clue 1, but this
assumption breaks in British-style crosswords.
---
 xword |   10 +++++++++-
 1 files changed, 9 insertions(+), 1 deletions(-)

diff --git a/xword b/xword
index 8da2492..da69d8c 100755
--- a/xword
+++ b/xword
@@ -382,6 +382,14 @@ class Puzzle:
             if mode == DOWN and self.is_down[n]: break
         return n
 
+    def first_number(self, mode):
+        n = 1
+        while True:
+            if mode == ACROSS and self.is_across[n]: break
+            if mode == DOWN and self.is_down[n]: break
+            n += 1
+        return n
+
     def final_number(self, mode):
         n = self.max_number
         while True:
@@ -1048,7 +1056,7 @@ class PuzzleController:
         n = self.puzzle.incr_number(self.x, self.y, self.mode, incr)
         if n == 0:
             self.switch_mode()
-            if incr == 1: n = 1
+            if incr == 1: n = self.puzzle.first_number(self.mode)
             else: n = self.puzzle.final_number(self.mode)
         (x, y) = self.puzzle.number_map[n]
         (x, y) = self.puzzle.find_blank_cell(x, y, self.mode, 1)
-- 
1.6.2.4

>From 7d9e8dd3b6fef2d2ffbfebc1c512d39639df7bd1 Mon Sep 17 00:00:00 2001
From: Dafydd Harries <d...@rhydd.org>
Date: Sun, 3 May 2009 16:44:15 +0300
Subject: [PATCH 3/6] when opening a puzzle, move to the first across clue 
instead of the first white cell

In US-style crosswords, the first white cell is always in an across clue, but
this assumption breaks in British-style crosswords.
---
 xword |    4 +---
 1 files changed, 1 insertions(+), 3 deletions(-)

diff --git a/xword b/xword
index da69d8c..2bcd2b5 100755
--- a/xword
+++ b/xword
@@ -946,9 +946,7 @@ class PuzzleController:
         self.selection = []
 
         self.mode = ACROSS
-        (x, y) = (0, 0)
-        if puzzle.is_black(x, y):
-            ((x, y), _) = puzzle.next_cell(0, 0, ACROSS, 1, True)
+        x, y = self.puzzle.number_map[self.puzzle.first_number(ACROSS)]
         self.move_to(x, y)
 
     def connect(self, ev, handler):
-- 
1.6.2.4

>From d8b3cf71ceea05b765f4379867d45a295b6beb7a Mon Sep 17 00:00:00 2001
From: Dafydd Harries <d...@rhydd.org>
Date: Sun, 3 May 2009 17:24:53 +0300
Subject: [PATCH 4/6] don't switch mode when moving unless the current cell 
exists in the other mode

In British-style puzzles, some cells only exist in one of the two modes.
---
 xword |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/xword b/xword
index 2bcd2b5..996870d 100755
--- a/xword
+++ b/xword
@@ -1040,7 +1040,7 @@ class PuzzleController:
             ((x, y), _) = self.puzzle.next_cell(self.x, self.y,
                                                 self.mode, amt, skip_black)
             self.move_to(x, y)
-        else:
+        elif (self.x, self.y) in self.puzzle.mode_maps[1 - self.mode]:
             self.switch_mode()
 
     def back_space(self):
-- 
1.6.2.4

>From a886fea203e96e6fd0ec1c15170867e4943215c6 Mon Sep 17 00:00:00 2001
From: Dafydd Harries <d...@rhydd.org>
Date: Sun, 3 May 2009 17:24:25 +0300
Subject: [PATCH 5/6] ignore keypresses that can't be named

---
 xword |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)

diff --git a/xword b/xword
index 996870d..a0775c6 100755
--- a/xword
+++ b/xword
@@ -1700,6 +1700,8 @@ class PuzzleWindow:
 
     def puzzle_key_event(self, item, event):
         name = gtk.gdk.keyval_name(event.keyval)
+        if name is None:
+            return False
         c = self.control
         if len(name) is 1 and name.isalpha():
             c.input_char(self.skip_filled, name)
-- 
1.6.2.4

>From 068c8f2d17e26a57a8ac35e3679a12aea6fb2e41 Mon Sep 17 00:00:00 2001
From: Dafydd Harries <d...@rhydd.org>
Date: Sun, 3 May 2009 19:46:18 +0300
Subject: [PATCH 6/6] add a rudimentary test suite

---
 test.py |  128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 128 insertions(+), 0 deletions(-)
 create mode 100644 test.py

diff --git a/test.py b/test.py
new file mode 100644
index 0000000..6d558c8
--- /dev/null
+++ b/test.py
@@ -0,0 +1,128 @@
+
+import unittest
+
+def read_file(path):
+    fh = file(path)
+    contents = fh.read()
+    fh.close()
+    return contents
+
+def load_module(name, path):
+    # hacketty hack
+
+    mod = __builtins__.__class__(name)
+    code = compile(read_file(path), path, 'exec')
+
+    g, l = {}, {}
+    exec code in g, l
+
+    for k, v in l.iteritems():
+        setattr(mod, k, v)
+
+    return mod
+
+xword = load_module('xword', 'xword')
+
+class TestPuzzle(xword.Puzzle):
+    def __init__(self):
+        # Override Puzzle.__init__ to avoid having to read a file.
+        pass
+
+def dump_map(width, height, m):
+    return ''.join([
+        ' '.join([
+            '%s' % m.get((x, y), '#')
+            for x in xrange(width)]) + '\n'
+        for y in xrange(height)])
+
+class SetupTest(unittest.TestCase):
+    def test_american(self):
+        puzzle = TestPuzzle()
+        puzzle.width = 5
+        puzzle.height = 5
+        puzzle.clues = [
+            'a1', 'd1', 'd2', 'd3', 'a4', 'd5', 'a6', 'd7', 'a8', 'a9']
+        puzzle.responses = dict([
+            ((x, y), ' .'[int(x + y == 4 and x != 2 and y != 2)])
+            for x in xrange(5)
+            for y in xrange(5)])
+
+        puzzle.setup()
+
+        self.assertEquals([], puzzle.clues)
+
+        self.assertEquals(
+            {1: True, 2: False, 3: False, 4: True, 5: False, 6: True, 7: False,
+             8: True, 9: True},
+            puzzle.is_across)
+        self.assertEquals(
+            {8: 'a8', 1: 'a1', 4: 'a4', 6: 'a6', 9: 'a9'},
+            puzzle.across_clues)
+        self.assertEquals(
+            '1 1 1 1 #\n'
+            '4 4 4 # #\n'
+            '6 6 6 6 6\n'
+            '# # 8 8 8\n'
+            '# 9 9 9 9\n',
+            dump_map(puzzle.width, puzzle.height, puzzle.across_map))
+
+        self.assertEquals(
+            {1: True, 2: True, 3: True, 4: False, 5: True, 6: False, 7: True,
+             8: False, 9: False},
+            puzzle.is_down)
+        self.assertEquals(
+            {1: 'd1', 2: 'd2', 3: 'd3', 5: 'd5', 7: 'd7'},
+            puzzle.down_clues)
+        self.assertEquals(
+            '1 2 3 # #\n'
+            '1 2 3 # 5\n'
+            '1 2 3 7 5\n'
+            '1 # 3 7 5\n'
+            '# # 3 7 5\n',
+            dump_map(puzzle.width, puzzle.height, puzzle.down_map))
+
+    def test_british(self):
+        puzzle = TestPuzzle()
+        puzzle.width = 5
+        puzzle.height = 5
+        puzzle.clues = ['a1', 'd1', 'd2', 'd3', 'a4', 'a5']
+        puzzle.responses = dict([
+            ((x, y), ' .'[int(x % 2 != 0 and y % 2 != 0)])
+            for x in xrange(5)
+            for y in xrange(5)])
+
+        puzzle.setup()
+
+        self.assertEquals([], puzzle.clues)
+
+        self.assertEquals(
+            {1: True, 2: False, 3: False, 4: True, 5: True},
+            puzzle.is_across)
+        self.assertEquals(
+            {1: 'a1', 4: 'a4', 5: 'a5'},
+            puzzle.across_clues)
+        self.assertEquals(
+            '1 1 1 1 1\n'
+            '# # # # #\n'
+            '4 4 4 4 4\n'
+            '# # # # #\n'
+            '5 5 5 5 5\n',
+            dump_map(puzzle.width, puzzle.height, puzzle.across_map))
+
+        self.assertEquals(
+            {1: True, 2: True, 3: True, 4: False, 5: False},
+            puzzle.is_down)
+        self.assertEquals(
+            {1: 'd1', 2: 'd2', 3: 'd3'},
+            puzzle.down_clues)
+        self.assertEquals(
+            '1 # 2 # 3\n'
+            '1 # 2 # 3\n'
+            '1 # 2 # 3\n'
+            '1 # 2 # 3\n'
+            '1 # 2 # 3\n',
+            dump_map(puzzle.width, puzzle.height, puzzle.down_map))
+
+if __name__ == '__main__':
+    unittest.main()
+
-- 
1.6.2.4

Reply via email to