ffdd9157ba0608294b283d3927ea6dcbbad93760
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 a word number is not found in the grid
113 class NoNumberException (Exception):
114 def __init__ (self
, num
):
117 # exception when no words are present in the grid
118 class NoWordsException (Exception):
120 self
.msg
= "No words in grid"
122 class CrosswordPuzzle
:
123 def __init__ (self
, rows
, cols
):
124 # define number of rows and columns
128 # initialize the list to hold the grid
131 # initial state of the grid is unfrozen
132 self
.frozen_grid
= False
134 # create the grid data
135 for i
in range (rows
):
136 self
.data
.append ([])
137 for j
in range (cols
):
138 self
.data
[i
].append (GridItem ())
141 def export_image (self
, pngfile
, htmlfile
=None, puztitle
="Crossword Puzzle",
143 # don't export if grid is not frozen
144 self
.assert_frozen_grid ()
146 # create cairo image surface and context
148 surf
= cairo
.ImageSurface (cairo
.FORMAT_RGB24
, self
.cols
*px
, self
.rows
*px
)
149 ctx
= cairo
.Context (surf
)
151 ctx
.set_source_rgb (1, 1, 1)
152 ctx
.rectangle (0, 0, self
.cols
*px
, self
.rows
*px
)
155 # get the clues across and down
156 clues_across
= self
.get_clues_across ()
157 clues_down
= self
.get_clues_down ()
160 # traverse through the grid
161 for row
in range (self
.rows
):
162 for col
in range (self
.cols
):
163 # if grid is un-occupied
164 if (self
.data
[row
][col
].occupied_across
is False and
165 self
.data
[row
][col
].occupied_down
is False):
166 ctx
.set_source_rgb (0, 0, 0)
167 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
171 ctx
.set_source_rgb (1, 1, 1)
172 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
174 ctx
.set_source_rgb (0, 0, 0)
175 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
177 # if solution is not to be provided, number the grid
178 if solution
is False:
179 if self
.data
[row
][col
].numbered
<> 0:
180 ctx
.select_font_face ("Serif")
181 ctx
.set_font_size (10)
182 ctx
.move_to (col
*px
+5, row
*px
+10)
183 ctx
.show_text (str(self
.data
[row
][col
].numbered
))
186 ctx
.select_font_face ("Serif")
187 ctx
.set_font_size (16)
188 ctx
.move_to (col
*px
+10, row
*px
+20)
189 ctx
.show_text (self
.data
[row
][col
].char
)
191 surf
.write_to_png (open (pngfile
, "wb"))
193 # if solution is false, publish the clues and the image in a HTML file
194 if htmlfile
and solution
is False:
195 html_contents
= ["<html>", "<head>", "<title>"]
196 html_contents
.append (puztitle
)
197 html_contents
.append ("</title>")
198 html_contents
.append ("</head>")
199 html_contents
.append ("<body>")
200 html_contents
.append ("<h1>" + puztitle
+ "</h1>")
201 html_contents
.append ('<img src="' + pngfile
+ '" alt="puzzle" />')
203 html_contents
.append ("<h2>Across clues</h2>")
204 html_contents
.append ("<p>")
205 for word
, clue
in clues_across
:
206 # clue should be: <num> - clue text (chars)
207 clue_str
= str (self
.data
[word
[1]][word
[2]].numbered
) + " - " \
208 + clue
+ " (" + str (word
[3]) + ")"
209 html_contents
.append (clue_str
)
210 html_contents
.append ("<br />")
211 html_contents
.append ("</p>")
213 html_contents
.append ("<h2>Down clues</h2>")
214 html_contents
.append ("<p>")
215 for word
, clue
in clues_down
:
216 clue_str
= str (self
.data
[word
[1]][word
[2]].numbered
) + " - " \
217 + clue
+ " (" + str (word
[3]) + ")"
218 html_contents
.append (clue_str
)
219 html_contents
.append ("<br />")
220 html_contents
.append ("</p>")
221 html_contents
.append ("</body>")
222 html_contents
.append ("</html>")
224 html_str
= "\r\n".join (html_contents
)
226 fhtml
= open (htmlfile
, "wb")
227 fhtml
.write (html_str
)
230 # get the AcrossLite(TM) data for exporting
231 def export_acrosslite (self
, title
, author
, copyright
):
232 # don't export if grid is not frozen
233 self
.assert_frozen_grid ()
236 across_data
.append ("<ACROSS PUZZLE>\r\n")
237 across_data
.append ("<TITLE>\r\n")
238 across_data
.append (title
+ "\r\n")
239 across_data
.append ("<AUTHOR>\r\n")
240 across_data
.append (author
+ "\r\n")
241 across_data
.append ("<COPYRIGHT>\r\n")
242 across_data
.append (copyright
+ "\r\n")
243 across_data
.append ("<SIZE>\r\n")
244 str_size
= str (self
.cols
) + "x" + str (self
.rows
)
245 across_data
.append (str_size
+ "\r\n")
246 across_data
.append ("<GRID>\r\n")
247 for row
in range (self
.rows
):
248 for col
in range (self
.cols
):
249 if (self
.data
[row
][col
].occupied_across
is True or
250 self
.data
[row
][col
].occupied_down
is True):
251 across_data
.append (self
.data
[row
][col
].char
)
253 across_data
.append (".")
254 across_data
.append ("\r\n")
256 across_data
.append ("<ACROSS>\r\n")
257 clues_across
= self
.get_clues_across ()
258 for word
, clue
in clues_across
:
260 across_data
.append (clue
+ "\r\n")
262 across_data
.append ("(No clue yet)\r\n")
264 across_data
.append ("<DOWN>\r\n")
265 clues_down
= self
.get_clues_down ()
266 for word
, clue
in clues_down
:
268 across_data
.append (clue
+ "\r\n")
270 across_data
.append ("(No clue yet\r\n")
272 acrosslite_str
= "".join (across_data
)
273 return acrosslite_str
275 # get all the clues for across
276 def get_clues_across (self
):
279 for row
in range (self
.rows
):
280 for col
in range (self
.cols
):
281 if (self
.data
[row
][col
].occupied_across
is True and
282 self
.data
[row
][col
].across_start
is True):
283 word_across
= self
.get_word_across (row
, col
)
284 clues
.append ((word_across
, self
.data
[row
][col
].clue_across
))
285 # if no across words are found at all
287 raise NoWordsException
291 # get all the clues for down
292 def get_clues_down (self
):
295 for row
in range (self
.rows
):
296 for col
in range (self
.cols
):
297 if (self
.data
[row
][col
].occupied_down
is True and
298 self
.data
[row
][col
].down_start
is True):
299 word_down
= self
.get_word_down (row
, col
)
300 clues
.append ((word_down
, self
.data
[row
][col
].clue_down
))
301 # if no down words are found at all
303 raise NoWordsException
307 # getting an across word at a number (note that the grid should be
308 # frozen for calling this otherwise a FrozenGridException will be raised)
309 def get_word_across_at_num (self
, num
):
310 # assert that the grid is frozen
311 self
.assert_frozen_grid ()
314 for row
in range (self
.rows
):
315 for col
in range (self
.cols
):
316 if self
.data
[row
][col
].numbered
== num
:
317 word
= self
.get_word_across (row
, col
)
320 # if number is not found
321 raise NoNumberException (num
)
323 # getting a down word at a number (note that the grid should be frozen
324 # for calling this otherwise a FrozenGridException will be raised)
325 def get_word_down_at_num (self
, num
):
326 # assert that the grid is frozen
327 self
.assert_frozen_grid ()
330 for row
in range (self
.rows
):
331 for col
in range (self
.cols
):
332 if self
.data
[row
][col
].numbered
== num
:
333 word
= self
.get_word_down (row
, col
)
336 # if number is not found
337 raise NoNumberException (num
)
339 # getting the position of a number on the grid (note that the grid should
340 # be frozen for calling this otherwise a FrozenGridException will be raised)
341 def get_position_of_num (self
, num
):
342 # assert that the grid is frozen
343 self
.assert_frozen_grid ()
346 for row
in range (self
.rows
):
347 for col
in range (self
.cols
):
348 if self
.data
[row
][col
].numbered
== num
:
351 # if number is not found
352 raise NoNumberException (num
)
354 # getting a down word at a position
355 def get_word_down (self
, row
, col
):
356 # if index is out of bounds
357 if row
>= self
.rows
or col
>= self
.cols
:
358 raise NoWordException (row
, col
)
360 # if there is no occupied down letter at that position
361 if self
.data
[row
][col
].occupied_down
is False:
362 raise NoWordException (row
, col
)
364 # now traverse the grid to find the beginning of the word
367 # if it is occupied down and is the beginning of the word
368 if (self
.data
[i
][col
].occupied_down
is True and
369 self
.data
[i
][col
].down_start
is True):
376 # now seek the end of the word
378 if self
.data
[i
][col
].occupied_down
is True:
379 word_chars
.append (self
.data
[i
][col
].char
)
384 word
= "".join (word_chars
)
386 # return the word, starting row, column and length as a tuple
387 return (word
, start_row
, col
, len(word
))
389 # getting an across word at a position
390 def get_word_across (self
, row
, col
):
391 # if index is out of bounds
392 if row
>= self
.rows
or col
>= self
.cols
:
393 raise NoWordException (row
, col
)
395 # if there is no occupied across letter at that position
396 if self
.data
[row
][col
].occupied_across
is False:
397 raise NoWordException (row
, col
)
399 # now traverse the grid to look for the beginning of the word
402 # if it is occupied across and is the beginning of the word
403 if (self
.data
[row
][i
].occupied_across
is True and
404 self
.data
[row
][i
].across_start
is True):
411 # now seek the end of the word
413 if self
.data
[row
][i
].occupied_across
is True:
414 word_chars
.append (self
.data
[row
][i
].char
)
419 word
= "".join (word_chars
)
421 # return the word, starting column, row and length as a tuple
422 return (word
, row
, start_col
, len(word
))
424 # setting a down word
425 def set_word_down (self
, row
, col
, word
):
426 # if the grid is frozen then abort
427 self
.assert_unfrozen_grid ()
429 # if the word length greater than totalrows - startrow
430 if len(word
) > self
.rows
- row
:
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
+i
][col
].occupied_down
is True:
437 raise IntersectWordException (word
, len(word
))
438 # on the previous column except first column
440 # except the first and last col
441 if i
> 0 and i
< len(word
) - 1:
442 if self
.data
[row
+i
][col
-1].occupied_down
is True:
443 raise IntersectWordException (word
, len(word
))
444 # if the previous column is the end of an across word
445 if (self
.data
[row
+i
][col
-1].occupied_across
is True and
446 self
.data
[row
+i
][col
].occupied_across
is False):
447 raise IntersectWordException (word
, len(word
))
449 # on the next column except last column
450 if col
< len(word
) - 1:
451 # except the first and last row check if there is any
452 # down word in previous column
453 if i
> 0 and i
< len(word
) - 1:
454 if self
.data
[row
+i
][col
+1].occupied_down
is True:
455 raise IntersectWordException (word
, len(word
))
456 # check if there is any across word starting in the
458 if self
.data
[row
+i
][col
+1].across_start
is True:
459 raise IntersectWordException (word
, len(word
))
461 # also check the character before and after
462 if (row
> 0 and self
.data
[row
-1][col
].occupied_down
is True
463 and self
.data
[row
-1][col
].occupied_across
is True):
464 raise IntersectWordException (word
, len(word
))
465 if (row
+ len(word
) < self
.rows
and
466 self
.data
[row
+len(word
)][col
].occupied_across
is True and
467 self
.data
[row
+len(word
)][col
].occupied_down
is True):
468 raise IntersectWordException (word
, len(word
))
470 # set the down start to true
471 self
.data
[row
][col
].down_start
= True
473 for i
in range (len(word
)):
474 self
.data
[row
+i
][col
].occupied_down
= True
475 self
.data
[row
+i
][col
].char
= word
[i
].upper ()
478 # setting an across word
479 def set_word_across (self
, row
, col
, word
):
480 # if the grid is frozen then abort
481 self
.assert_unfrozen_grid ()
483 # is the word length greater than totalcols - startcol?
484 if len(word
) > self
.cols
- col
:
485 raise TooLongWordException (word
, len(word
))
487 # is the word intersecting any other word?
488 for i
in range (len(word
)):
490 if self
.data
[row
][col
+i
].occupied_across
is True:
491 raise IntersectWordException (word
, len(word
))
492 # on a previous row except first row
494 # if not the first or last col
495 if i
> 0 and i
< len(word
) - 1:
496 if self
.data
[row
-1][col
+i
].occupied_across
is True:
497 raise IntersectWordException (word
, len(word
))
498 # if the previous row is the end of a down word
499 if (self
.data
[row
-1][col
+i
].occupied_down
is True and
500 self
.data
[row
][col
+i
].occupied_down
is False):
501 raise IntersectWordException (word
, len(word
))
504 if (row
< (self
.rows
- 1)):
505 # except the first and last letter check if there is
506 # any across intersection
507 if i
> 0 and i
< len (word
) - 1:
508 if self
.data
[row
+1][col
+i
].occupied_across
is True:
509 raise IntersectWordException (word
, len(word
))
510 # if a down word is starting at any column below the
512 if self
.data
[row
+1][col
+i
].down_start
is True:
513 raise IntersectWordException (word
, len(word
))
515 # also check the character beyond and before and after
516 if (col
> 0 and (self
.data
[row
][col
-1].occupied_across
is True or
517 self
.data
[row
][col
-1].occupied_down
is True)):
518 raise IntersectWordException (word
, len(word
))
519 if (col
+ len(word
) < self
.cols
and
520 (self
.data
[row
][col
+len(word
)].occupied_across
is True or
521 self
.data
[row
][col
+len(word
)].occupied_down
is True)):
522 raise IntersectWordException (word
, len(word
))
524 # set across start to true
525 self
.data
[row
][col
].across_start
= True
528 for i
in range (len(word
)):
529 self
.data
[row
][col
+i
].char
= word
[i
].upper ()
530 self
.data
[row
][col
+i
].occupied_across
= True
532 # freeze the grid numbers etc.
533 def freeze_grid (self
):
536 # run through the grid
537 for row
in range (self
.rows
):
538 for col
in range (self
.cols
):
539 # if grid is blank set the character to #
540 if (self
.data
[row
][col
].occupied_across
is False
541 and self
.data
[row
][col
].occupied_down
is False):
542 self
.data
[row
][col
].char
= "#"
543 elif (self
.data
[row
][col
].across_start
is True or
544 self
.data
[row
][col
].down_start
is True):
545 self
.data
[row
][col
].numbered
= numbering
548 self
.frozen_grid
= True
550 # unfreeze the grid numbers etc.
551 def unfreeze_grid (self
):
552 # run through the grid
553 for row
in range (self
.rows
):
554 for col
in range (self
.cols
):
555 self
.data
[row
][col
].numbered
= 0
556 if (self
.data
[row
][col
].occupied_across
is False and
557 self
.data
[row
][col
].occupied_down
is False):
558 self
.data
[row
][col
].char
= '.'
560 self
.frozen_grid
= False
562 # raise an exception if the grid is frozen
563 def assert_unfrozen_grid (self
):
564 if self
.frozen_grid
is True:
565 raise FrozenGridException
567 # raise an exception if the grid is NOT frozen
568 def assert_frozen_grid (self
):
569 if self
.frozen_grid
is False:
570 raise FrozenGridException
572 # reset the entire grid
573 def reset_grid (self
):
574 # run through the grid
575 for row
in range (self
.rows
):
576 for col
in range (self
.cols
):
577 # re-initialize all data
578 self
.data
[row
][col
].reset ()
580 self
.frozen_grid
= False
582 # remove an across word at position
583 def remove_word_across (self
, row
, col
):
584 # if grid is frozen don't allow removal of word
585 self
.assert_unfrozen_grid ()
587 word
, brow
, bcol
, l
= self
.get_word_across (row
, col
)
589 # traverse from the beginning to end of the word and erase it
592 if self
.data
[brow
][c
].occupied_across
is True:
593 self
.data
[brow
][c
].clear_across_data ()
598 # remove a down word at position
599 def remove_word_down (self
, row
, col
):
600 # if grid is frozen don't allow removal of word
601 self
.assert_unfrozen_grid ()
603 word
, brow
, bcol
, l
= self
.get_word_down (row
, col
)
604 # traverse from the beginn to end of the word and erase it
607 if self
.data
[r
][bcol
].occupied_down
is True:
608 self
.data
[r
][bcol
].clear_down_data ()
613 # reveal/unreveal a word at position
614 def reveal_word_across (self
, row
, col
, revealed
=True):
615 # set the revealed flag for the word at the position
616 word
= self
.get_word_across (row
, col
)
620 if self
.data
[word
[1]][c
].occupied_across
is True:
621 self
.data
[word
[1]][c
].revealed
= revealed
626 # reveal/unreveal a word at position
627 def reveal_word_down (self
, row
, col
, revealed
=True):
628 # set the revealed flag for the word at the position
629 word
= self
.get_word_down (row
, col
)
633 if self
.data
[r
][word
[2]].occupied_down
is True:
634 self
.data
[r
][word
[2]].revealed
= revealed
639 # reveal/hide the entire solution by resetting revealed flag at all cells
640 def reveal_solution (self
, revealed
=True):
641 # run through the grid and set revealed to False
642 for row
in range (self
.rows
):
643 for col
in range (self
.cols
):
644 self
.data
[row
][col
].revealed
= revealed