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