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
='.', 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
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
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
34 # exception for too long words
35 class TooLongWordException (Exception):
36 def __init__ (self
, word
, length
):
40 # exception for intersecting words
41 class IntersectWordException (Exception):
42 def __init__ (self
, word
, length
):
46 # exception when grid is sought to be changed when frozen
47 class FrozenGridException (Exception):
49 self
.msg
= "Grid is frozen and cannot be edited"
51 # exception when no word is found at a position
52 class NoWordException (Exception):
53 def __init__ (self
, row
, col
):
56 # exception when no words are present in the grid
57 class NoWordsException (Exception):
59 self
.msg
= "No words in grid"
61 class CrosswordPuzzle
:
62 def __init__ (self
, rows
, cols
):
63 # define number of rows and columns
67 # initialize the list to hold the grid
70 # initial state of the grid is unfrozen
71 self
.frozen_grid
= False
73 # create the grid data
74 for i
in range (rows
):
76 for j
in range (cols
):
77 self
.data
[i
].append (GridItem ())
80 def export_image (self
, pngfile
, htmlfile
=None, puztitle
="Crossword Puzzle",
82 # don't export if grid is not frozen
83 if self
.frozen_grid
is False:
84 raise FrozenGridException
86 # create cairo image surface and context
88 surf
= cairo
.ImageSurface (cairo
.FORMAT_RGB24
, self
.cols
*px
, self
.rows
*px
)
89 ctx
= cairo
.Context (surf
)
91 ctx
.set_source_rgb (1, 1, 1)
92 ctx
.rectangle (0, 0, self
.cols
*px
, self
.rows
*px
)
95 # get the clues across and down
96 clues_across
= self
.get_clues_across ()
97 clues_down
= self
.get_clues_down ()
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
)
111 ctx
.set_source_rgb (1, 1, 1)
112 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
114 ctx
.set_source_rgb (0, 0, 0)
115 ctx
.rectangle (col
*px
, row
*px
, px
, px
)
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
))
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
)
131 surf
.write_to_png (open (pngfile
, "wb"))
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" />')
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
) + " - " \
148 html_contents
.append (clue_str
)
149 html_contents
.append ("<br />")
150 html_contents
.append ("</p>")
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
) + " - " \
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>")
163 html_str
= "\r\n".join (html_contents
)
165 fhtml
= open (htmlfile
, "wb")
166 fhtml
.write (html_str
)
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
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
)
193 across_data
.append (".")
194 across_data
.append ("\r\n")
196 across_data
.append ("<ACROSS>\r\n")
197 clues_across
= self
.get_clues_across ()
198 for word
, clue
in clues_across
:
200 across_data
.append (clue
+ "\r\n")
202 across_data
.append ("(No clue yet)\r\n")
204 across_data
.append ("<DOWN>\r\n")
205 clues_down
= self
.get_clues_down ()
206 for word
, clue
in clues_down
:
208 across_data
.append (clue
+ "\r\n")
210 across_data
.append ("(No clue yet\r\n")
212 acrosslite_str
= "".join (across_data
)
213 return acrosslite_str
215 # get all the clues for across
216 def get_clues_across (self
):
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
227 raise NoWordsException
231 # get all the clues for down
232 def get_clues_down (self
):
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
243 raise NoWordsException
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
)
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
)
257 # now traverse the grid to find the beginning of the word
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):
269 # now seek the end of the word
271 if self
.data
[i
][col
].occupied_down
is True:
272 word_chars
.append (self
.data
[i
][col
].char
)
277 word
= "".join (word_chars
)
279 # return the word, starting row, column and length as a tuple
280 return (word
, start_row
, col
, len(word
))
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
)
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
)
292 # now traverse the grid to look for the beginning of the word
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):
304 # now seek the end of the word
306 if self
.data
[row
][i
].occupied_across
is True:
307 word_chars
.append (self
.data
[row
][i
].char
)
312 word
= "".join (word_chars
)
314 # return the word, starting column, row and length as a tuple
315 return (word
, row
, start_col
, len(word
))
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
323 # if the word length greater than totalrows - startrow
324 if len(word
) > self
.rows
- row
:
325 raise TooLongWordException (word
, len(word
))
327 # is the word intersecting any other word?
328 for i
in range (len(word
)):
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
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
347 if self
.data
[row
+i
][col
+1].across_start
is True:
348 raise IntersectWordException (word
, len(word
))
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
))
359 # set the down start to true
360 self
.data
[row
][col
].down_start
= True
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 ()
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
373 # is the word length greater than totalcols - startcol?
374 if len(word
) > self
.cols
- col
:
375 raise TooLongWordException (word
, len(word
))
377 # is the word intersecting any other word?
378 for i
in range (len(word
)):
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
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
))
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
397 if self
.data
[row
+1][col
+i
].down_start
is True:
398 raise IntersectWordException (word
, len(word
))
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
))
409 # set across start to true
410 self
.data
[row
][col
].across_start
= True
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
417 # freeze the grid numbers etc.
418 def freeze_grid (self
):
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
433 self
.frozen_grid
= True
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
= '.'
445 self
.frozen_grid
= False