1 # Get A Clue (C) 2010 V. Harishankar
2 # Crossword puzzle maker program
3 # Licensed under the GNU GPL v3
5 # Class for the puzzle data representation
7 # for export to PNG image
12 def __init__ (self
, item_char
='.', item_guess
= None, across_start
= False,
13 down_start
= False, occupied_across
= False,
14 occupied_down
= False, num
= 0, clue_across
= None,
15 clue_down
= None, revealed
= False):
16 # character in the cell
18 # guess of character in cell
19 self
.guess
= item_guess
20 # is the cell the start of an across word?
21 self
.across_start
= across_start
22 # is the cell the start of a down word?
23 self
.down_start
= down_start
24 # is the cell occupied by a letter in an across word?
25 self
.occupied_across
= occupied_across
26 # is the cell occupied by a letter in a down word?
27 self
.occupied_down
= occupied_down
28 # numbering of the cell if it is the start of a word
30 # clue across if the cell is the start of an across word
31 self
.clue_across
= clue_across
32 # clue down if the cell is the start of a down word
33 self
.clue_down
= clue_down
34 # is the letter revealed or hidden?
35 self
.revealed
= revealed
37 # clear the across data
38 def clear_across_data (self
):
39 self
.across_start
= False
40 self
.occupied_across
= False
41 self
.clue_across
= None
42 # if no down word starting at item
43 if self
.down_start
is False:
45 # if no down word at the item
46 if self
.occupied_down
is False:
52 def clear_down_data (self
):
53 self
.down_start
= False
54 self
.occupied_down
= False
56 # if no across word starting at item
57 if self
.across_start
is False:
59 # if no across word at the item
60 if self
.occupied_across
is False:
65 # reset a grid item completely - use only to destroy whole grid, otherwise
66 # use either clear_across_data () or clear_down_data () for erasing single
69 # character in the cell
71 # guess of character in cell
73 # is the cell the start of an across word?
74 self
.across_start
= False
75 # is the cell the start of a down word?
76 self
.down_start
= False
77 # is the cell occupied by a letter in an across word?
78 self
.occupied_across
= False
79 # is the cell occupied by a letter in a down word?
80 self
.occupied_down
= False
81 # numbering of the cell if it is the start of a word
83 # clue across if the cell is the start of an across word
84 self
.clue_across
= None
85 # clue down if the cell is the start of a down word
87 # is the letter revealed or hidden?
90 # exception for too long words
91 class TooLongWordException (Exception):
92 def __init__ (self
, word
, length
):
96 # exception for intersecting words
97 class IntersectWordException (Exception):
98 def __init__ (self
, word
, length
):
102 # exception when grid is sought to be changed when frozen
103 class FrozenGridException (Exception):
105 self
.msg
= "Grid is frozen and cannot be edited"
107 # exception when no word is found at a position
108 class NoWordException (Exception):
109 def __init__ (self
, row
, col
):
110 self
.pos
= (row
, col
)
112 # exception when no words are present in the grid
113 class NoWordsException (Exception):
115 self
.msg
= "No words in grid"
117 class CrosswordPuzzle
:
118 def __init__ (self
, rows
, cols
):
119 # define number of rows and columns
123 # initialize the list to hold the grid
126 # initial state of the grid is unfrozen
127 self
.frozen_grid
= False
129 # create the grid data
130 for i
in range (rows
):
131 self
.data
.append ([])
132 for j
in range (cols
):
133 self
.data
[i
].append (GridItem ())
136 def export_image (self
, pngfile
, htmlfile
=None, puztitle
="Crossword Puzzle",
138 # don't export if grid is not frozen
139 if self
.frozen_grid
is False:
140 raise FrozenGridException
142 # create cairo image surface and context
144 surf
= cairo
.ImageSurface (cairo
.FORMAT_RGB24
, self
.cols
*px
, self
.rows
*px
)
145 ctx
= cairo
.Context (surf
)
147 ctx
.set_source_rgb (1, 1, 1)
148 ctx
.rectangle (0, 0, self
.cols
*px
, self
.rows
*px
)
151 # get the clues across and down
152 clues_across
= self
.get_clues_across ()
153 clues_down
= self
.get_clues_down ()
156 # traverse through the grid
157 for row
in range (self
.rows
):
158 for col
in range (self
.cols
):
159 # if grid is un-occupied
160 if (self
.data
[row
][col
].occupied_across
is False and
161 self
.data
[row
][col
].occupied_down
is False):
162 ctx
.set_source_rgb (0, 0, 0)
163 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
167 ctx
.set_source_rgb (1, 1, 1)
168 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
170 ctx
.set_source_rgb (0, 0, 0)
171 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
173 # if solution is not to be provided, number the grid
174 if solution
is False:
175 if self
.data
[row
][col
].numbered
<> 0:
176 ctx
.select_font_face ("Serif")
177 ctx
.set_font_size (10)
178 ctx
.move_to (col
*px
+5, row
*px
+10)
179 ctx
.show_text (str(self
.data
[row
][col
].numbered
))
182 ctx
.select_font_face ("Serif")
183 ctx
.set_font_size (16)
184 ctx
.move_to (col
*px
+10, row
*px
+20)
185 ctx
.show_text (self
.data
[row
][col
].char
)
187 surf
.write_to_png (open (pngfile
, "wb"))
189 # if solution is false, publish the clues and the image in a HTML file
190 if htmlfile
and solution
is False:
191 html_contents
= ["<html>", "<head>", "<title>"]
192 html_contents
.append (puztitle
)
193 html_contents
.append ("</title>")
194 html_contents
.append ("</head>")
195 html_contents
.append ("<body>")
196 html_contents
.append ("<h1>" + puztitle
+ "</h1>")
197 html_contents
.append ('<img src="' + pngfile
+ '" alt="puzzle" />')
199 html_contents
.append ("<h2>Across clues</h2>")
200 html_contents
.append ("<p>")
201 for word
, clue
in clues_across
:
202 clue_str
= str (self
.data
[word
[1]][word
[2]].numbered
) + " - " \
204 html_contents
.append (clue_str
)
205 html_contents
.append ("<br />")
206 html_contents
.append ("</p>")
208 html_contents
.append ("<h2>Down clues</h2>")
209 html_contents
.append ("<p>")
210 for word
, clue
in clues_down
:
211 clue_str
= str (self
.data
[word
[1]][word
[2]].numbered
) + " - " \
213 html_contents
.append (clue_str
)
214 html_contents
.append ("<br />")
215 html_contents
.append ("</p>")
216 html_contents
.append ("</body>")
217 html_contents
.append ("</html>")
219 html_str
= "\r\n".join (html_contents
)
221 fhtml
= open (htmlfile
, "wb")
222 fhtml
.write (html_str
)
225 # get the AcrossLite(TM) data for exporting
226 def export_acrosslite (self
, title
, author
, copyright
):
227 # don't export if grid is not frozen
228 if self
.frozen_grid
is False:
229 raise FrozenGridException
232 across_data
.append ("<ACROSS PUZZLE>\r\n")
233 across_data
.append ("<TITLE>\r\n")
234 across_data
.append (title
+ "\r\n")
235 across_data
.append ("<AUTHOR>\r\n")
236 across_data
.append (author
+ "\r\n")
237 across_data
.append ("<COPYRIGHT>\r\n")
238 across_data
.append (copyright
+ "\r\n")
239 across_data
.append ("<SIZE>\r\n")
240 str_size
= str (self
.cols
) + "x" + str (self
.rows
)
241 across_data
.append (str_size
+ "\r\n")
242 across_data
.append ("<GRID>\r\n")
243 for row
in range (self
.rows
):
244 for col
in range (self
.cols
):
245 if (self
.data
[row
][col
].occupied_across
is True or
246 self
.data
[row
][col
].occupied_down
is True):
247 across_data
.append (self
.data
[row
][col
].char
)
249 across_data
.append (".")
250 across_data
.append ("\r\n")
252 across_data
.append ("<ACROSS>\r\n")
253 clues_across
= self
.get_clues_across ()
254 for word
, clue
in clues_across
:
256 across_data
.append (clue
+ "\r\n")
258 across_data
.append ("(No clue yet)\r\n")
260 across_data
.append ("<DOWN>\r\n")
261 clues_down
= self
.get_clues_down ()
262 for word
, clue
in clues_down
:
264 across_data
.append (clue
+ "\r\n")
266 across_data
.append ("(No clue yet\r\n")
268 acrosslite_str
= "".join (across_data
)
269 return acrosslite_str
271 # get all the clues for across
272 def get_clues_across (self
):
275 for row
in range (self
.rows
):
276 for col
in range (self
.cols
):
277 if (self
.data
[row
][col
].occupied_across
is True and
278 self
.data
[row
][col
].across_start
is True):
279 word_across
= self
.get_word_across (row
, col
)
280 clues
.append ((word_across
, self
.data
[row
][col
].clue_across
))
281 # if no across words are found at all
283 raise NoWordsException
287 # get all the clues for down
288 def get_clues_down (self
):
291 for row
in range (self
.rows
):
292 for col
in range (self
.cols
):
293 if (self
.data
[row
][col
].occupied_down
is True and
294 self
.data
[row
][col
].down_start
is True):
295 word_down
= self
.get_word_down (row
, col
)
296 clues
.append ((word_down
, self
.data
[row
][col
].clue_down
))
297 # if no down words are found at all
299 raise NoWordsException
303 # getting a down word at a position
304 def get_word_down (self
, row
, col
):
305 # if index is out of bounds
306 if row
>= self
.rows
or col
>= self
.cols
:
307 raise NoWordException (row
, col
)
309 # if there is no occupied down letter at that position
310 if self
.data
[row
][col
].occupied_down
is False:
311 raise NoWordException (row
, col
)
313 # now traverse the grid to find the beginning of the word
316 # if it is occupied down and is the beginning of the word
317 if (self
.data
[i
][col
].occupied_down
is True and
318 self
.data
[i
][col
].down_start
is True):
325 # now seek the end of the word
327 if self
.data
[i
][col
].occupied_down
is True:
328 word_chars
.append (self
.data
[i
][col
].char
)
333 word
= "".join (word_chars
)
335 # return the word, starting row, column and length as a tuple
336 return (word
, start_row
, col
, len(word
))
338 # getting an across word at a position
339 def get_word_across (self
, row
, col
):
340 # if index is out of bounds
341 if row
>= self
.rows
or col
>= self
.cols
:
342 raise NoWordException (row
, col
)
344 # if there is no occupied across letter at that position
345 if self
.data
[row
][col
].occupied_across
is False:
346 raise NoWordException (row
, col
)
348 # now traverse the grid to look for the beginning of the word
351 # if it is occupied across and is the beginning of the word
352 if (self
.data
[row
][i
].occupied_across
is True and
353 self
.data
[row
][i
].across_start
is True):
360 # now seek the end of the word
362 if self
.data
[row
][i
].occupied_across
is True:
363 word_chars
.append (self
.data
[row
][i
].char
)
368 word
= "".join (word_chars
)
370 # return the word, starting column, row and length as a tuple
371 return (word
, row
, start_col
, len(word
))
373 # setting a down word
374 def set_word_down (self
, row
, col
, word
):
375 # if the grid is frozen the abort
376 if self
.frozen_grid
is True:
377 raise FrozenGridException
379 # if the word length greater than totalrows - startrow
380 if len(word
) > self
.rows
- row
:
381 raise TooLongWordException (word
, len(word
))
383 # is the word intersecting any other word?
384 for i
in range (len(word
)):
386 if self
.data
[row
+i
][col
].occupied_down
is True:
387 raise IntersectWordException (word
, len(word
))
388 # on the previous column except first column
390 # except the first and last col
391 if i
> 0 and i
< len(word
) - 1:
392 if self
.data
[row
+i
][col
-1].occupied_down
is True:
393 raise IntersectWordException (word
, len(word
))
394 # on the next column except last column
395 if col
< len(word
) - 1:
396 # except the first and last row check if there is any
397 # down word in previous column
398 if i
> 0 and i
< len(word
) - 1:
399 if self
.data
[row
+i
][col
+1].occupied_down
is True:
400 raise IntersectWordException (word
, len(word
))
401 # check if there is any across word starting in the
403 if self
.data
[row
+i
][col
+1].across_start
is True:
404 raise IntersectWordException (word
, len(word
))
406 # also check the character before and after
407 if (row
> 0 and self
.data
[row
-1][col
].occupied_down
is True
408 and self
.data
[row
-1][col
].occupied_across
is True):
409 raise IntersectWordException (word
, len(word
))
410 if (row
+ len(word
) < self
.rows
and
411 self
.data
[row
+len(word
)][col
].occupied_across
is True and
412 self
.data
[row
+len(word
)][col
].occupied_down
is True):
413 raise IntersectWordException (word
, len(word
))
415 # set the down start to true
416 self
.data
[row
][col
].down_start
= True
418 for i
in range (len(word
)):
419 self
.data
[row
+i
][col
].occupied_down
= True
420 self
.data
[row
+i
][col
].char
= word
[i
].upper ()
423 # setting an across word
424 def set_word_across (self
, row
, col
, word
):
425 # if the grid is frozen the abort
426 if self
.frozen_grid
is True:
427 raise FrozenGridException
429 # is the word length greater than totalcols - startcol?
430 if len(word
) > self
.cols
- col
:
431 raise TooLongWordException (word
, len(word
))
433 # is the word intersecting any other word?
434 for i
in range (len(word
)):
436 if self
.data
[row
][col
+i
].occupied_across
is True:
437 raise IntersectWordException (word
, len(word
))
438 # on a previous row except first row
440 # if not the first or last col
441 if i
> 0 and i
< len(word
) - 1:
442 if self
.data
[row
-1][col
+i
].occupied_across
is True:
443 raise IntersectWordException (word
, len(word
))
445 if (row
< (self
.rows
- 1)):
446 # except the first and last letter check if there is
447 # any across intersection
448 if i
> 0 and i
< len (word
) - 1:
449 if self
.data
[row
+1][col
+i
].occupied_across
is True:
450 raise IntersectWordException (word
, len(word
))
451 # if a down word is starting at any column below the
453 if self
.data
[row
+1][col
+i
].down_start
is True:
454 raise IntersectWordException (word
, len(word
))
456 # also check the character beyond and before and after
457 if (col
> 0 and (self
.data
[row
][col
-1].occupied_across
is True or
458 self
.data
[row
][col
-1].occupied_down
is True)):
459 raise IntersectWordException (word
, len(word
))
460 if (col
+ len(word
) < self
.cols
and
461 (self
.data
[row
][col
+len(word
)].occupied_across
is True or
462 self
.data
[row
][col
+len(word
)].occupied_down
is True)):
463 raise IntersectWordException (word
, len(word
))
465 # set across start to true
466 self
.data
[row
][col
].across_start
= True
469 for i
in range (len(word
)):
470 self
.data
[row
][col
+i
].char
= word
[i
].upper ()
471 self
.data
[row
][col
+i
].occupied_across
= True
473 # freeze the grid numbers etc.
474 def freeze_grid (self
):
477 # run through the grid
478 for row
in range (self
.rows
):
479 for col
in range (self
.cols
):
480 # if grid is blank set the character to #
481 if (self
.data
[row
][col
].occupied_across
is False
482 and self
.data
[row
][col
].occupied_down
is False):
483 self
.data
[row
][col
].char
= "#"
484 elif (self
.data
[row
][col
].across_start
is True or
485 self
.data
[row
][col
].down_start
is True):
486 self
.data
[row
][col
].numbered
= numbering
489 self
.frozen_grid
= True
491 # unfreeze the grid numbers etc.
492 def unfreeze_grid (self
):
493 # run through the grid
494 for row
in range (self
.rows
):
495 for col
in range (self
.cols
):
496 self
.data
[row
][col
].numbered
= 0
497 if (self
.data
[row
][col
].occupied_across
is False and
498 self
.data
[row
][col
].occupied_down
is False):
499 self
.data
[row
][col
].char
= '.'
501 self
.frozen_grid
= False
503 # reset the entire grid
504 def reset_grid (self
):
505 # run through the grid
506 for row
in range (self
.rows
):
507 for col
in range (self
.cols
):
508 # re-initialize all data
509 self
.data
[row
][col
].reset ()
511 self
.frozen_grid
= False