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