Added functionality to across and down clues list
[getaclue.git] / crosswordpuzzlecreator.py
1 # Get A Clue (C) 2010 V. Harishankar
2 # Crossword puzzle maker program
3 # Licensed under the GNU GPL v3
4
5 # Cross puzzle creator class
6
7 import sys
8 import cPickle
9 import readline
10
11 import crosswordpuzzle
12
13 class CrosswordPuzzleCreator:
14 # ansi color codes for grid display
15 BRICKRED = '\033[31m'
16 # bold
17 BOLD = '\033[33m'
18 # blue
19 BLUE = '\033[34m'
20 # grey
21 GREY = '\033[30m'
22 # Black background white text
23 BLACK_BG = '\033[40;1;37m'
24 # white background black text
25 WHITE_BG = '\033[47;1;30m'
26 # white background red text
27 REDWHITE_BG = '\033[47;1;31m'
28 # disable colors
29 ENDCOL = '\033[0m'
30
31 def __init__ (self):
32 self.do_main_loop ()
33 self.current_file = None
34 self.puzzle = None
35
36 # save the puzzle
37 def save_puzzle (self):
38 if self.current_file:
39 fpuzzle = open (self.current_file, "wb")
40 cPickle.dump (self.puzzle, fpuzzle, cPickle.HIGHEST_PROTOCOL)
41 sys.stdout.write (self.BLUE + "Puzzle saved to: " +
42 self.current_file + "\n" + self.ENDCOL)
43
44 # load the puzzle
45 def load_puzzle (self):
46 if self.current_file:
47 fpuzzle = open (self.current_file, "rb")
48 self.puzzle = cPickle.load (fpuzzle)
49
50 # display the grid with words
51 def print_puzzle (self, no_words=False):
52 # if self.puzzle is not none
53 if self.puzzle:
54 # get row, col and print them (with grid number if set)
55 for col in range (self.puzzle.cols):
56 # print the first row as column headers
57 sys.stdout.write (self.BLUE + ' %02d' % col + self.ENDCOL)
58 sys.stdout.write ("\n")
59
60 for row in range (self.puzzle.rows):
61 for col in range (self.puzzle.cols):
62 # print the data
63 # if the cell is numbered i.e. start of a word
64 if self.puzzle.data[row][col].numbered != 0:
65 sys.stdout.write (self.REDWHITE_BG +
66 '%02d ' % self.puzzle.data[row][col].numbered + self.ENDCOL)
67 # print spaces
68 else:
69 if self.puzzle.data[row][col].char == "#":
70 sys.stdout.write (self.BLACK_BG + ' ' + self.ENDCOL)
71 else:
72 sys.stdout.write (self.WHITE_BG + ' ' + self.ENDCOL)
73 # if the character is not a blank or a block
74 if (self.puzzle.data[row][col].char <> "." and
75 self.puzzle.data[row][col].char <> "#"):
76 # if words are to be shown regardless of hidden/revealed state
77 if no_words is False:
78 sys.stdout.write (self.WHITE_BG +
79 self.puzzle.data[row][col].char + self.ENDCOL)
80 else:
81 # display only revealed
82 if self.puzzle.data[row][col].revealed is True:
83 sys.stdout.write (self.WHITE_BG +
84 self.puzzle.data[row][col].char + self.ENDCOL)
85 # else print a blank
86 else:
87 sys.stdout.write (self.WHITE_BG + '.' + self.ENDCOL)
88 elif self.puzzle.data[row][col].char == '.':
89 sys.stdout.write (self.WHITE_BG +
90 self.puzzle.data[row][col].char + self.ENDCOL)
91 elif self.puzzle.data[row][col].char == '#':
92 sys.stdout.write (self.BLACK_BG + " " + self.ENDCOL)
93
94 sys.stdout.write (' ' + self.BLUE + "%2d" % row + self.ENDCOL + "\n")
95 raw_input (self.BRICKRED + "Press <return> to continue" + self.ENDCOL)
96
97 # display existing clues
98 def on_display_clues (self):
99 try:
100 aclues = self.puzzle.get_clues_across ()
101 sys.stdout.write (self.BOLD + "\n------------\n")
102 sys.stdout.write ("Across words\n")
103 sys.stdout.write ("------------\n" + self.ENDCOL)
104
105 for word, clue in aclues:
106 sys.stdout.write (self.BOLD + word[0] + ": " + self.ENDCOL)
107 if clue:
108 sys.stdout.write (self.BLUE + clue + "\n" + self.ENDCOL)
109 else:
110 sys.stdout.write (self.BLUE + "(No clue yet)\n" + self.ENDCOL)
111 except crosswordpuzzle.NoWordsException:
112 sys.stderr.write ("No words across\n")
113
114 try:
115 dclues = self.puzzle.get_clues_down ()
116 sys.stdout.write (self.BOLD + "\n----------\n")
117 sys.stdout.write ("Down words\n")
118 sys.stdout.write ("----------\n" + self.ENDCOL)
119
120 for word, clue in dclues:
121 sys.stdout.write (self.BOLD + word[0] + ": " + self.ENDCOL)
122 if clue:
123 sys.stdout.write (self.BLUE + clue + "\n" + self.ENDCOL)
124 else:
125 sys.stdout.write (self.BLUE + "(No clue yet)\n" + self.ENDCOL)
126 except crosswordpuzzle.NoWordsException:
127 sys.stderr.write ("No words down\n")
128
129 raw_input (self.BRICKRED + "Press <return> to continue" + self.ENDCOL)
130
131 # set a clue to a word
132 def on_set_clue (self):
133 self.print_puzzle ()
134 # get the row and column
135 srow = raw_input (self.BRICKRED + "At row: " + self.ENDCOL)
136 scol = raw_input (self.BRICKRED + "At col: " + self.ENDCOL)
137 # try converting it to number
138 try:
139 row = int (srow)
140 col = int (scol)
141 except ValueError:
142 sys.stderr.write ("Invalid row or column\n")
143 return
144
145 try:
146 # across word set the clue if found
147 aword, arow, acol, alen = self.puzzle.get_word_across (row, col)
148 sys.stdout.write (self.BLUE + "Across word at position: " + aword + "\n" + self.ENDCOL)
149 clue = raw_input (self.BRICKRED + "Clue for across word: " + self.ENDCOL)
150 self.puzzle.data[arow][acol].clue_across = clue
151 sys.stdout.write (self.BLUE + "Set the clue: \n" + self.puzzle.data[arow][acol].clue_across)
152 except crosswordpuzzle.NoWordException:
153 sys.stderr.write ("No across word found at that position\n")
154
155 try:
156 # down word set the clue if found
157 dword, drow, dcol, dlen = self.puzzle.get_word_down (row, col)
158 sys.stdout.write (self.BLUE + "Down word at position: " + dword + "\n" + self.ENDCOL)
159 clue = raw_input (self.BRICKRED + "Clue for down word: " + self.ENDCOL)
160 self.puzzle.data[drow][dcol].clue_down = clue
161 sys.stdout.write (self.BLUE + "Set the clue: \n" + self.puzzle.data[drow][dcol].clue_down)
162 except crosswordpuzzle.NoWordException:
163 sys.stderr.write ("No down word found at that position\n")
164
165 # remove a down word
166 def on_remove_down (self):
167 self.print_puzzle ()
168
169 srow = raw_input (self.BRICKRED + "At row: " + self.ENDCOL)
170 scol = raw_input (self.BRICKRED + "At col: " + self.ENDCOL)
171 try:
172 row = int (srow)
173 col = int (scol)
174 except ValueError:
175 sys.stderr.write ("Invalid row or column\n")
176 return
177
178 try:
179 self.puzzle.remove_word_down (row, col)
180 sys.stdout.write (self.BLUE + "Down word removed\n" + self.ENDCOL)
181 except crosswordpuzzle.FrozenGridException:
182 sys.stderr.write ("Word cannot be removed from a frozen puzzle\n")
183 except crosswordpuzzle.NoWordException:
184 sys.stderr.write ("No down word found at that position\n")
185
186 # remove an across word
187 def on_remove_across (self):
188 self.print_puzzle ()
189
190 srow = raw_input (self.BRICKRED + "At row: " + self.ENDCOL)
191 scol = raw_input (self.BRICKRED + "At col: " + self.ENDCOL)
192 try:
193 row = int (srow)
194 col = int (scol)
195 except ValueError:
196 sys.stderr.write ("Invalid row or column\n")
197 return
198
199 try:
200 self.puzzle.remove_word_across (row, col)
201 sys.stdout.write (self.BLUE + "Across word removed\n" + self.ENDCOL)
202 except crosswordpuzzle.FrozenGridException:
203 sys.stderr.write ("Word cannot be removed from a frozen puzzle\n")
204 except crosswordpuzzle.NoWordException:
205 sys.stderr.write ("No across word found at that position\n")
206
207 # add a word to the puzzle
208 def on_add_word (self, across=True):
209 # first display the grid
210 self.print_puzzle ()
211 # get the row and column
212 srow = raw_input (self.BRICKRED + "Start row: " + self.ENDCOL)
213 scol = raw_input (self.BRICKRED + "Start col: " + self.ENDCOL)
214 # try converting it to number
215 try:
216 row = int (srow)
217 col = int (scol)
218 except ValueError:
219 sys.stderr.write ("Invalid row or column\n")
220 return
221 # get the word
222 word = raw_input (self.BRICKRED + "Word: " + self.ENDCOL)
223
224 # try to add the word to the puzzle grid
225 try:
226 if across == True:
227 self.puzzle.set_word_across (row, col, word)
228 else:
229 self.puzzle.set_word_down (row, col, word)
230 except crosswordpuzzle.TooLongWordException:
231 sys.stderr.write ("Word is too long to fit in the grid! Aborting.\n")
232 except crosswordpuzzle.IntersectWordException:
233 sys.stderr.write ("Word intersects badly with another word!\n")
234 except crosswordpuzzle.FrozenGridException:
235 sys.stderr.write ("Word cannot be added to a frozen puzzle.\n")
236
237 # Export to image/HTML
238 def on_export_image (self, solution=True):
239 try:
240 sys.stdout.write (self.BLUE + "Exporting puzzle to image/HTML\n")
241 pngfile = raw_input (self.BRICKRED + "Filename (PNG): " + self.ENDCOL)
242 if solution is False:
243 htmlfile = raw_input (self.BRICKRED + "Filename (HTML): " +
244 self.ENDCOL)
245 puztitle = raw_input (self.BRICKRED + "Title of puzzle: " +
246 self.ENDCOL)
247 self.puzzle.export_image (pngfile, htmlfile, puztitle, solution)
248 else:
249 self.puzzle.export_image (pngfile)
250
251 sys.stdout.write (self.BLUE + "Successfully exported!")
252 except crosswordpuzzle.FrozenGridException:
253 sys.stderr.write ("Cannot export as grid is not frozen/finalized\n")
254 except crosswordpuzzle.NoWordsException:
255 sys.stderr.write ("No words to export!\n")
256
257 # Export to across lite
258 def on_export_acrosslite (self):
259 try:
260 sys.stdout.write (self.BLUE + "Exporting to AcrossLite(tm) Format\n" +
261 self.ENDCOL)
262 title = raw_input (self.BRICKRED + "Puzzle title: " + self.ENDCOL)
263 name = raw_input (self.BRICKRED + "Author name: " + self.ENDCOL)
264 copyright = raw_input (self.BRICKRED + "Copyright: " + self.ENDCOL)
265 exportfile = raw_input (self.BRICKRED + "Export to file: " + self.ENDCOL)
266
267 acrosslite_str = self.puzzle.export_acrosslite (title, name, copyright)
268 fexport = open (exportfile, "w")
269 fexport.write (acrosslite_str)
270 fexport.close ()
271 sys.stdout.write (self.BLUE + "Exported AcrossLite(tm) File: " +
272 exportfile + "\n" + self.ENDCOL)
273 except crosswordpuzzle.FrozenGridException:
274 sys.stderr.write ("Cannot export as grid is not frozen/finalized\n")
275 except crosswordpuzzle.NoWordsException:
276 sys.stderr.write ("No words to export!\n")
277
278 # Puzzle loop
279 def do_puzzle_loop (self):
280 # there is a current file
281 if self.current_file and self.puzzle:
282 while True:
283 sys.stdout.write (self.BOLD + "\n-----------------------------------\n")
284 sys.stdout.write ("Puzzle: " + self.current_file + "\n")
285 sys.stdout.write ("-----------------------------------" + self.ENDCOL + "\n")
286 sys.stdout.write (self.BLUE + "1. Display grid\n")
287 sys.stdout.write ("2. Add across word\n")
288 sys.stdout.write ("3. Add down word\n")
289 sys.stdout.write ("4. Remove across word\n")
290 sys.stdout.write ("5. Remove down word\n")
291 sys.stdout.write ("6. Freeze grid\n")
292 sys.stdout.write ("7. Unfreeze grid\n")
293 sys.stdout.write ("8. Set clue for word\n")
294 sys.stdout.write ("9. Display clues\n")
295 sys.stdout.write ("R. Reset grid\n")
296 sys.stdout.write ("S. Save puzzle\n")
297 sys.stdout.write ("E. Export to AcrossLite(TM) format\n")
298 sys.stdout.write ("H. Export puzzle as image/HTML\n")
299 sys.stdout.write ("I. Export solution as image\n")
300 sys.stdout.write ("X. Exit to main menu\n" + self.ENDCOL)
301 ch = raw_input (self.BRICKRED + "Your choice: " + self.ENDCOL)
302 if ch == "1":
303 self.print_puzzle ()
304 elif ch == "2":
305 self.on_add_word ()
306 elif ch == "3":
307 self.on_add_word (False)
308 elif ch == "4":
309 self.on_remove_across ()
310 elif ch == "5":
311 self.on_remove_down ()
312 elif ch == "6":
313 self.puzzle.freeze_grid ()
314 elif ch == "7":
315 self.puzzle.unfreeze_grid ()
316 elif ch == "8":
317 self.on_set_clue ()
318 elif ch == "9":
319 self.on_display_clues ()
320 elif ch == "R" or ch == "r":
321 self.on_reset_grid ()
322 elif ch == "S" or ch == "s":
323 self.save_puzzle ()
324 elif ch == "E" or ch == "e":
325 self.on_export_acrosslite ()
326 elif ch == "H" or ch == "h":
327 self.on_export_image (False)
328 elif ch == "I" or ch == "i":
329 self.on_export_image ()
330 elif ch == "X" or ch == "x":
331 break
332
333 # when user chooses new puzzle
334 def on_new_puzzle (self):
335 self.current_file = raw_input (self.BRICKRED + "New puzzle file name: "
336 + self.ENDCOL)
337 srows = raw_input (self.BRICKRED + "Number of rows: " + self.ENDCOL)
338 scols = raw_input (self.BRICKRED + "Number of cols: " + self.ENDCOL)
339 try:
340 rows = int (srows)
341 cols = int (scols)
342 except ValueError:
343 sys.stderr.write ("Invalid number of rows/columns")
344 return
345 self.puzzle = crosswordpuzzle.CrosswordPuzzle (rows, cols)
346 self.do_puzzle_loop ()
347
348 # when user chooses to load puzzle
349 def on_load_puzzle (self):
350 self.current_file = raw_input (self.BRICKRED + "Puzzle to load: "
351 + self.ENDCOL)
352 self.load_puzzle ()
353 self.do_puzzle_loop ()
354
355 # when user chooses to reset grid
356 def on_reset_grid (self):
357 ans = raw_input (self.BRICKRED +
358 "This will clear the entire grid! Are you sure (Y/N)? " + self.ENDCOL)
359 if ans == "y" or ans == "Y":
360 self.puzzle.reset_grid ()
361 sys.stdout.write (self.BLUE + "Grid has been cleared of all data!"
362 + self.ENDCOL + "\n")
363
364 # Main application loop
365 def do_main_loop (self):
366 # display the menu
367 while True:
368 sys.stdout.write (self.BOLD + "\n-----------------------------------\n")
369 sys.stdout.write ("Get A Clue - Crossword Puzzle Maker\n")
370 sys.stdout.write ("-----------------------------------\n" + self.ENDCOL)
371 sys.stdout.write (self.BLUE + "1. Start a new puzzle\n")
372 sys.stdout.write ("2. Open an existing puzzle\n")
373 sys.stdout.write ("X. Exit\n" + self.ENDCOL)
374 ch = raw_input (self.BRICKRED + "Your choice: " + self.ENDCOL)
375 if ch == '1':
376 self.on_new_puzzle ()
377 if ch == '2':
378 self.on_load_puzzle ()
379 if ch == 'x' or ch == 'X':
380 break