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 words containing non-alpha characters
97 class WordCharsException (Exception):
98 def __init__ (self
, word
):
101 # exception for intersecting words
102 class IntersectWordException (Exception):
103 def __init__ (self
, word
, length
):
107 # exception when grid is sought to be changed when frozen
108 class FrozenGridException (Exception):
110 self
.msg
= "Grid is frozen and cannot be edited"
112 # exception when no word is found at a position
113 class NoWordException (Exception):
114 def __init__ (self
, row
, col
):
115 self
.pos
= (row
, col
)
117 # exception when a word number is not found in the grid
118 class NoNumberException (Exception):
119 def __init__ (self
, num
):
122 # exception when no words are present in the grid
123 class NoWordsException (Exception):
125 self
.msg
= "No words in grid"
127 # exception to raise when solution is imcomplete when trying to verify it
128 class IncompleteSolutionException (Exception):
130 self
.msg
= "Solution incomplete"
132 class CrosswordPuzzle
:
133 def __init__ (self
, rows
, cols
):
134 # define number of rows and columns
138 # initialize the list to hold the grid
141 # initial state of the grid is unfrozen
142 self
.frozen_grid
= False
144 # create the grid data
145 for i
in range (rows
):
146 self
.data
.append ([])
147 for j
in range (cols
):
148 self
.data
[i
].append (GridItem ())
151 def export_image (self
, pngfile
, htmlfile
=None, puztitle
="Crossword Puzzle",
153 # don't export if grid is not frozen
154 self
.assert_frozen_grid ()
156 # create cairo image surface and context
158 surf
= cairo
.ImageSurface (cairo
.FORMAT_RGB24
, self
.cols
*px
, self
.rows
*px
)
159 ctx
= cairo
.Context (surf
)
161 ctx
.set_source_rgb (1, 1, 1)
162 ctx
.rectangle (0, 0, self
.cols
*px
, self
.rows
*px
)
165 # get the clues across and down
166 clues_across
= self
.get_clues_across ()
167 clues_down
= self
.get_clues_down ()
170 # traverse through the grid
171 for row
in range (self
.rows
):
172 for col
in range (self
.cols
):
173 # if grid is un-occupied
174 if (self
.data
[row
][col
].occupied_across
is False and
175 self
.data
[row
][col
].occupied_down
is False):
176 ctx
.set_source_rgb (0, 0, 0)
177 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
181 ctx
.set_source_rgb (1, 1, 1)
182 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
184 ctx
.set_source_rgb (0, 0, 0)
185 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
187 # if solution is not to be provided, number the grid
188 if solution
is False:
189 if self
.data
[row
][col
].numbered
<> 0:
190 ctx
.select_font_face ("Serif")
191 ctx
.set_font_size (10)
192 ctx
.move_to (col
*px
+5, row
*px
+10)
193 ctx
.show_text (str(self
.data
[row
][col
].numbered
))
196 ctx
.select_font_face ("Serif")
197 ctx
.set_font_size (16)
198 ctx
.move_to (col
*px
+10, row
*px
+20)
199 ctx
.show_text (self
.data
[row
][col
].char
)
201 surf
.write_to_png (open (pngfile
, "wb"))
203 # if solution is false, publish the clues and the image in a HTML file
204 if htmlfile
and solution
is False:
205 html_contents
= ["<html>", "<head>", "<title>"]
206 html_contents
.append (puztitle
)
207 html_contents
.append ("</title>")
208 html_contents
.append ("</head>")
209 html_contents
.append ("<body>")
210 html_contents
.append ("<h1>" + puztitle
+ "</h1>")
211 html_contents
.append ('<img src="' + pngfile
+ '" alt="puzzle" />')
213 html_contents
.append ("<h2>Across clues</h2>")
214 html_contents
.append ("<p>")
215 for word
, clue
in clues_across
:
216 # clue should be: <num> - clue text (chars)
217 clue_str
= str (self
.data
[word
[1]][word
[2]].numbered
) + " - " \
218 + clue
+ " (" + str (word
[3]) + ")"
219 html_contents
.append (clue_str
)
220 html_contents
.append ("<br />")
221 html_contents
.append ("</p>")
223 html_contents
.append ("<h2>Down clues</h2>")
224 html_contents
.append ("<p>")
225 for word
, clue
in clues_down
:
226 clue_str
= str (self
.data
[word
[1]][word
[2]].numbered
) + " - " \
227 + clue
+ " (" + str (word
[3]) + ")"
228 html_contents
.append (clue_str
)
229 html_contents
.append ("<br />")
230 html_contents
.append ("</p>")
231 html_contents
.append ("</body>")
232 html_contents
.append ("</html>")
234 html_str
= "\r\n".join (html_contents
)
236 fhtml
= open (htmlfile
, "wb")
237 fhtml
.write (html_str
)
240 # get the AcrossLite(TM) data for exporting
241 def export_acrosslite (self
, title
, author
, copyright
):
242 # don't export if grid is not frozen
243 self
.assert_frozen_grid ()
246 across_data
.append ("<ACROSS PUZZLE>\r\n")
247 across_data
.append ("<TITLE>\r\n")
248 across_data
.append (title
+ "\r\n")
249 across_data
.append ("<AUTHOR>\r\n")
250 across_data
.append (author
+ "\r\n")
251 across_data
.append ("<COPYRIGHT>\r\n")
252 across_data
.append (copyright
+ "\r\n")
253 across_data
.append ("<SIZE>\r\n")
254 str_size
= str (self
.cols
) + "x" + str (self
.rows
)
255 across_data
.append (str_size
+ "\r\n")
256 across_data
.append ("<GRID>\r\n")
257 for row
in range (self
.rows
):
258 for col
in range (self
.cols
):
259 if (self
.data
[row
][col
].occupied_across
is True or
260 self
.data
[row
][col
].occupied_down
is True):
261 across_data
.append (self
.data
[row
][col
].char
)
263 across_data
.append (".")
264 across_data
.append ("\r\n")
266 across_data
.append ("<ACROSS>\r\n")
267 clues_across
= self
.get_clues_across ()
268 for word
, clue
in clues_across
:
270 across_data
.append (clue
+ "\r\n")
272 across_data
.append ("(No clue yet)\r\n")
274 across_data
.append ("<DOWN>\r\n")
275 clues_down
= self
.get_clues_down ()
276 for word
, clue
in clues_down
:
278 across_data
.append (clue
+ "\r\n")
280 across_data
.append ("(No clue yet\r\n")
282 acrosslite_str
= "".join (across_data
)
283 return acrosslite_str
285 # get all the clues for across
286 def get_clues_across (self
):
289 for row
in range (self
.rows
):
290 for col
in range (self
.cols
):
291 if (self
.data
[row
][col
].occupied_across
is True and
292 self
.data
[row
][col
].across_start
is True):
293 word_across
= self
.get_word_across (row
, col
)
294 clues
.append ((word_across
, self
.data
[row
][col
].clue_across
))
295 # if no across words are found at all
297 raise NoWordsException
301 # get all the clues for down
302 def get_clues_down (self
):
305 for row
in range (self
.rows
):
306 for col
in range (self
.cols
):
307 if (self
.data
[row
][col
].occupied_down
is True and
308 self
.data
[row
][col
].down_start
is True):
309 word_down
= self
.get_word_down (row
, col
)
310 clues
.append ((word_down
, self
.data
[row
][col
].clue_down
))
311 # if no down words are found at all
313 raise NoWordsException
317 # getting an across word at a number (note that the grid should be
318 # frozen for calling this otherwise a FrozenGridException will be raised)
319 def get_word_across_at_num (self
, num
):
320 # assert that the grid is frozen
321 self
.assert_frozen_grid ()
324 for row
in range (self
.rows
):
325 for col
in range (self
.cols
):
326 if self
.data
[row
][col
].numbered
== num
:
327 word
= self
.get_word_across (row
, col
)
330 # if number is not found
331 raise NoNumberException (num
)
333 # getting a down word at a number (note that the grid should be frozen
334 # for calling this otherwise a FrozenGridException will be raised)
335 def get_word_down_at_num (self
, num
):
336 # assert that the grid is frozen
337 self
.assert_frozen_grid ()
340 for row
in range (self
.rows
):
341 for col
in range (self
.cols
):
342 if self
.data
[row
][col
].numbered
== num
:
343 word
= self
.get_word_down (row
, col
)
346 # if number is not found
347 raise NoNumberException (num
)
349 # getting the position of a number on the grid (note that the grid should
350 # be frozen for calling this otherwise a FrozenGridException will be raised)
351 def get_position_of_num (self
, num
):
352 # assert that the grid is frozen
353 self
.assert_frozen_grid ()
356 for row
in range (self
.rows
):
357 for col
in range (self
.cols
):
358 if self
.data
[row
][col
].numbered
== num
:
361 # if number is not found
362 raise NoNumberException (num
)
364 # getting a down word at a position
365 def get_word_down (self
, row
, col
):
366 # if index is out of bounds
367 if row
>= self
.rows
or col
>= self
.cols
:
368 raise NoWordException (row
, col
)
370 # if there is no occupied down letter at that position
371 if self
.data
[row
][col
].occupied_down
is False:
372 raise NoWordException (row
, col
)
374 # now traverse the grid to find the beginning of the word
377 # if it is occupied down and is the beginning of the word
378 if (self
.data
[i
][col
].occupied_down
is True and
379 self
.data
[i
][col
].down_start
is True):
386 # now seek the end of the word
388 if self
.data
[i
][col
].occupied_down
is True:
389 word_chars
.append (self
.data
[i
][col
].char
)
394 word
= "".join (word_chars
)
396 # return the word, starting row, column and length as a tuple
397 return (word
, start_row
, col
, len(word
))
399 # getting an across word at a position
400 def get_word_across (self
, row
, col
):
401 # if index is out of bounds
402 if row
>= self
.rows
or col
>= self
.cols
:
403 raise NoWordException (row
, col
)
405 # if there is no occupied across letter at that position
406 if self
.data
[row
][col
].occupied_across
is False:
407 raise NoWordException (row
, col
)
409 # now traverse the grid to look for the beginning of the word
412 # if it is occupied across and is the beginning of the word
413 if (self
.data
[row
][i
].occupied_across
is True and
414 self
.data
[row
][i
].across_start
is True):
421 # now seek the end of the word
423 if self
.data
[row
][i
].occupied_across
is True:
424 word_chars
.append (self
.data
[row
][i
].char
)
429 word
= "".join (word_chars
)
431 # return the word, starting column, row and length as a tuple
432 return (word
, row
, start_col
, len(word
))
434 # setting a down word
435 def set_word_down (self
, row
, col
, word
):
436 # if the grid is frozen then abort
437 self
.assert_unfrozen_grid ()
439 # if the word has non-alphabetic characters
440 if not word
.isalpha ():
441 raise WordCharsException (word
)
443 # if the word length greater than totalrows - startrow
444 if len(word
) > self
.rows
- row
:
445 raise TooLongWordException (word
, len(word
))
447 # is the word intersecting any other word?
448 for i
in range (len(word
)):
450 if self
.data
[row
+i
][col
].occupied_down
is True:
451 raise IntersectWordException (word
, len(word
))
452 # on the previous column except first column
454 # except the first and last col
455 if i
> 0 and i
< len(word
) - 1:
456 if self
.data
[row
+i
][col
-1].occupied_down
is True:
457 raise IntersectWordException (word
, len(word
))
458 # if the previous column is the end of an across word
459 if (self
.data
[row
+i
][col
-1].occupied_across
is True and
460 self
.data
[row
+i
][col
].occupied_across
is False):
461 raise IntersectWordException (word
, len(word
))
463 # on the next column except last column
464 if col
< len(word
) - 1:
465 # except the first and last row check if there is any
466 # down word in previous column
467 if i
> 0 and i
< len(word
) - 1:
468 if self
.data
[row
+i
][col
+1].occupied_down
is True:
469 raise IntersectWordException (word
, len(word
))
470 # check if there is any across word starting in the
472 if self
.data
[row
+i
][col
+1].across_start
is True:
473 raise IntersectWordException (word
, len(word
))
475 # also check the character before and after
476 if (row
> 0 and self
.data
[row
-1][col
].occupied_down
is True
477 and self
.data
[row
-1][col
].occupied_across
is True):
478 raise IntersectWordException (word
, len(word
))
479 if (row
+ len(word
) < self
.rows
and
480 self
.data
[row
+len(word
)][col
].occupied_across
is True and
481 self
.data
[row
+len(word
)][col
].occupied_down
is True):
482 raise IntersectWordException (word
, len(word
))
484 # set the down start to true
485 self
.data
[row
][col
].down_start
= True
487 for i
in range (len(word
)):
488 self
.data
[row
+i
][col
].occupied_down
= True
489 self
.data
[row
+i
][col
].char
= word
[i
].upper ()
492 # setting an across word
493 def set_word_across (self
, row
, col
, word
):
494 # if the grid is frozen then abort
495 self
.assert_unfrozen_grid ()
497 # if the word has non-alphabetic characters
498 if not word
.isalpha ():
499 raise WordCharsException (word
)
501 # is the word length greater than totalcols - startcol?
502 if len(word
) > self
.cols
- col
:
503 raise TooLongWordException (word
, len(word
))
505 # is the word intersecting any other word?
506 for i
in range (len(word
)):
508 if self
.data
[row
][col
+i
].occupied_across
is True:
509 raise IntersectWordException (word
, len(word
))
510 # on a previous row except first row
512 # if not the first or last col
513 if i
> 0 and i
< len(word
) - 1:
514 if self
.data
[row
-1][col
+i
].occupied_across
is True:
515 raise IntersectWordException (word
, len(word
))
516 # if the previous row is the end of a down word
517 if (self
.data
[row
-1][col
+i
].occupied_down
is True and
518 self
.data
[row
][col
+i
].occupied_down
is False):
519 raise IntersectWordException (word
, len(word
))
522 if (row
< (self
.rows
- 1)):
523 # except the first and last letter check if there is
524 # any across intersection
525 if i
> 0 and i
< len (word
) - 1:
526 if self
.data
[row
+1][col
+i
].occupied_across
is True:
527 raise IntersectWordException (word
, len(word
))
528 # if a down word is starting at any column below the
530 if self
.data
[row
+1][col
+i
].down_start
is True:
531 raise IntersectWordException (word
, len(word
))
533 # also check the character beyond and before and after
534 if (col
> 0 and (self
.data
[row
][col
-1].occupied_across
is True or
535 self
.data
[row
][col
-1].occupied_down
is True)):
536 raise IntersectWordException (word
, len(word
))
537 if (col
+ len(word
) < self
.cols
and
538 (self
.data
[row
][col
+len(word
)].occupied_across
is True or
539 self
.data
[row
][col
+len(word
)].occupied_down
is True)):
540 raise IntersectWordException (word
, len(word
))
542 # set across start to true
543 self
.data
[row
][col
].across_start
= True
546 for i
in range (len(word
)):
547 self
.data
[row
][col
+i
].char
= word
[i
].upper ()
548 self
.data
[row
][col
+i
].occupied_across
= True
550 # freeze the grid numbers etc.
551 def freeze_grid (self
):
554 # run through the grid
555 for row
in range (self
.rows
):
556 for col
in range (self
.cols
):
557 # if grid is blank set the character to #
558 if (self
.data
[row
][col
].occupied_across
is False
559 and self
.data
[row
][col
].occupied_down
is False):
560 self
.data
[row
][col
].char
= "#"
561 elif (self
.data
[row
][col
].across_start
is True or
562 self
.data
[row
][col
].down_start
is True):
563 self
.data
[row
][col
].numbered
= numbering
566 self
.frozen_grid
= True
568 # unfreeze the grid numbers etc.
569 def unfreeze_grid (self
):
570 # run through the grid
571 for row
in range (self
.rows
):
572 for col
in range (self
.cols
):
573 self
.data
[row
][col
].numbered
= 0
574 if (self
.data
[row
][col
].occupied_across
is False and
575 self
.data
[row
][col
].occupied_down
is False):
576 self
.data
[row
][col
].char
= '.'
578 self
.frozen_grid
= False
580 # raise an exception if the grid is frozen
581 def assert_unfrozen_grid (self
):
582 if self
.frozen_grid
is True:
583 raise FrozenGridException
585 # raise an exception if the grid is NOT frozen
586 def assert_frozen_grid (self
):
587 if self
.frozen_grid
is False:
588 raise FrozenGridException
590 # reset the entire grid
591 def reset_grid (self
):
592 # run through the grid
593 for row
in range (self
.rows
):
594 for col
in range (self
.cols
):
595 # re-initialize all data
596 self
.data
[row
][col
].reset ()
598 self
.frozen_grid
= False
600 # remove an across word at position
601 def remove_word_across (self
, row
, col
):
602 # if grid is frozen don't allow removal of word
603 self
.assert_unfrozen_grid ()
605 word
, brow
, bcol
, l
= self
.get_word_across (row
, col
)
607 # traverse from the beginning to end of the word and erase it
610 if self
.data
[brow
][c
].occupied_across
is True:
611 self
.data
[brow
][c
].clear_across_data ()
616 # remove a down word at position
617 def remove_word_down (self
, row
, col
):
618 # if grid is frozen don't allow removal of word
619 self
.assert_unfrozen_grid ()
621 word
, brow
, bcol
, l
= self
.get_word_down (row
, col
)
622 # traverse from the beginn to end of the word and erase it
625 if self
.data
[r
][bcol
].occupied_down
is True:
626 self
.data
[r
][bcol
].clear_down_data ()
631 # reveal/unreveal a word at position
632 def reveal_word_across (self
, row
, col
, revealed
=True):
633 # set the revealed flag for the word at the position
634 word
= self
.get_word_across (row
, col
)
638 if self
.data
[word
[1]][c
].occupied_across
is True:
639 self
.data
[word
[1]][c
].revealed
= revealed
644 # reveal/unreveal a word at position
645 def reveal_word_down (self
, row
, col
, revealed
=True):
646 # set the revealed flag for the word at the position
647 word
= self
.get_word_down (row
, col
)
651 if self
.data
[r
][word
[2]].occupied_down
is True:
652 self
.data
[r
][word
[2]].revealed
= revealed
657 # reveal/hide the entire solution by resetting revealed flag at all cells
658 def reveal_solution (self
, revealed
=True):
659 # run through the grid and set revealed to False
660 for row
in range (self
.rows
):
661 for col
in range (self
.cols
):
662 self
.data
[row
][col
].revealed
= revealed
664 # clear the guesses for the board
665 def clear_guesses (self
):
666 # run through the grid and set the guesses to None
667 for row
in range (self
.rows
):
668 for col
in range (self
.cols
):
669 self
.data
[row
][col
].guess
= None
671 # verify the solution - return True if all guessed characters are correct
672 # return False if some of them are wrong.
673 # if the board is not completed as yet, raise a IncompleteSolutionException
674 def is_solution_correct (self
):
675 # run through the grid and check for each character in occupied cells
677 for row
in range (self
.rows
):
678 for col
in range (self
.cols
):
679 if (self
.data
[row
][col
].occupied_across
is True or
680 self
.data
[row
][col
].occupied_down
is True):
681 # if there is no guess at a particular location raise
682 # the incomplete solution exception
683 if not self
.data
[row
][col
].guess
:
684 raise IncompleteSolutionException
685 # if a character doesn't match, return False
686 if self
.data
[row
][col
].char
<> self
.data
[row
][col
].guess
:
689 # finally return result