012450f07e320e1a989cf77e9f380f4baf0bf4ae
[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 # exception when no words are present in the grid
54 class NoWordsException (Exception):
55 def __init__ (self):
56 self.msg = "No words in grid"
57
58 class CrosswordPuzzle:
59 def __init__ (self, rows, cols):
60 # define number of rows and columns
61 self.rows = rows
62 self.cols = cols
63
64 # initialize the list to hold the grid
65 self.data = []
66
67 # initial state of the grid is unfrozen
68 self.frozen_grid = False
69
70 # create the grid data
71 for i in range (rows):
72 self.data.append ([])
73 for j in range (cols):
74 self.data[i].append (GridItem ())
75
76 # get the AcrossLite(TM) data for exporting
77 def export_acrosslite (self, title, author, copyright):
78 # don't export if grid is frozen
79 if self.frozen_grid is False:
80 raise FrozenGridException
81
82 across_data = []
83 across_data.append ("<ACROSS PUZZLE>\n")
84 across_data.append ("<TITLE>\n")
85 across_data.append (title + "\n")
86 across_data.append ("<AUTHOR>\n")
87 across_data.append (author + "\n")
88 across_data.append ("<COPYRIGHT>\n")
89 across_data.append (copyright + "\n")
90 across_data.append ("<SIZE>\n")
91 str_size = str (self.cols) + "x" + str (self.rows)
92 across_data.append (str_size + "\n")
93 across_data.append ("<GRID>\n")
94 for row in range (self.rows):
95 for col in range (self.cols):
96 if (self.data[row][col].occupied_across is True or
97 self.data[row][col].occupied_down is True):
98 across_data.append (self.data[row][col].char)
99 else:
100 across_data.append (".")
101 across_data.append ("\n")
102
103 across_data.append ("<ACROSS>\n")
104 clues_across = self.get_clues_across ()
105 for word, clue in clues_across:
106 if clue:
107 across_data.append (clue + "\n")
108 else:
109 across_data.append ("(No clue yet)\n")
110
111 across_data.append ("<DOWN>\n")
112 clues_down = self.get_clues_down ()
113 for word, clue in clues_down:
114 if clue:
115 across_data.append (clue + "\n")
116 else:
117 across_data.append ("(No clue yet\n")
118
119 acrosslite_str = "".join (across_data)
120 return acrosslite_str
121
122 # get all the clues for across
123 def get_clues_across (self):
124 clues = []
125 # traverse the grid
126 for row in range (self.rows):
127 for col in range (self.cols):
128 if (self.data[row][col].occupied_across is True and
129 self.data[row][col].across_start is True):
130 word_across = self.get_word_across (row, col)
131 clues.append ((word_across, self.data[row][col].clue_across))
132 # if no across words are found at all
133 if not clues:
134 raise NoWordsException
135
136 return clues
137
138 # get all the clues for down
139 def get_clues_down (self):
140 clues = []
141 # traverse the grid
142 for row in range (self.rows):
143 for col in range (self.cols):
144 if (self.data[row][col].occupied_down is True and
145 self.data[row][col].down_start is True):
146 word_down = self.get_word_down (row, col)
147 clues.append ((word_down, self.data[row][col].clue_down))
148 # if no down words are found at all
149 if not clues:
150 raise NoWordsException
151
152 return clues
153
154 # getting a down word at a position
155 def get_word_down (self, row, col):
156 # if index is out of bounds
157 if row >= self.rows or col >= self.cols:
158 raise NoWordException (row, col)
159
160 # if there is no occupied down letter at that position
161 if self.data[row][col].occupied_down is False:
162 raise NoWordException (row, col)
163
164 # now traverse the grid to find the beginning of the word
165 i = row
166 while i >= 0:
167 # if it is occupied down and is the beginning of the word
168 if (self.data[i][col].occupied_down is True and
169 self.data[i][col].down_start is True):
170 start_row = i
171 break
172 i -= 1
173
174 i = start_row
175 word_chars = []
176 # now seek the end of the word
177 while i < self.rows:
178 if self.data[i][col].occupied_down is True:
179 word_chars.append (self.data[i][col].char)
180 else:
181 break
182 i += 1
183
184 word = "".join (word_chars)
185
186 # return the word, starting row, column and length as a tuple
187 return (word, start_row, col, len(word))
188
189 # getting an across word at a position
190 def get_word_across (self, row, col):
191 # if index is out of bounds
192 if row >= self.rows or col >= self.cols:
193 raise NoWordException (row, col)
194
195 # if there is no occupied across letter at that position
196 if self.data[row][col].occupied_across is False:
197 raise NoWordException (row, col)
198
199 # now traverse the grid to look for the beginning of the word
200 i = col
201 while i >= 0:
202 # if it is occupied across and is the beginning of the word
203 if (self.data[row][i].occupied_across is True and
204 self.data[row][i].across_start is True):
205 start_col = i
206 break
207 i -= 1
208
209 i = start_col
210 word_chars = []
211 # now seek the end of the word
212 while i < self.cols:
213 if self.data[row][i].occupied_across is True:
214 word_chars.append (self.data[row][i].char)
215 else:
216 break
217 i += 1
218
219 word = "".join (word_chars)
220
221 # return the word, starting column, row and length as a tuple
222 return (word, row, start_col, len(word))
223
224 # setting a down word
225 def set_word_down (self, row, col, word):
226 # if the grid is frozen the abort
227 if self.frozen_grid is True:
228 raise FrozenGridException
229
230 # if the word length greater than totalrows - startrow
231 if len(word) > self.rows - row:
232 raise TooLongWordException (word, len(word))
233
234 # is the word intersecting any other word?
235 for i in range (len(word)):
236 # on the same column
237 if self.data[row+i][col].occupied_down is True:
238 raise IntersectWordException (word, len(word))
239 # on the previous column except first column
240 if col > 0:
241 # except the first and last col
242 if i > 0 and i < len(word) - 1:
243 if self.data[row+i][col-1].occupied_down is True:
244 raise IntersectWordException (word, len(word))
245 # on the next column except last column
246 if col < len(word) - 1:
247 # except the first and last row check if there is any
248 # down word in previous column
249 if i > 0 and i < len(word) - 1:
250 if self.data[row+i][col+1].occupied_down is True:
251 raise IntersectWordException (word, len(word))
252 # check if there is any across word starting in the
253 # next column
254 if self.data[row+i][col+1].across_start is True:
255 raise IntersectWordException (word, len(word))
256
257 # also check the character before and after
258 if (row > 0 and self.data[row-1][col].occupied_down is True
259 and self.data[row-1][col].occupied_across is True):
260 raise IntersectWordException (word, len(word))
261 if (row + len(word) < self.rows and
262 self.data[row+len(word)][col].occupied_across is True and
263 self.data[row+len(word)][col].occupied_down is True):
264 raise IntersectWordException (word, len(word))
265
266 # set the down start to true
267 self.data[row][col].down_start = True
268 # set the word
269 for i in range (len(word)):
270 self.data[row+i][col].occupied_down = True
271 self.data[row+i][col].char = word[i].upper ()
272
273
274 # setting an across word
275 def set_word_across (self, row, col, word):
276 # if the grid is frozen the abort
277 if self.frozen_grid is True:
278 raise FrozenGridException
279
280 # is the word length greater than totalcols - startcol?
281 if len(word) > self.cols - col:
282 raise TooLongWordException (word, len(word))
283
284 # is the word intersecting any other word?
285 for i in range (len(word)):
286 # on the same row
287 if self.data[row][col+i].occupied_across is True:
288 raise IntersectWordException (word, len(word))
289 # on a previous row except first row
290 if row > 0:
291 # if not the first or last col
292 if i > 0 and i < len(word) - 1:
293 if self.data[row-1][col+i].occupied_across is True:
294 raise IntersectWordException (word, len(word))
295 # on a next row
296 if (row < (self.rows - 1)):
297 # except the first and last letter check if there is
298 # any across intersection
299 if i > 0 and i < len (word) - 1:
300 if self.data[row+1][col+i].occupied_across is True:
301 raise IntersectWordException (word, len(word))
302 # if a down word is starting at any column below the
303 # word
304 if self.data[row+1][col+i].down_start is True:
305 raise IntersectWordException (word, len(word))
306
307 # also check the character beyond and before and after
308 if (col > 0 and (self.data[row][col-1].occupied_across is True or
309 self.data[row][col-1].occupied_down is True)):
310 raise IntersectWordException (word, len(word))
311 if (col + len(word) < self.cols and
312 (self.data[row][col+len(word)].occupied_across is True or
313 self.data[row][col+len(word)].occupied_down is True)):
314 raise IntersectWordException (word, len(word))
315
316 # set across start to true
317 self.data[row][col].across_start = True
318
319 # set the word
320 for i in range (len(word)):
321 self.data[row][col+i].char = word[i].upper ()
322 self.data[row][col+i].occupied_across = True
323
324 # freeze the grid numbers etc.
325 def freeze_grid (self):
326 # numbering
327 numbering = 1
328 # run through the grid
329 for row in range (self.rows):
330 for col in range (self.cols):
331 # if grid is blank set the character to #
332 if (self.data[row][col].occupied_across is False
333 and self.data[row][col].occupied_down is False):
334 self.data[row][col].char = "#"
335 elif (self.data[row][col].across_start is True or
336 self.data[row][col].down_start is True):
337 self.data[row][col].numbered = numbering
338 numbering += 1
339
340 self.frozen_grid = True
341
342 # unfreeze the grid numbers etc.
343 def unfreeze_grid (self):
344 # run through the grid
345 for row in range (self.rows):
346 for col in range (self.cols):
347 self.data[row][col].numbered = 0
348 if (self.data[row][col].occupied_across is False and
349 self.data[row][col].occupied_down is False):
350 self.data[row][col].char = '.'
351
352 self.frozen_grid = False
353