Put the clues in to a scrollable pad so that we can see scroll them
[curses-crossword.git] / curses_crossword.py
1 #!/usr/bin/python
2 """
3 Play a crossword puzzle in a curses grid. Takes a command line argument of the
4 crossword puzzle file.
5 """
6
7 import curses
8 import curses.ascii
9 import locale
10 import codecs
11 import getopt
12 import os
13 import sys
14
15 locale.setlocale(locale.LC_ALL, '')
16 code = locale.getpreferredencoding()
17
18 superscript_numbers = {}
19
20 try:
21     superscript_numbers = {
22         "0": unichr(8304).encode(code),
23         "1": unichr(185).encode(code),
24         "2": unichr(178).encode(code),
25         "3": unichr(179).encode(code),
26         "4": unichr(8308).encode(code),
27         "5": unichr(8309).encode(code),
28         "6": unichr(8310).encode(code),
29         "7": unichr(8311).encode(code),
30         "8": unichr(8312).encode(code),
31         "9": unichr(8313).encode(code),
32     }
33 except UnicodeError, e:
34     for number in range(0, 10):
35         superscript_numbers[str(number)] = str(number)
36
37 filename = None
38
39 try:
40     (options, args) = getopt.getopt(sys.argv[1:], "f:", "file=")
41 except getopt.GetoptError, err:
42     print str(err)
43     sys.exit(2)
44
45 for option in options:
46     if option[0] == "-f" or option[0] == "--file":
47         filename = option[1]
48
49 if not filename and len(args) > 0:
50     filename = args[0]
51
52 if not filename:
53     sys.stderr.write("No crossword file specified, exiting.\n")
54     sys.exit(0)
55
56 if os.path.exists(filename) and os.path.isfile(filename):
57     crossworddata = codecs.open(filename, "r", "utf-8").read()
58 else:
59     sys.stderr.write("Couldn't open file %s\n" %(filename))
60     sys.exit(0)
61
62 def parsecrossword(crossworddata):
63     """
64     Read the content of a crossword file in to memory and rejig it in to the 3
65     seperate sections that we expect - namely the grid, the across clues and
66     the down clues.
67     """
68     ingrid = False
69     inacross = False
70     indown = False
71     crossword = {"grid": [],
72         "across": {},
73         "down": {},
74         "grid_questions_start": [],
75         "grid_questions_end": []}
76
77     for line in crossworddata.split("\n"):
78         line = line.strip("\n")
79         if line == "GRID":
80             ingrid = True
81             inacross = False
82             indown = False
83         elif line == "ACROSS":
84             ingrid = False
85             inacross = True
86             indown = False
87         elif line == "DOWN":
88             ingrid = False
89             inacross = False
90             indown = True
91         else:
92             if ingrid:
93                 if line != "":
94                     crossword["grid"].append(line)
95             if inacross:
96                 if line != "":
97                     parts = line.split()
98                     question_number = int(parts[0].split(",")[0])
99                     clue = " ".join(parts[1:])
100                     crossword["across"][int(question_number)] = \
101                         clue.encode(code)
102             if indown:
103                 if line != "":
104                     parts = line.split()
105                     question_number = int(parts[0].split(",")[0])
106                     clue = " ".join(parts[1:])
107                     crossword["down"][int(question_number)] = clue.encode(code)
108     num_cols = len(crossword["grid"][0])
109     num_rows = len(crossword["grid"])
110
111     current_clue_number = 1
112
113     for row in range(0, num_rows):
114         crossword["grid_questions_start"].append([])
115
116     for row in range(0, num_rows):
117         for col in range(0, num_cols):
118             have_clue = False
119             if col > 0 \
120                 and crossword["grid"][row][col - 1] == "x" \
121                 and crossword["grid"][row][col] != "x" \
122                 and col < (num_cols - 1) \
123                 and crossword["grid"][row][col + 1] != "x":
124                 have_clue = True
125             if col == 0 and crossword["grid"][row][col] != "x" \
126                 and crossword["grid"][row][col + 1] != "x":
127                 have_clue = True
128             if row > 0 and crossword["grid"][row-1][col] == "x" \
129                 and row < (num_rows - 1) \
130                 and crossword["grid"][row][col] != "x" \
131                 and crossword["grid"][row + 1][col] != "x":
132                 have_clue = True
133             if row == 0 and crossword["grid"][row][col] != "x" \
134                 and crossword["grid"][row + 1][col] != "x":
135                 have_clue = True
136             if have_clue:
137                 crossword["grid_questions_start"][row] \
138                     .append(current_clue_number)
139                 current_clue_number += 1
140             else:
141                 crossword["grid_questions_start"][row].append(0)
142
143     return crossword
144
145 def crossword(stdscr, crossworddata):
146     crossword = parsecrossword(crossworddata)
147     cury = 0
148     grid_length = len(crossword["grid"][0])
149     curx = 0
150     stdscr.addch(cury, curx, curses.ACS_ULCORNER)
151     curx += 1
152     while ((curx - 1) / 4) < grid_length:
153         stdscr.addch(cury, curx, curses.ACS_HLINE)
154         stdscr.addch(cury, curx+1, curses.ACS_HLINE)
155         stdscr.addch(cury, curx+2, curses.ACS_HLINE)
156         stdscr.addch(cury, curx+3, curses.ACS_TTEE)
157         curx += 4
158     curx -= 1
159     stdscr.addch(cury, curx, curses.ACS_URCORNER)
160     cury += 1
161     curgridy = 0
162     for line in crossword["grid"]:
163         curx = 0
164         curgridx = 0
165         for curch in line:
166             stdscr.addch(cury, curx, curses.ACS_VLINE)
167             if curx > 0:
168                 stdscr.addch(cury+1, curx, curses.ACS_PLUS)
169             else:
170                 stdscr.addch(cury+1, curx, curses.ACS_LTEE)
171             stdscr.addch(cury+1, curx + 1, curses.ACS_HLINE)
172             stdscr.addch(cury+1, curx + 2, curses.ACS_HLINE)
173             stdscr.addch(cury+1, curx + 3, curses.ACS_HLINE)
174             curx += 1
175             if curch == "x":
176                 stdscr.addch(cury, curx, curses.ACS_BLOCK)
177                 stdscr.addch(cury, curx+1, curses.ACS_BLOCK)
178                 stdscr.addch(cury, curx+2, curses.ACS_BLOCK)
179             elif crossword["grid_questions_start"][curgridy][curgridx] > 0:
180                 stdscr.addstr(cury, curx, \
181                     ''.join( \
182                         [superscript_numbers[x].decode(code) \
183                         for x in str( \
184                             crossword["grid_questions_start"] \
185                             [curgridy][curgridx])]\
186                         ).encode(code))
187             curx += 3
188             curgridx += 1
189         else:
190             stdscr.addch(cury, curx, curses.ACS_VLINE)
191             stdscr.addch(cury + 1, curx, curses.ACS_RTEE)
192         cury += 2
193         curgridy += 1
194     cury -= 1
195     curx = 0
196     stdscr.addch(cury, curx, curses.ACS_LLCORNER)
197     curx += 1
198     while ((curx - 1) / 4) < grid_length:
199         stdscr.addch(cury, curx, curses.ACS_HLINE)
200         stdscr.addch(cury, curx+1, curses.ACS_HLINE)
201         stdscr.addch(cury, curx+2, curses.ACS_HLINE)
202         stdscr.addch(cury, curx+3, curses.ACS_BTEE)
203         curx += 4
204     curx -= 1
205     stdscr.addch(cury, curx, curses.ACS_LRCORNER)
206     # draw the clues in (in their own pad)
207     cluespad = curses.newpad( \
208         len(crossword["across"].keys()) \
209         + len(crossword["down"].keys()) \
210         + 3, stdscr.getmaxyx()[1])
211     cury = (len(crossword["grid"]) * 2) + 1
212     curx = 0
213     padtlx = curx
214     padtly = cury
215
216     (padbry, padbrx) = stdscr.getmaxyx()
217     sys.stderr.write("Pady, padx, max bits: %dx%d, %dx%d\n" %(padtly, padtlx, padbry, padbrx))
218     cury = 0
219     cluespad.addstr(cury, curx, "Across")
220     cury += 1
221     for cluenumber in crossword["across"].keys():
222         cluespad.addstr(cury, curx, "%3s: %s" \
223             %(str(cluenumber), crossword["across"][cluenumber]))
224         cury += 1
225
226     cury += 1
227     cluespad.addstr(cury, curx, "Down")
228     cury += 1
229     for cluenumber in crossword["down"].keys():
230         cluespad.addstr(cury, curx, "%3s: %s" \
231             %(str(cluenumber), crossword["down"][cluenumber]))
232         cury += 1
233
234     curx = 3
235     cury = 1
236     gridx = 0
237     gridy = 0
238
239     while crossword["grid"][gridy][gridx] == "x":
240         curx += 4
241         gridx += 1
242
243     stdscr.move(cury, curx)
244     cluescury = 0
245
246     cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
247     cluespad.overlay(stdscr, cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
248
249
250     while 1:
251         inch = stdscr.getch()
252         if inch == curses.ascii.ESC:
253             break
254         if inch == curses.KEY_NPAGE:
255             if cluescury < cluespad.getmaxyx()[0] - (padbry - padtly):
256                 cluescury += 1
257             cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
258         if inch == curses.KEY_PPAGE:
259             if cluescury > 0:
260                 cluescury -= 1
261             cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
262         if inch == curses.KEY_RIGHT:
263             if gridx < (len(crossword["grid"][0]) - 1):
264                 gridx += 1
265                 curx += 4
266                 while gridx < (len(crossword["grid"][0]) -1) \
267                     and crossword["grid"][gridy][gridx] == "x":
268                     gridx += 1
269                     curx += 4
270                 while crossword["grid"][gridy][gridx] == "x":
271                     gridx -= 1
272                     curx -= 4
273                 stdscr.move(cury, curx)
274         if inch == curses.KEY_LEFT:
275             if gridx > 0:
276                 curx -= 4
277                 gridx -= 1
278                 while gridx > 0 \
279                     and crossword["grid"][gridy][gridx] == "x":
280                     gridx -= 1
281                     curx -= 4
282                 while crossword["grid"][gridy][gridx] == "x":
283                     gridx += 1
284                     curx += 4
285                 stdscr.move(cury, curx)
286         if inch == curses.KEY_UP:
287             if gridy > 0:
288                 gridy -= 1
289                 cury -= 2
290                 while gridy > 0 \
291                     and crossword["grid"][gridy][gridx] == "x":
292                     gridy -= 1
293                     cury -= 2
294                 while crossword["grid"][gridy][gridx] == "x":
295                     gridy += 1
296                     cury += 2
297                 stdscr.move(cury, curx)
298         if inch == curses.KEY_DOWN:
299             if gridy < (len(crossword["grid"]) - 1):
300                 gridy += 1
301                 cury += 2
302                 while gridy < (len(crossword["grid"]) - 1) \
303                     and crossword["grid"][gridy][gridx] == "x":
304                     gridy += 1
305                     cury += 2
306                 while crossword["grid"][gridy][gridx] == "x":
307                     gridy -= 1
308                     cury -= 2
309                 stdscr.move(cury, curx)
310         if curses.ascii.isalpha(inch) or inch == ord(" "):
311             stdscr.addch(cury, curx, inch)
312             stdscr.move(cury, curx)
313         if inch == curses.KEY_BACKSPACE or inch == curses.KEY_DC:
314             stdscr.addch(cury, curx, ord(" "))
315             stdscr.move(cury, curx)
316
317
318 curses.wrapper(crossword, crossworddata)