9617897fddada51883187489cdb2153e268ab6f6
[getaclue.git] / crosswordpuzzle.py
1 # Get A Clue (C) 2010 V. Harishankar
2 # Crossword puzzle maker program
3 # Licensed under the GNU GPL v3
4
5 # Class for the puzzle data representation
6
7 class GridItem:
8 # initialize the item
9 def __init__ (self, item_char='.', across_start = False, down_start = False,
10 occupied_across = False, occupied_down = False, num = 0,
11 clue_across = None, clue_down = None, revealed = False):
12 # character in the cell
13 self.char = item_char
14 # is the cell the start of an across word?
15 self.across_start = across_start
16 # is the cell the start of a down word?
17 self.down_start = down_start
18 # is the cell occupied by a letter in an across word?
19 self.occupied_across = occupied_across
20 # is the cell occupied by a letter in a down word?
21 self.occupied_down = occupied_down
22 # numbering of the cell if it is the start of a word
23 self.numbered = num
24 # clue across if the cell is the start of an across word
25 self.clue_across = clue_across
26 # clue down if the cell is the start of a down word
27 self.clue_down = clue_down
28 # is the letter revealed or hidden?
29 self.revealed = revealed
30
31 # exception for too long words
32 class TooLongWordException (Exception):
33 def __init__ (self, word, length):
34 self.word = word
35 self.length = length
36
37 # exception for intersecting words
38 class IntersectWordException (Exception):
39 def __init__ (self, word, length):
40 self.word = word
41 self.length = length
42
43 # exception when grid is sought to be changed when frozen
44 class FrozenGridException (Exception):
45 def __init__ (self):
46 self.msg = "Grid is frozen and cannot be edited"
47
48 # exception when no word is found at a position
49 class NoWordException (Exception):
50 def __init__ (self, row, col):
51 self.pos = (row, col)
52
53 class CrosswordPuzzle:
54 def __init__ (self, rows, cols):
55 # define number of rows and columns
56 self.rows = rows
57 self.cols = cols
58
59 # initialize the list to hold the grid
60 self.data = []
61
62 # initial state of the grid is unfrozen
63 self.frozen_grid = False
64
65 # create the grid data
66 for i in range (rows):
67 self.data.append ([])
68 for j in range (cols):
69 self.data[i].append (GridItem ())
70
71 # getting a down word at a position
72 def get_word_down (self, row, col):
73 # if index is out of bounds
74 if row >= self.rows or col >= self.cols:
75 raise NoWordException (row, col)
76
77 # if there is no occupied down letter at that position
78 if self.data[row][col].occupied_down is False:
79 raise NoWordException (row, col)
80
81 # now traverse the grid to find the beginning of the word
82 i = row
83 while i >= 0:
84 # if it is occupied down and is the beginning of the word
85 if (self.data[i][col].occupied_down is True and
86 self.data[i][col].down_start is True):
87 start_row = i
88 break
89 i -= 1
90
91 i = start_row
92 word_chars = []
93 # now seek the end of the word
94 while i < self.rows:
95 if self.data[i][col].occupied_down is True:
96 word_chars.append (self.data[i][col].char)
97 else:
98 break
99 i += 1
100
101 word = "".join (word_chars)
102
103 # return the word, starting row, column and length as a tuple
104 return (word, start_row, col, len(word))
105
106 # getting an across word at a position
107 def get_word_across (self, row, col):
108 # if index is out of bounds
109 if row >= self.rows or col >= self.cols:
110 raise NoWordException (row, col)
111
112 # if there is no occupied across letter at that position
113 if self.data[row][col].occupied_across is False:
114 raise NoWordException (row, col)
115
116 # now traverse the grid to look for the beginning of the word
117 i = col
118 while i >= 0:
119 # if it is occupied across and is the beginning of the word
120 if (self.data[row][i].occupied_across is True and
121 self.data[row][i].across_start is True):
122 start_col = i
123 break
124 i -= 1
125
126 i = start_col
127 word_chars = []
128 # now seek the end of the word
129 while i < self.cols:
130 if self.data[row][i].occupied_across is True:
131 word_chars.append (self.data[row][i].char)
132 else:
133 break
134 i += 1
135
136 word = "".join (word_chars)
137
138 # return the word, starting column, row and length as a tuple
139 return (word, start_col, row, len(word))
140
141 # setting a down word
142 def set_word_down (self, row, col, word):
143 # if the grid is frozen the abort
144 if self.frozen_grid is True:
145 raise FrozenGridException
146
147 # if the word length greater than totalrows - startrow
148 if len(word) > self.rows - row:
149 raise TooLongWordException (word, len(word))
150
151 # is the word intersecting any other word?
152 for i in range (len(word)):
153 # on the same column
154 if self.data[row+i][col].occupied_down is True:
155 raise IntersectWordException (word, len(word))
156 # on the previous column except first column
157 if col > 0:
158 # except the first and last col
159 if i > 0 and i < len(word) - 1:
160 if self.data[row+i][col-1].occupied_down is True:
161 raise IntersectWordException (word, len(word))
162 # on the next column except last column
163 if col < len(word) - 1:
164 # except the first and last row check if there is any
165 # down word in previous column
166 if i > 0 and i < len(word) - 1:
167 if self.data[row+i][col+1].occupied_down is True:
168 raise IntersectWordException (word, len(word))
169 # check if there is any across word starting in the
170 # next column
171 if self.data[row+i][col+1].across_start is True:
172 raise IntersectWordException (word, len(word))
173
174 # also check the character before and after
175 if (row > 0 and self.data[row-1][col].occupied_down is True
176 and self.data[row-1][col].occupied_across is True):
177 raise IntersectWordException (word, len(word))
178 if (row + len(word) < self.rows and
179 self.data[row+len(word)][col].occupied_across is True and
180 self.data[row+len(word)][col].occupied_down is True):
181 raise IntersectWordException (word, len(word))
182
183 # set the down start to true
184 self.data[row][col].down_start = True
185 # set the word
186 for i in range (len(word)):
187 self.data[row+i][col].occupied_down = True
188 self.data[row+i][col].char = word[i].upper ()
189
190
191 # setting an across word
192 def set_word_across (self, row, col, word):
193 # if the grid is frozen the abort
194 if self.frozen_grid is True:
195 raise FrozenGridException
196
197 # is the word length greater than totalcols - startcol?
198 if len(word) > self.cols - col:
199 raise TooLongWordException (word, len(word))
200
201 # is the word intersecting any other word?
202 for i in range (len(word)):
203 # on the same row
204 if self.data[row][col+i].occupied_across is True:
205 raise IntersectWordException (word, len(word))
206 # on a previous row except first row
207 if row > 0:
208 # if not the first or last col
209 if i > 0 and i < len(word) - 1:
210 if self.data[row-1][col+i].occupied_across is True:
211 raise IntersectWordException (word, len(word))
212 # on a next row
213 if (row < (self.rows - 1)):
214 # except the first and last letter check if there is
215 # any across intersection
216 if i > 0 and i < len (word) - 1:
217 if self.data[row+1][col+i].occupied_across is True:
218 raise IntersectWordException (word, len(word))
219 # if a down word is starting at any column below the
220 # word
221 if self.data[row+1][col+i].down_start is True:
222 raise IntersectWordException (word, len(word))
223
224 # also check the character beyond and before and after
225 if (col > 0 and (self.data[row][col-1].occupied_across is True or
226 self.data[row][col-1].occupied_down is True)):
227 raise IntersectWordException (word, len(word))
228 if (col + len(word) < self.cols and
229 (self.data[row][col+len(word)].occupied_across is True or
230 self.data[row][col+len(word)].occupied_down is True)):
231 raise IntersectWordException (word, len(word))
232
233 # set across start to true
234 self.data[row][col].across_start = True
235
236 # set the word
237 for i in range (len(word)):
238 self.data[row][col+i].char = word[i].upper ()
239 self.data[row][col+i].occupied_across = True
240
241 # freeze the grid numbers etc.
242 def freeze_grid (self):
243 # numbering
244 numbering = 1
245 # run through the grid
246 for row in range (self.rows):
247 for col in range (self.cols):
248 # if grid is blank set the character to #
249 if (self.data[row][col].occupied_across is False
250 and self.data[row][col].occupied_down is False):
251 self.data[row][col].char = "#"
252 elif (self.data[row][col].across_start is True or
253 self.data[row][col].down_start is True):
254 self.data[row][col].numbered = numbering
255 numbering += 1
256
257 self.frozen_grid = True
258
259 # unfreeze the grid numbers etc.
260 def unfreeze_grid (self):
261 # run through the grid
262 for row in range (self.rows):
263 for col in range (self.cols):
264 self.data[row][col].numbered = 0
265 if (self.data[row][col].occupied_across is False and
266 self.data[row][col].occupied_down is False):
267 self.data[row][col].char = '.'
268
269 self.frozen_grid = False
270