Export as image functionality halfway done
[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 import cairo
8
9 class GridItem:
10 # initialize the item
11 def __init__ (self, item_char='.', across_start = False, down_start = False,
12 occupied_across = False, occupied_down = False, num = 0,
13 clue_across = None, clue_down = None, revealed = False):
14 # character in the cell
15 self.char = item_char
16 # is the cell the start of an across word?
17 self.across_start = across_start
18 # is the cell the start of a down word?
19 self.down_start = down_start
20 # is the cell occupied by a letter in an across word?
21 self.occupied_across = occupied_across
22 # is the cell occupied by a letter in a down word?
23 self.occupied_down = occupied_down
24 # numbering of the cell if it is the start of a word
25 self.numbered = num
26 # clue across if the cell is the start of an across word
27 self.clue_across = clue_across
28 # clue down if the cell is the start of a down word
29 self.clue_down = clue_down
30 # is the letter revealed or hidden?
31 self.revealed = revealed
32
33 # exception for too long words
34 class TooLongWordException (Exception):
35 def __init__ (self, word, length):
36 self.word = word
37 self.length = length
38
39 # exception for intersecting words
40 class IntersectWordException (Exception):
41 def __init__ (self, word, length):
42 self.word = word
43 self.length = length
44
45 # exception when grid is sought to be changed when frozen
46 class FrozenGridException (Exception):
47 def __init__ (self):
48 self.msg = "Grid is frozen and cannot be edited"
49
50 # exception when no word is found at a position
51 class NoWordException (Exception):
52 def __init__ (self, row, col):
53 self.pos = (row, col)
54
55 # exception when no words are present in the grid
56 class NoWordsException (Exception):
57 def __init__ (self):
58 self.msg = "No words in grid"
59
60 class CrosswordPuzzle:
61 def __init__ (self, rows, cols):
62 # define number of rows and columns
63 self.rows = rows
64 self.cols = cols
65
66 # initialize the list to hold the grid
67 self.data = []
68
69 # initial state of the grid is unfrozen
70 self.frozen_grid = False
71
72 # create the grid data
73 for i in range (rows):
74 self.data.append ([])
75 for j in range (cols):
76 self.data[i].append (GridItem ())
77
78 # export to an image
79 def export_image (self, filename, solution=False):
80 # don't export if grid is not frozen
81 if self.frozen_grid is False:
82 raise FrozenGridException
83
84 # create cairo image surface and context
85 px = 30
86 surf = cairo.ImageSurface (cairo.FORMAT_RGB24, self.cols*px, self.rows*px)
87 ctx = cairo.Context (surf)
88
89 ctx.set_source_rgb (1, 1, 1)
90 ctx.rectangle (0, 0, self.cols*px, self.rows*px)
91 ctx.fill ()
92
93 # traverse through the grid
94 for row in range (self.rows):
95 for col in range (self.cols):
96 # if grid is un-occupied
97 if (self.data[row][col].occupied_across is False and
98 self.data[row][col].occupied_down is False):
99 ctx.set_source_rgb (0, 0, 0)
100 ctx.rectangle (col*px, row*px, px, px)
101 ctx.fill ()
102 # grid is occupied
103 else:
104 ctx.set_source_rgb (1, 1, 1)
105 ctx.rectangle (col*px, row*px, px, px)
106 ctx.fill ()
107 ctx.set_source_rgb (0, 0, 0)
108 ctx.rectangle (col*px, row*px, px, px)
109 ctx.stroke ()
110 # if solution is not to be provided, number the grid
111 if solution is False:
112 if self.data[row][col].numbered <> 0:
113 ctx.select_font_face ("Serif")
114 ctx.set_font_size (10)
115 ctx.move_to (col*px+5, row*px+10)
116 ctx.show_text (str(self.data[row][col].numbered))
117 # display the words
118 else:
119 ctx.select_font_face ("Serif")
120 ctx.set_font_size (16)
121 ctx.move_to (col*px+10, row*px+20)
122 ctx.show_text (self.data[row][col].char)
123
124 surf.write_to_png (open (filename, "wb"))
125
126 # get the AcrossLite(TM) data for exporting
127 def export_acrosslite (self, title, author, copyright):
128 # don't export if grid is not frozen
129 if self.frozen_grid is False:
130 raise FrozenGridException
131
132 across_data = []
133 across_data.append ("<ACROSS PUZZLE>\r\n")
134 across_data.append ("<TITLE>\r\n")
135 across_data.append (title + "\r\n")
136 across_data.append ("<AUTHOR>\r\n")
137 across_data.append (author + "\r\n")
138 across_data.append ("<COPYRIGHT>\r\n")
139 across_data.append (copyright + "\r\n")
140 across_data.append ("<SIZE>\r\n")
141 str_size = str (self.cols) + "x" + str (self.rows)
142 across_data.append (str_size + "\r\n")
143 across_data.append ("<GRID>\r\n")
144 for row in range (self.rows):
145 for col in range (self.cols):
146 if (self.data[row][col].occupied_across is True or
147 self.data[row][col].occupied_down is True):
148 across_data.append (self.data[row][col].char)
149 else:
150 across_data.append (".")
151 across_data.append ("\r\n")
152
153 across_data.append ("<ACROSS>\r\n")
154 clues_across = self.get_clues_across ()
155 for word, clue in clues_across:
156 if clue:
157 across_data.append (clue + "\r\n")
158 else:
159 across_data.append ("(No clue yet)\r\n")
160
161 across_data.append ("<DOWN>\r\n")
162 clues_down = self.get_clues_down ()
163 for word, clue in clues_down:
164 if clue:
165 across_data.append (clue + "\r\n")
166 else:
167 across_data.append ("(No clue yet\r\n")
168
169 acrosslite_str = "".join (across_data)
170 return acrosslite_str
171
172 # get all the clues for across
173 def get_clues_across (self):
174 clues = []
175 # traverse the grid
176 for row in range (self.rows):
177 for col in range (self.cols):
178 if (self.data[row][col].occupied_across is True and
179 self.data[row][col].across_start is True):
180 word_across = self.get_word_across (row, col)
181 clues.append ((word_across, self.data[row][col].clue_across))
182 # if no across words are found at all
183 if not clues:
184 raise NoWordsException
185
186 return clues
187
188 # get all the clues for down
189 def get_clues_down (self):
190 clues = []
191 # traverse the grid
192 for row in range (self.rows):
193 for col in range (self.cols):
194 if (self.data[row][col].occupied_down is True and
195 self.data[row][col].down_start is True):
196 word_down = self.get_word_down (row, col)
197 clues.append ((word_down, self.data[row][col].clue_down))
198 # if no down words are found at all
199 if not clues:
200 raise NoWordsException
201
202 return clues
203
204 # getting a down word at a position
205 def get_word_down (self, row, col):
206 # if index is out of bounds
207 if row >= self.rows or col >= self.cols:
208 raise NoWordException (row, col)
209
210 # if there is no occupied down letter at that position
211 if self.data[row][col].occupied_down is False:
212 raise NoWordException (row, col)
213
214 # now traverse the grid to find the beginning of the word
215 i = row
216 while i >= 0:
217 # if it is occupied down and is the beginning of the word
218 if (self.data[i][col].occupied_down is True and
219 self.data[i][col].down_start is True):
220 start_row = i
221 break
222 i -= 1
223
224 i = start_row
225 word_chars = []
226 # now seek the end of the word
227 while i < self.rows:
228 if self.data[i][col].occupied_down is True:
229 word_chars.append (self.data[i][col].char)
230 else:
231 break
232 i += 1
233
234 word = "".join (word_chars)
235
236 # return the word, starting row, column and length as a tuple
237 return (word, start_row, col, len(word))
238
239 # getting an across word at a position
240 def get_word_across (self, row, col):
241 # if index is out of bounds
242 if row >= self.rows or col >= self.cols:
243 raise NoWordException (row, col)
244
245 # if there is no occupied across letter at that position
246 if self.data[row][col].occupied_across is False:
247 raise NoWordException (row, col)
248
249 # now traverse the grid to look for the beginning of the word
250 i = col
251 while i >= 0:
252 # if it is occupied across and is the beginning of the word
253 if (self.data[row][i].occupied_across is True and
254 self.data[row][i].across_start is True):
255 start_col = i
256 break
257 i -= 1
258
259 i = start_col
260 word_chars = []
261 # now seek the end of the word
262 while i < self.cols:
263 if self.data[row][i].occupied_across is True:
264 word_chars.append (self.data[row][i].char)
265 else:
266 break
267 i += 1
268
269 word = "".join (word_chars)
270
271 # return the word, starting column, row and length as a tuple
272 return (word, row, start_col, len(word))
273
274 # setting a down word
275 def set_word_down (self, row, col, word):
276 # if the grid is frozen the abort
277 if self.frozen_grid is True:
278 raise FrozenGridException
279
280 # if the word length greater than totalrows - startrow
281 if len(word) > self.rows - row:
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 column
287 if self.data[row+i][col].occupied_down is True:
288 raise IntersectWordException (word, len(word))
289 # on the previous column except first column
290 if col > 0:
291 # except the first and last col
292 if i > 0 and i < len(word) - 1:
293 if self.data[row+i][col-1].occupied_down is True:
294 raise IntersectWordException (word, len(word))
295 # on the next column except last column
296 if col < len(word) - 1:
297 # except the first and last row check if there is any
298 # down word in previous column
299 if i > 0 and i < len(word) - 1:
300 if self.data[row+i][col+1].occupied_down is True:
301 raise IntersectWordException (word, len(word))
302 # check if there is any across word starting in the
303 # next column
304 if self.data[row+i][col+1].across_start is True:
305 raise IntersectWordException (word, len(word))
306
307 # also check the character before and after
308 if (row > 0 and self.data[row-1][col].occupied_down is True
309 and self.data[row-1][col].occupied_across is True):
310 raise IntersectWordException (word, len(word))
311 if (row + len(word) < self.rows and
312 self.data[row+len(word)][col].occupied_across is True and
313 self.data[row+len(word)][col].occupied_down is True):
314 raise IntersectWordException (word, len(word))
315
316 # set the down start to true
317 self.data[row][col].down_start = True
318 # set the word
319 for i in range (len(word)):
320 self.data[row+i][col].occupied_down = True
321 self.data[row+i][col].char = word[i].upper ()
322
323
324 # setting an across word
325 def set_word_across (self, row, col, word):
326 # if the grid is frozen the abort
327 if self.frozen_grid is True:
328 raise FrozenGridException
329
330 # is the word length greater than totalcols - startcol?
331 if len(word) > self.cols - col:
332 raise TooLongWordException (word, len(word))
333
334 # is the word intersecting any other word?
335 for i in range (len(word)):
336 # on the same row
337 if self.data[row][col+i].occupied_across is True:
338 raise IntersectWordException (word, len(word))
339 # on a previous row except first row
340 if row > 0:
341 # if not the first or last col
342 if i > 0 and i < len(word) - 1:
343 if self.data[row-1][col+i].occupied_across is True:
344 raise IntersectWordException (word, len(word))
345 # on a next row
346 if (row < (self.rows - 1)):
347 # except the first and last letter check if there is
348 # any across intersection
349 if i > 0 and i < len (word) - 1:
350 if self.data[row+1][col+i].occupied_across is True:
351 raise IntersectWordException (word, len(word))
352 # if a down word is starting at any column below the
353 # word
354 if self.data[row+1][col+i].down_start is True:
355 raise IntersectWordException (word, len(word))
356
357 # also check the character beyond and before and after
358 if (col > 0 and (self.data[row][col-1].occupied_across is True or
359 self.data[row][col-1].occupied_down is True)):
360 raise IntersectWordException (word, len(word))
361 if (col + len(word) < self.cols and
362 (self.data[row][col+len(word)].occupied_across is True or
363 self.data[row][col+len(word)].occupied_down is True)):
364 raise IntersectWordException (word, len(word))
365
366 # set across start to true
367 self.data[row][col].across_start = True
368
369 # set the word
370 for i in range (len(word)):
371 self.data[row][col+i].char = word[i].upper ()
372 self.data[row][col+i].occupied_across = True
373
374 # freeze the grid numbers etc.
375 def freeze_grid (self):
376 # numbering
377 numbering = 1
378 # run through the grid
379 for row in range (self.rows):
380 for col in range (self.cols):
381 # if grid is blank set the character to #
382 if (self.data[row][col].occupied_across is False
383 and self.data[row][col].occupied_down is False):
384 self.data[row][col].char = "#"
385 elif (self.data[row][col].across_start is True or
386 self.data[row][col].down_start is True):
387 self.data[row][col].numbered = numbering
388 numbering += 1
389
390 self.frozen_grid = True
391
392 # unfreeze the grid numbers etc.
393 def unfreeze_grid (self):
394 # run through the grid
395 for row in range (self.rows):
396 for col in range (self.cols):
397 self.data[row][col].numbered = 0
398 if (self.data[row][col].occupied_across is False and
399 self.data[row][col].occupied_down is False):
400 self.data[row][col].char = '.'
401
402 self.frozen_grid = False
403