Fix refresh clues bug, add COPYING and TODO files and add GPL header to crossword...
[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 # curses_crossword.py - play crosswords in a terminal
7 # Copyright (C) 2009 Brett Parker <iDunno@sommitrealweird.co.uk>
8 #
9 # This program is free software; you can redistribute it and/or modify 
10 # it under the terms of the GNU General Public License as published by 
11 # the Free Software Foundation; either version 2 of the License, or 
12 # (at your option) any later version.
13
14 # This program is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
17 # GNU General Public License for more details.
18
19 # You should have received a copy of the GNU General Public License along
20 # with this program; if not, write to the Free Software Foundation, Inc.,
21 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23 import curses
24 import curses.ascii
25 import locale
26 import codecs
27 import getopt
28 import os
29 import sys
30
31 locale.setlocale(locale.LC_ALL, '')
32 code = locale.getpreferredencoding()
33
34 superscript_numbers = {}
35
36 try:
37     superscript_numbers = {
38         "0": unichr(8304).encode(code),
39         "1": unichr(185).encode(code),
40         "2": unichr(178).encode(code),
41         "3": unichr(179).encode(code),
42         "4": unichr(8308).encode(code),
43         "5": unichr(8309).encode(code),
44         "6": unichr(8310).encode(code),
45         "7": unichr(8311).encode(code),
46         "8": unichr(8312).encode(code),
47         "9": unichr(8313).encode(code),
48     }
49 except UnicodeError, e:
50     for number in range(0, 10):
51         superscript_numbers[str(number)] = str(number)
52
53 filename = None
54
55 try:
56     (options, args) = getopt.getopt(sys.argv[1:], "f:", "file=")
57 except getopt.GetoptError, err:
58     print str(err)
59     sys.exit(2)
60
61 for option in options:
62     if option[0] == "-f" or option[0] == "--file":
63         filename = option[1]
64
65 if not filename and len(args) > 0:
66     filename = args[0]
67
68 if not filename:
69     sys.stderr.write("No crossword file specified, exiting.\n")
70     sys.exit(0)
71
72 if os.path.exists(filename) and os.path.isfile(filename):
73     crossworddata = codecs.open(filename, "r", "utf-8").read()
74 else:
75     sys.stderr.write("Couldn't open file %s\n" %(filename))
76     sys.exit(0)
77
78 def parsecrossword(crossworddata):
79     """
80     Read the content of a crossword file in to memory and rejig it in to the 3
81     seperate sections that we expect - namely the grid, the across clues and
82     the down clues.
83     """
84     ingrid = False
85     inacross = False
86     indown = False
87     crossword = {"grid": [],
88         "across": {},
89         "down": {},
90         "grid_questions_start": [],
91         "grid_questions_end": []}
92
93     for line in crossworddata.split("\n"):
94         line = line.strip("\n")
95         if line == "GRID":
96             ingrid = True
97             inacross = False
98             indown = False
99         elif line == "ACROSS":
100             ingrid = False
101             inacross = True
102             indown = False
103         elif line == "DOWN":
104             ingrid = False
105             inacross = False
106             indown = True
107         else:
108             if ingrid:
109                 if line != "":
110                     crossword["grid"].append(line)
111             if inacross:
112                 if line != "":
113                     parts = line.split()
114                     question_number = int(parts[0].split(",")[0])
115                     clue = " ".join(parts[1:])
116                     crossword["across"][int(question_number)] = \
117                         clue.encode(code)
118             if indown:
119                 if line != "":
120                     parts = line.split()
121                     question_number = int(parts[0].split(",")[0])
122                     clue = " ".join(parts[1:])
123                     crossword["down"][int(question_number)] = clue.encode(code)
124     num_cols = len(crossword["grid"][0])
125     num_rows = len(crossword["grid"])
126
127     current_clue_number = 1
128
129     for row in range(0, num_rows):
130         crossword["grid_questions_start"].append([])
131
132     for row in range(0, num_rows):
133         for col in range(0, num_cols):
134             have_clue = False
135             if col > 0 \
136                 and crossword["grid"][row][col - 1] == "x" \
137                 and crossword["grid"][row][col] != "x" \
138                 and col < (num_cols - 1) \
139                 and crossword["grid"][row][col + 1] != "x":
140                 have_clue = True
141             if col == 0 and crossword["grid"][row][col] != "x" \
142                 and crossword["grid"][row][col + 1] != "x":
143                 have_clue = True
144             if row > 0 and crossword["grid"][row-1][col] == "x" \
145                 and row < (num_rows - 1) \
146                 and crossword["grid"][row][col] != "x" \
147                 and crossword["grid"][row + 1][col] != "x":
148                 have_clue = True
149             if row == 0 and crossword["grid"][row][col] != "x" \
150                 and crossword["grid"][row + 1][col] != "x":
151                 have_clue = True
152             if have_clue:
153                 crossword["grid_questions_start"][row] \
154                     .append(current_clue_number)
155                 current_clue_number += 1
156             else:
157                 crossword["grid_questions_start"][row].append(0)
158
159     return crossword
160
161 def crossword(stdscr, crossworddata):
162     crossword = parsecrossword(crossworddata)
163     cury = 0
164     grid_length = len(crossword["grid"][0])
165     curx = 0
166     stdscr.addch(cury, curx, curses.ACS_ULCORNER)
167     curx += 1
168     while ((curx - 1) / 4) < grid_length:
169         stdscr.addch(cury, curx, curses.ACS_HLINE)
170         stdscr.addch(cury, curx+1, curses.ACS_HLINE)
171         stdscr.addch(cury, curx+2, curses.ACS_HLINE)
172         stdscr.addch(cury, curx+3, curses.ACS_TTEE)
173         curx += 4
174     curx -= 1
175     stdscr.addch(cury, curx, curses.ACS_URCORNER)
176     cury += 1
177     curgridy = 0
178     for line in crossword["grid"]:
179         curx = 0
180         curgridx = 0
181         for curch in line:
182             stdscr.addch(cury, curx, curses.ACS_VLINE)
183             if curx > 0:
184                 stdscr.addch(cury+1, curx, curses.ACS_PLUS)
185             else:
186                 stdscr.addch(cury+1, curx, curses.ACS_LTEE)
187             stdscr.addch(cury+1, curx + 1, curses.ACS_HLINE)
188             stdscr.addch(cury+1, curx + 2, curses.ACS_HLINE)
189             stdscr.addch(cury+1, curx + 3, curses.ACS_HLINE)
190             curx += 1
191             if curch == "x":
192                 stdscr.addch(cury, curx, curses.ACS_BLOCK)
193                 stdscr.addch(cury, curx+1, curses.ACS_BLOCK)
194                 stdscr.addch(cury, curx+2, curses.ACS_BLOCK)
195             elif crossword["grid_questions_start"][curgridy][curgridx] > 0:
196                 stdscr.addstr(cury, curx, \
197                     ''.join( \
198                         [superscript_numbers[x].decode(code) \
199                         for x in str( \
200                             crossword["grid_questions_start"] \
201                             [curgridy][curgridx])]\
202                         ).encode(code))
203             curx += 3
204             curgridx += 1
205         else:
206             stdscr.addch(cury, curx, curses.ACS_VLINE)
207             stdscr.addch(cury + 1, curx, curses.ACS_RTEE)
208         cury += 2
209         curgridy += 1
210     cury -= 1
211     curx = 0
212     stdscr.addch(cury, curx, curses.ACS_LLCORNER)
213     curx += 1
214     while ((curx - 1) / 4) < grid_length:
215         stdscr.addch(cury, curx, curses.ACS_HLINE)
216         stdscr.addch(cury, curx+1, curses.ACS_HLINE)
217         stdscr.addch(cury, curx+2, curses.ACS_HLINE)
218         stdscr.addch(cury, curx+3, curses.ACS_BTEE)
219         curx += 4
220     curx -= 1
221     stdscr.addch(cury, curx, curses.ACS_LRCORNER)
222     # draw the clues in (in their own pad)
223     cluespad = curses.newpad( \
224         len(crossword["across"].keys()) \
225         + len(crossword["down"].keys()) \
226         + 3, stdscr.getmaxyx()[1])
227     cury = (len(crossword["grid"]) * 2) + 1
228     curx = 0
229     padtlx = curx
230     padtly = cury
231
232     (padbry, padbrx) = stdscr.getmaxyx()
233     if padbry > cluespad.getmaxyx()[0] + padtly:
234         padbry = cluespad.getmaxyx()[0] + padtly
235     cury = 0
236     cluespad.addstr(cury, curx, "Across")
237     cury += 1
238     for cluenumber in crossword["across"].keys():
239         cluespad.addstr(cury, curx, "%3s: %s" \
240             %(str(cluenumber), crossword["across"][cluenumber]))
241         cury += 1
242
243     cury += 1
244     cluespad.addstr(cury, curx, "Down")
245     cury += 1
246     for cluenumber in crossword["down"].keys():
247         cluespad.addstr(cury, curx, "%3s: %s" \
248             %(str(cluenumber), crossword["down"][cluenumber]))
249         cury += 1
250
251     curx = 3
252     cury = 1
253     gridx = 0
254     gridy = 0
255
256     while crossword["grid"][gridy][gridx] == "x":
257         curx += 4
258         gridx += 1
259
260     stdscr.move(cury, curx)
261     cluescury = 0
262
263     cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
264     cluespad.overlay(stdscr, cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
265
266
267     while 1:
268         inch = stdscr.getch()
269         if inch == curses.ascii.ESC:
270             break
271         if inch == curses.KEY_NPAGE:
272             if cluescury < cluespad.getmaxyx()[0] - (padbry - padtly):
273                 cluescury += 1
274             cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
275             stdscr.refresh()
276         if inch == curses.KEY_PPAGE:
277             if cluescury > 0:
278                 cluescury -= 1
279             cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
280             stdscr.refresh()
281         if inch == curses.KEY_RIGHT:
282             if gridx < (len(crossword["grid"][0]) - 1):
283                 gridx += 1
284                 curx += 4
285                 while gridx < (len(crossword["grid"][0]) -1) \
286                     and crossword["grid"][gridy][gridx] == "x":
287                     gridx += 1
288                     curx += 4
289                 while crossword["grid"][gridy][gridx] == "x":
290                     gridx -= 1
291                     curx -= 4
292                 stdscr.move(cury, curx)
293         if inch == curses.KEY_LEFT:
294             if gridx > 0:
295                 curx -= 4
296                 gridx -= 1
297                 while gridx > 0 \
298                     and crossword["grid"][gridy][gridx] == "x":
299                     gridx -= 1
300                     curx -= 4
301                 while crossword["grid"][gridy][gridx] == "x":
302                     gridx += 1
303                     curx += 4
304                 stdscr.move(cury, curx)
305         if inch == curses.KEY_UP:
306             if gridy > 0:
307                 gridy -= 1
308                 cury -= 2
309                 while gridy > 0 \
310                     and crossword["grid"][gridy][gridx] == "x":
311                     gridy -= 1
312                     cury -= 2
313                 while crossword["grid"][gridy][gridx] == "x":
314                     gridy += 1
315                     cury += 2
316                 stdscr.move(cury, curx)
317         if inch == curses.KEY_DOWN:
318             if gridy < (len(crossword["grid"]) - 1):
319                 gridy += 1
320                 cury += 2
321                 while gridy < (len(crossword["grid"]) - 1) \
322                     and crossword["grid"][gridy][gridx] == "x":
323                     gridy += 1
324                     cury += 2
325                 while crossword["grid"][gridy][gridx] == "x":
326                     gridy -= 1
327                     cury -= 2
328                 stdscr.move(cury, curx)
329         if curses.ascii.isalpha(inch) or inch == ord(" "):
330             stdscr.addch(cury, curx, inch)
331             stdscr.move(cury, curx)
332         if inch == curses.KEY_BACKSPACE or inch == curses.KEY_DC:
333             stdscr.addch(cury, curx, ord(" "))
334             stdscr.move(cury, curx)
335
336
337 curses.wrapper(crossword, crossworddata)