Save and restore functionality implemented
[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 all the clues for across
77 def get_clues_across (self):
78 clues = []
79 # traverse the grid
80 for row in range (self.rows):
81 for col in range (self.cols):
82 if (self.data[row][col].occupied_across is True and
83 self.data[row][col].across_start is True):
84 word_across = self.get_word_across (row, col)
85 clues.append ((word_across, self.data[row][col].clue_across))
86 # if no across words are found at all
87 if not clues:
88 raise NoWordsException
89
90 return clues
91
92 # get all the clues for down
93 def get_clues_down (self):
94 clues = []
95 # traverse the grid
96 for row in range (self.rows):
97 for col in range (self.cols):
98 if (self.data[row][col].occupied_down is True and
99 self.data[row][col].down_start is True):
100 word_down = self.get_word_down (row, col)
101 clues.append ((word_down, self.data[row][col].clue_down))
102 # if no down words are found at all
103 if not clues:
104 raise NoWordsException
105
106 return clues
107
108 # getting a down word at a position
109 def get_word_down (self, row, col):
110 # if index is out of bounds
111 if row >= self.rows or col >= self.cols:
112 raise NoWordException (row, col)
113
114 # if there is no occupied down letter at that position
115 if self.data[row][col].occupied_down is False:
116 raise NoWordException (row, col)
117
118 # now traverse the grid to find the beginning of the word
119 i = row
120 while i >= 0:
121 # if it is occupied down and is the beginning of the word
122 if (self.data[i][col].occupied_down is True and
123 self.data[i][col].down_start is True):
124 start_row = i
125 break
126 i -= 1
127
128 i = start_row
129 word_chars = []
130 # now seek the end of the word
131 while i < self.rows:
132 if self.data[i][col].occupied_down is True:
133 word_chars.append (self.data[i][col].char)
134 else:
135 break
136 i += 1
137
138 word = "".join (word_chars)
139
140 # return the word, starting row, column and length as a tuple
141 return (word, start_row, col, len(word))
142
143 # getting an across word at a position
144 def get_word_across (self, row, col):
145 # if index is out of bounds
146 if row >= self.rows or col >= self.cols:
147 raise NoWordException (row, col)
148
149 # if there is no occupied across letter at that position
150 if self.data[row][col].occupied_across is False:
151 raise NoWordException (row, col)
152
153 # now traverse the grid to look for the beginning of the word
154 i = col
155 while i >= 0:
156 # if it is occupied across and is the beginning of the word
157 if (self.data[row][i].occupied_across is True and
158 self.data[row][i].across_start is True):
159 start_col = i
160 break
161 i -= 1
162
163 i = start_col
164 word_chars = []
165 # now seek the end of the word
166 while i < self.cols:
167 if self.data[row][i].occupied_across is True:
168 word_chars.append (self.data[row][i].char)
169 else:
170 break
171 i += 1
172
173 word = "".join (word_chars)
174
175 # return the word, starting column, row and length as a tuple
176 return (word, start_col, row, len(word))
177
178 # setting a down word
179 def set_word_down (self, row, col, word):
180 # if the grid is frozen the abort
181 if self.frozen_grid is True:
182 raise FrozenGridException
183
184 # if the word length greater than totalrows - startrow
185 if len(word) > self.rows - row:
186 raise TooLongWordException (word, len(word))
187
188 # is the word intersecting any other word?
189 for i in range (len(word)):
190 # on the same column
191 if self.data[row+i][col].occupied_down is True:
192 raise IntersectWordException (word, len(word))
193 # on the previous column except first column
194 if col > 0:
195 # except the first and last col
196 if i > 0 and i < len(word) - 1:
197 if self.data[row+i][col-1].occupied_down is True:
198 raise IntersectWordException (word, len(word))
199 # on the next column except last column
200 if col < len(word) - 1:
201 # except the first and last row check if there is any
202 # down word in previous column
203 if i > 0 and i < len(word) - 1:
204 if self.data[row+i][col+1].occupied_down is True:
205 raise IntersectWordException (word, len(word))
206 # check if there is any across word starting in the
207 # next column
208 if self.data[row+i][col+1].across_start is True:
209 raise IntersectWordException (word, len(word))
210
211 # also check the character before and after
212 if (row > 0 and self.data[row-1][col].occupied_down is True
213 and self.data[row-1][col].occupied_across is True):
214 raise IntersectWordException (word, len(word))
215 if (row + len(word) < self.rows and
216 self.data[row+len(word)][col].occupied_across is True and
217 self.data[row+len(word)][col].occupied_down is True):
218 raise IntersectWordException (word, len(word))
219
220 # set the down start to true
221 self.data[row][col].down_start = True
222 # set the word
223 for i in range (len(word)):
224 self.data[row+i][col].occupied_down = True
225 self.data[row+i][col].char = word[i].upper ()
226
227
228 # setting an across word
229 def set_word_across (self, row, col, word):
230 # if the grid is frozen the abort
231 if self.frozen_grid is True:
232 raise FrozenGridException
233
234 # is the word length greater than totalcols - startcol?
235 if len(word) > self.cols - col:
236 raise TooLongWordException (word, len(word))
237
238 # is the word intersecting any other word?
239 for i in range (len(word)):
240 # on the same row
241 if self.data[row][col+i].occupied_across is True:
242 raise IntersectWordException (word, len(word))
243 # on a previous row except first row
244 if row > 0:
245 # if not the first or last col
246 if i > 0 and i < len(word) - 1:
247 if self.data[row-1][col+i].occupied_across is True:
248 raise IntersectWordException (word, len(word))
249 # on a next row
250 if (row < (self.rows - 1)):
251 # except the first and last letter check if there is
252 # any across intersection
253 if i > 0 and i < len (word) - 1:
254 if self.data[row+1][col+i].occupied_across is True:
255 raise IntersectWordException (word, len(word))
256 # if a down word is starting at any column below the
257 # word
258 if self.data[row+1][col+i].down_start is True:
259 raise IntersectWordException (word, len(word))
260
261 # also check the character beyond and before and after
262 if (col > 0 and (self.data[row][col-1].occupied_across is True or
263 self.data[row][col-1].occupied_down is True)):
264 raise IntersectWordException (word, len(word))
265 if (col + len(word) < self.cols and
266 (self.data[row][col+len(word)].occupied_across is True or
267 self.data[row][col+len(word)].occupied_down is True)):
268 raise IntersectWordException (word, len(word))
269
270 # set across start to true
271 self.data[row][col].across_start = True
272
273 # set the word
274 for i in range (len(word)):
275 self.data[row][col+i].char = word[i].upper ()
276 self.data[row][col+i].occupied_across = True
277
278 # freeze the grid numbers etc.
279 def freeze_grid (self):
280 # numbering
281 numbering = 1
282 # run through the grid
283 for row in range (self.rows):
284 for col in range (self.cols):
285 # if grid is blank set the character to #
286 if (self.data[row][col].occupied_across is False
287 and self.data[row][col].occupied_down is False):
288 self.data[row][col].char = "#"
289 elif (self.data[row][col].across_start is True or
290 self.data[row][col].down_start is True):
291 self.data[row][col].numbered = numbering
292 numbering += 1
293
294 self.frozen_grid = True
295
296 # unfreeze the grid numbers etc.
297 def unfreeze_grid (self):
298 # run through the grid
299 for row in range (self.rows):
300 for col in range (self.cols):
301 self.data[row][col].numbered = 0
302 if (self.data[row][col].occupied_across is False and
303 self.data[row][col].occupied_down is False):
304 self.data[row][col].char = '.'
305
306 self.frozen_grid = False
307