3 Play a crossword puzzle in a curses grid. Takes a command line argument of the
6 # curses_crossword.py - play crosswords in a terminal
7 # Copyright (C) 2009 Brett Parker <iDunno@sommitrealweird.co.uk>
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.
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.
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.
31 locale.setlocale(locale.LC_ALL, '')
32 code = locale.getpreferredencoding()
34 superscript_numbers = {}
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),
49 except UnicodeError, e:
50 for number in range(0, 10):
51 superscript_numbers[str(number)] = str(number)
56 (options, args) = getopt.getopt(sys.argv[1:], "f:", "file=")
57 except getopt.GetoptError, err:
61 for option in options:
62 if option[0] == "-f" or option[0] == "--file":
65 if not filename and len(args) > 0:
69 sys.stderr.write("No crossword file specified, exiting.\n")
72 if os.path.exists(filename) and os.path.isfile(filename):
73 crossworddata = codecs.open(filename, "r", "utf-8").read()
75 sys.stderr.write("Couldn't open file %s\n" %(filename))
78 def parsecrossword(crossworddata):
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
87 crossword = {"grid": [],
90 "grid_questions_start": [],
91 "grid_questions_end": []}
93 for line in crossworddata.split("\n"):
94 line = line.strip("\n")
99 elif line == "ACROSS":
110 crossword["grid"].append(line)
114 question_number = int(parts[0].split(",")[0])
115 clue = " ".join(parts[1:])
116 crossword["across"][int(question_number)] = \
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"])
127 current_clue_number = 1
129 for row in range(0, num_rows):
130 crossword["grid_questions_start"].append([])
132 for row in range(0, num_rows):
133 for col in range(0, num_cols):
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":
141 if col == 0 and crossword["grid"][row][col] != "x" \
142 and crossword["grid"][row][col + 1] != "x":
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":
149 if row == 0 and crossword["grid"][row][col] != "x" \
150 and crossword["grid"][row + 1][col] != "x":
153 crossword["grid_questions_start"][row] \
154 .append(current_clue_number)
155 current_clue_number += 1
157 crossword["grid_questions_start"][row].append(0)
161 def crossword(stdscr, crossworddata):
162 crossword = parsecrossword(crossworddata)
164 grid_length = len(crossword["grid"][0])
166 stdscr.addch(cury, curx, curses.ACS_ULCORNER)
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)
175 stdscr.addch(cury, curx, curses.ACS_URCORNER)
178 for line in crossword["grid"]:
182 stdscr.addch(cury, curx, curses.ACS_VLINE)
184 stdscr.addch(cury+1, curx, curses.ACS_PLUS)
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)
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, \
198 [superscript_numbers[x].decode(code) \
200 crossword["grid_questions_start"] \
201 [curgridy][curgridx])]\
206 stdscr.addch(cury, curx, curses.ACS_VLINE)
207 stdscr.addch(cury + 1, curx, curses.ACS_RTEE)
212 stdscr.addch(cury, curx, curses.ACS_LLCORNER)
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)
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
232 (padbry, padbrx) = stdscr.getmaxyx()
233 if padbry > cluespad.getmaxyx()[0] + padtly:
234 padbry = cluespad.getmaxyx()[0] + padtly
236 cluespad.addstr(cury, curx, "Across")
238 for cluenumber in crossword["across"].keys():
239 cluespad.addstr(cury, curx, "%3s: %s" \
240 %(str(cluenumber), crossword["across"][cluenumber]))
244 cluespad.addstr(cury, curx, "Down")
246 for cluenumber in crossword["down"].keys():
247 cluespad.addstr(cury, curx, "%3s: %s" \
248 %(str(cluenumber), crossword["down"][cluenumber]))
256 while crossword["grid"][gridy][gridx] == "x":
260 stdscr.move(cury, curx)
263 cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
264 cluespad.overlay(stdscr, cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
268 inch = stdscr.getch()
269 if inch == curses.ascii.ESC:
271 if inch == curses.KEY_NPAGE:
272 if cluescury < cluespad.getmaxyx()[0] - (padbry - padtly):
274 cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
276 if inch == curses.KEY_PPAGE:
279 cluespad.refresh(cluescury, 0, padtly, padtlx, padbry - 1, padbrx - 1)
281 if inch == curses.KEY_RIGHT:
282 if gridx < (len(crossword["grid"][0]) - 1):
285 while gridx < (len(crossword["grid"][0]) -1) \
286 and crossword["grid"][gridy][gridx] == "x":
289 while crossword["grid"][gridy][gridx] == "x":
292 stdscr.move(cury, curx)
293 if inch == curses.KEY_LEFT:
298 and crossword["grid"][gridy][gridx] == "x":
301 while crossword["grid"][gridy][gridx] == "x":
304 stdscr.move(cury, curx)
305 if inch == curses.KEY_UP:
310 and crossword["grid"][gridy][gridx] == "x":
313 while crossword["grid"][gridy][gridx] == "x":
316 stdscr.move(cury, curx)
317 if inch == curses.KEY_DOWN:
318 if gridy < (len(crossword["grid"]) - 1):
321 while gridy < (len(crossword["grid"]) - 1) \
322 and crossword["grid"][gridy][gridx] == "x":
325 while crossword["grid"][gridy][gridx] == "x":
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)
337 curses.wrapper(crossword, crossworddata)