334895ddd459d2d548d66f5227bae8bba22c4cf0
[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" +
152 self.puzzle.data[arow][acol].clue_across + "\n")
153 except crosswordpuzzle.NoWordException:
154 sys.stderr.write ("No across word found at that position\n")
155
156 try:
157 # down word set the clue if found
158 dword, drow, dcol, dlen = self.puzzle.get_word_down (row, col)
159 sys.stdout.write (self.BLUE + "Down word at position: " + dword + "\n" + self.ENDCOL)
160 clue = raw_input (self.BRICKRED + "Clue for down word: " + self.ENDCOL)
161 self.puzzle.data[drow][dcol].clue_down = clue
162 sys.stdout.write (self.BLUE + "Set the clue: \n" +
163 self.puzzle.data[drow][dcol].clue_down + "\n")
164 except crosswordpuzzle.NoWordException:
165 sys.stderr.write ("No down word found at that position\n")
166
167 # remove a down word
168 def on_remove_down (self):
169 self.print_puzzle ()
170
171 srow = raw_input (self.BRICKRED + "At row: " + self.ENDCOL)
172 scol = raw_input (self.BRICKRED + "At col: " + self.ENDCOL)
173 try:
174 row = int (srow)
175 col = int (scol)
176 except ValueError:
177 sys.stderr.write ("Invalid row or column\n")
178 return
179
180 try:
181 self.puzzle.remove_word_down (row, col)
182 sys.stdout.write (self.BLUE + "Down word removed\n" + self.ENDCOL)
183 except crosswordpuzzle.FrozenGridException:
184 sys.stderr.write ("Word cannot be removed from a frozen puzzle\n")
185 except crosswordpuzzle.NoWordException:
186 sys.stderr.write ("No down word found at that position\n")
187
188 # remove an across word
189 def on_remove_across (self):
190 self.print_puzzle ()
191
192 srow = raw_input (self.BRICKRED + "At row: " + self.ENDCOL)
193 scol = raw_input (self.BRICKRED + "At col: " + self.ENDCOL)
194 try:
195 row = int (srow)
196 col = int (scol)
197 except ValueError:
198 sys.stderr.write ("Invalid row or column\n")
199 return
200
201 try:
202 self.puzzle.remove_word_across (row, col)
203 sys.stdout.write (self.BLUE + "Across word removed\n" + self.ENDCOL)
204 except crosswordpuzzle.FrozenGridException:
205 sys.stderr.write ("Word cannot be removed from a frozen puzzle\n")
206 except crosswordpuzzle.NoWordException:
207 sys.stderr.write ("No across word found at that position\n")
208
209 # add a word to the puzzle
210 def on_add_word (self, across=True):
211 # first display the grid
212 self.print_puzzle ()
213 # get the row and column
214 srow = raw_input (self.BRICKRED + "Start row: " + self.ENDCOL)
215 scol = raw_input (self.BRICKRED + "Start col: " + self.ENDCOL)
216 # try converting it to number
217 try:
218 row = int (srow)
219 col = int (scol)
220 except ValueError:
221 sys.stderr.write ("Invalid row or column\n")
222 return
223 # get the word
224 word = raw_input (self.BRICKRED + "Word: " + self.ENDCOL)
225
226 # try to add the word to the puzzle grid
227 try:
228 if across == True:
229 self.puzzle.set_word_across (row, col, word)
230 else:
231 self.puzzle.set_word_down (row, col, word)
232 except crosswordpuzzle.TooLongWordException:
233 sys.stderr.write ("Word is too long to fit in the grid! Aborting.\n")
234 except crosswordpuzzle.IntersectWordException:
235 sys.stderr.write ("Word intersects badly with another word!\n")
236 except crosswordpuzzle.FrozenGridException:
237 sys.stderr.write ("Word cannot be added to a frozen puzzle.\n")
238
239 # Export to image/HTML
240 def on_export_image (self, solution=True):
241 try:
242 sys.stdout.write (self.BLUE + "Exporting puzzle to image/HTML\n")
243 pngfile = raw_input (self.BRICKRED + "Filename (PNG): " + self.ENDCOL)
244 if solution is False:
245 htmlfile = raw_input (self.BRICKRED + "Filename (HTML): " +
246 self.ENDCOL)
247 puztitle = raw_input (self.BRICKRED + "Title of puzzle: " +
248 self.ENDCOL)
249 self.puzzle.export_image (pngfile, htmlfile, puztitle, solution)
250 else:
251 self.puzzle.export_image (pngfile)
252
253 sys.stdout.write (self.BLUE + "Successfully exported!")
254 except crosswordpuzzle.FrozenGridException:
255 sys.stderr.write ("Cannot export as grid is not frozen/finalized\n")
256 except crosswordpuzzle.NoWordsException:
257 sys.stderr.write ("No words to export!\n")
258
259 # Export to across lite
260 def on_export_acrosslite (self):
261 try:
262 sys.stdout.write (self.BLUE + "Exporting to AcrossLite(tm) Format\n" +
263 self.ENDCOL)
264 title = raw_input (self.BRICKRED + "Puzzle title: " + self.ENDCOL)
265 name = raw_input (self.BRICKRED + "Author name: " + self.ENDCOL)
266 copyright = raw_input (self.BRICKRED + "Copyright: " + self.ENDCOL)
267 exportfile = raw_input (self.BRICKRED + "Export to file: " + self.ENDCOL)
268
269 acrosslite_str = self.puzzle.export_acrosslite (title, name, copyright)
270 fexport = open (exportfile, "w")
271 fexport.write (acrosslite_str)
272 fexport.close ()
273 sys.stdout.write (self.BLUE + "Exported AcrossLite(tm) File: " +
274 exportfile + "\n" + self.ENDCOL)
275 except crosswordpuzzle.FrozenGridException:
276 sys.stderr.write ("Cannot export as grid is not frozen/finalized\n")
277 except crosswordpuzzle.NoWordsException:
278 sys.stderr.write ("No words to export!\n")
279
280 # Puzzle loop
281 def do_puzzle_loop (self):
282 # there is a current file
283 if self.current_file and self.puzzle:
284 while True:
285 sys.stdout.write (self.BOLD + "\n-----------------------------------\n")
286 sys.stdout.write ("Puzzle: " + self.current_file + "\n")
287 sys.stdout.write ("-----------------------------------" + self.ENDCOL + "\n")
288 sys.stdout.write (self.BLUE + "1. Display grid\n")
289 sys.stdout.write ("2. Add across word\n")
290 sys.stdout.write ("3. Add down word\n")
291 sys.stdout.write ("4. Remove across word\n")
292 sys.stdout.write ("5. Remove down word\n")
293 sys.stdout.write ("6. Freeze grid\n")
294 sys.stdout.write ("7. Unfreeze grid\n")
295 sys.stdout.write ("8. Set clue for word\n")
296 sys.stdout.write ("9. Display clues\n")
297 sys.stdout.write ("R. Reset grid\n")
298 sys.stdout.write ("S. Save puzzle\n")
299 sys.stdout.write ("E. Export to AcrossLite(TM) format\n")
300 sys.stdout.write ("H. Export puzzle as image/HTML\n")
301 sys.stdout.write ("I. Export solution as image\n")
302 sys.stdout.write ("X. Exit to main menu\n" + self.ENDCOL)
303 ch = raw_input (self.BRICKRED + "Your choice: " + self.ENDCOL)
304 if ch == "1":
305 self.print_puzzle ()
306 elif ch == "2":
307 self.on_add_word ()
308 elif ch == "3":
309 self.on_add_word (False)
310 elif ch == "4":
311 self.on_remove_across ()
312 elif ch == "5":
313 self.on_remove_down ()
314 elif ch == "6":
315 self.puzzle.freeze_grid ()
316 elif ch == "7":
317 self.puzzle.unfreeze_grid ()
318 elif ch == "8":
319 self.on_set_clue ()
320 elif ch == "9":
321 self.on_display_clues ()
322 elif ch == "R" or ch == "r":
323 self.on_reset_grid ()
324 elif ch == "S" or ch == "s":
325 self.save_puzzle ()
326 elif ch == "E" or ch == "e":
327 self.on_export_acrosslite ()
328 elif ch == "H" or ch == "h":
329 self.on_export_image (False)
330 elif ch == "I" or ch == "i":
331 self.on_export_image ()
332 elif ch == "X" or ch == "x":
333 break
334
335 # when user chooses new puzzle
336 def on_new_puzzle (self):
337 self.current_file = raw_input (self.BRICKRED + "New puzzle file name: "
338 + self.ENDCOL)
339 srows = raw_input (self.BRICKRED + "Number of rows: " + self.ENDCOL)
340 scols = raw_input (self.BRICKRED + "Number of cols: " + self.ENDCOL)
341 try:
342 rows = int (srows)
343 cols = int (scols)
344 except ValueError:
345 sys.stderr.write ("Invalid number of rows/columns")
346 return
347 self.puzzle = crosswordpuzzle.CrosswordPuzzle (rows, cols)
348 self.do_puzzle_loop ()
349
350 # when user chooses to load puzzle
351 def on_load_puzzle (self):
352 self.current_file = raw_input (self.BRICKRED + "Puzzle to load: "
353 + self.ENDCOL)
354 self.load_puzzle ()
355 self.do_puzzle_loop ()
356
357 # when user chooses to reset grid
358 def on_reset_grid (self):
359 ans = raw_input (self.BRICKRED +
360 "This will clear the entire grid! Are you sure (Y/N)? " + self.ENDCOL)
361 if ans == "y" or ans == "Y":
362 self.puzzle.reset_grid ()
363 sys.stdout.write (self.BLUE + "Grid has been cleared of all data!"
364 + self.ENDCOL + "\n")
365
366 # Main application loop
367 def do_main_loop (self):
368 # display the menu
369 while True:
370 sys.stdout.write (self.BOLD + "\n-----------------------------------\n")
371 sys.stdout.write ("Get A Clue - Crossword Puzzle Maker\n")
372 sys.stdout.write ("-----------------------------------\n" + self.ENDCOL)
373 sys.stdout.write (self.BLUE + "1. Start a new puzzle\n")
374 sys.stdout.write ("2. Open an existing puzzle\n")
375 sys.stdout.write ("X. Exit\n" + self.ENDCOL)
376 ch = raw_input (self.BRICKRED + "Your choice: " + self.ENDCOL)
377 if ch == '1':
378 self.on_new_puzzle ()
379 if ch == '2':
380 self.on_load_puzzle ()
381 if ch == 'x' or ch == 'X':
382 break