#!/usr/bin/python '''Curses client/OO-interface to AcrossLite crossword data I don't have a spec, so this code is based on the Convert::AcrossLite perl module. According to the code, a .puz file's format is: - 44 char unknown - 1 char height - 1 char width - 6 char unknown - (height * width) char solution map - (height * width) char grid map - NULL delimited title, author, copyright - NULL delimited clues (across, down mixed together in regular order) Sample client usage: % acrosslite.py <.puz file> Sample library usage: import * from acrosslite puzzle = acrosslite( open( 'today.puz' ).read() ) puzzle.print_grid() print puzzle.clues['0d'], puzzle.clues['0a'] ''' __version__ = '2.0' __revision__ = '$Id: acrosslite.py,v 2.1 2004/01/30 18:49:16 eric Exp eric $' __author__ = 'Eric Wong ' __copyright__ = '''Copyright (c) 2004 Eric Wong. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Python itself.''' import re, struct class acrosslite( object ): height = 0 # height of the puzzle width = 0 # width solution = '' # solution grid = '' # blank puzzle title = '' # title author = '' # author copyright = '' # copyright clues = {} # dict of the clues, keys are 'position + {a,d}' def __init__( self, data ): ''''data' is the .puz file read into a variable. Note there's no data validation, so if something other than a .puz file is passed in, an exception will be thrown.''' (self.height, self.width) = struct.unpack( "44x B B", data[0:46] ) self.solution = data[52 : 52 + (self.height * self.width)] self.grid = data[52 + (self.height * self.width) : 52 + (2 * self.height * self.width)] # the rest of the data is a NULL delimited list of # the title, author, copyright and clues dat = re.split("\0", data[52 + (2 * self.height * self.width):]) self.title = dat.pop( 0 ) self.author = dat.pop( 0 ) self.copyright = dat.pop( 0 ) j = 0 for i in range( 0, len( self.grid ) ): if ( self.grid[i] == '.' ): continue # across - start in the first column or after a blank if ( i % self.height == 0 or self.grid[i-1] == '.' ): self.clues[str(i)+'a'] = dat[j] # # how to get the across solution. # end = self.solution.find('.', i, # i + (self.width - (i % self.width))) # if ( end == -1 ): end = i + (self.width - ( i % self.width )) # answer = self.solution[i:end] j = j + 1 # down - start on the first row or below a blank if ( i < self.width or self.grid[i-self.width] == '.' ): self.clues[str(i)+'d'] = dat[j] j = j + 1 def print_solution( self ): '''Print out the solution.''' for i in range( 0, self.height ): print self.solution[(i * self.width) : ((i+1) * self.width)] def print_grid( self ): '''Print out the blank puzzle. This could be combined with print_solution() in a general print_foo( what ) function.''' for i in range( 0, self.height ): print self.grid[(i * self.width) : ((i+1) * self.width)] def grid_array( self ): '''Return the grid as an array with each element being a row. This isn't necessary anywhere, but I wanted a place to show off my map/lambda-fu.''' return map( lambda x: self.grid[x*self.width : (x+1)*self.width], range( 0, self.height ) ) def clue( self, position, is_across ): '''Get the clue for the given position. 'is_across' should be True for the across clue (go left until a clue is found), False for the down clue (do up until a clue is found).''' if is_across: while not self.clues.has_key( str(position) + 'a' ): position = position - 1 return self.clues[ str(position) + 'a' ] else: while not self.clues.has_key( str(position) + 'd' ): position = position - self.width return self.clues[ str(position) + 'd' ] if __name__ == '__main__': import curses, re, sys # indicies for the state array BOARD = 0 LOCATION = 1 IS_ACROSS = 2 ERROR = 3 SHOW_SOLUTION = 4 # ah, here's a good resource for crossword terminology: # http://www.biddlecombe.demon.co.uk/yagcc/YAGCC1.html # I'll use the term 'SPACE' to refer to a blank square and # 'BLOCK' to refer to a shaded square. SPACE = '-' BLOCK = ' ' def play_puzzle( stdscr, puzzle ): # this is the actual game state. state[BOARD] is the board the # player is playing and state[LOCATION] is the index of the cursor # on that board. state[IS_ACROSS] is the direction the cursor will # move (across (True) or down (False)) grid = puzzle.grid if ( SPACE != '-' ): grid = re.sub( '-', SPACE, grid ) if ( BLOCK != '.' ): grid = re.sub( '\.', BLOCK, grid ) puzzle.solution = re.sub( '\.', BLOCK, puzzle.solution ) state = [ list( grid ), 0, True, '', False ] puzzle.solution = puzzle.solution.lower() # 4 screens: # o stdscr: the main screen # o across_scr: the across clue # o down_scr: the down clue # o board_scr: the playing board # calculate window geometries (stdscr_y, stdscr_x) = stdscr.getmaxyx() clue_width = stdscr_x - puzzle.width - 7 clue_height = int(puzzle.height / 2) + 1 clue_left = stdscr_x - clue_width - 2 # main screen. truncate (if nec.) and right justify author, # copyright at the bottom of the screen stdscr.box() title = puzzle.title if len( title ) > stdscr_x - 4: title = title[0:stdscr_x - 4] stdscr.addstr( 0, 1, " " + title + " " ) credit = puzzle.author + " / " + puzzle.copyright if len( credit ) > stdscr_x - 4: credit = credit[0:stdscr_x - 4] stdscr.addstr(stdscr_y - 1, stdscr_x - len(credit) - 3, " " + credit + " ") help = "ESC quits, TAB changes direction, '/' toggles solution."[0: stdscr_x - clue_left - 4] stdscr.addstr( 2 * clue_height + 1, clue_left, "[" + help + "]" ) stdscr.refresh() across_scr = curses.newwin( clue_height, clue_width, 1, clue_left ) across_scr.hline( 0, 0, curses.ACS_HLINE, stdscr_x ) down_scr = curses.newwin(clue_height, clue_width, clue_height+1, clue_left) down_scr.hline( 0, 0, curses.ACS_HLINE, stdscr_x ) board_scr = curses.newwin( puzzle.height+2, puzzle.width+2, 1, 2 ) board_scr.box() show_all( board_scr, across_scr, down_scr, puzzle, state ) while 1: c = stdscr.getch() if c == 27: break # escape else : do_command( c, puzzle, state ) show_all( board_scr, across_scr, down_scr, puzzle, state ) def show_all( board_scr, across_scr, down_scr, puzzle, state ): show_clue( across_scr, puzzle, state, True ) show_clue( down_scr, puzzle, state, False ) show_board( board_scr, puzzle, state ) def show_board( scr, puzzle, state ): p = state[SHOW_SOLUTION] and puzzle.solution or ''.join(state[BOARD]) for i in range( 0, puzzle.height ): scr.addstr( i+1, 1, p[i*puzzle.width:(i+1)*puzzle.width] ) (y, x) = divmod( state[LOCATION], puzzle.width ) scr.move( y+1, x+1 ) scr.refresh() def show_clue( scr, puzzle, state, is_across ): scr.addstr( 0, 1, is_across and " across " or " down ", state[IS_ACROSS] == is_across and curses.A_REVERSE | curses.A_BOLD or 0 ) scr.move( 1, 0 ) scr.clrtobot() scr.addstr( 1, 0, puzzle.clue( state[LOCATION], is_across ) ) scr.refresh() def do_command( c, puzzle, state, old_prev_state=0 ): prev_state = old_prev_state or state[LOCATION] # movement if c == curses.KEY_LEFT: state[LOCATION] = state[LOCATION] - 1 elif c == curses.KEY_RIGHT: state[LOCATION] = state[LOCATION] + 1 elif c == curses.KEY_UP: state[LOCATION] = state[LOCATION] - puzzle.width elif c == curses.KEY_DOWN: state[LOCATION] = state[LOCATION] + puzzle.width # letters elif c < 256 and chr( c ).isalpha(): state[BOARD][state[LOCATION]] = chr( c ).lower() do_command( state[IS_ACROSS] and curses.KEY_RIGHT or curses.KEY_DOWN, puzzle, state ) # change direction elif c == ord( "\t" ): state[IS_ACROSS] = not state[IS_ACROSS] # backspace/delete elif c == curses.KEY_BACKSPACE: do_command( state[IS_ACROSS] and curses.KEY_LEFT or curses.KEY_UP, puzzle, state ) state[BOARD][state[LOCATION]] = SPACE elif c == curses.KEY_DC: state[BOARD][state[LOCATION]] = SPACE elif c == ord( " " ): state[BOARD][state[LOCATION]] = SPACE do_command( state[IS_ACROSS] and curses.KEY_RIGHT or curses.KEY_DOWN, puzzle, state ) # show solution elif c == ord( '/' ): state[SHOW_SOLUTION] = not state[SHOW_SOLUTION] # if the cursor falls on an invalid square, keep on moving in the # same direction if ( state[LOCATION] < len( state[BOARD] ) and state[BOARD][state[LOCATION]] == BLOCK ): do_command( c, puzzle, state, prev_state ) # if the movement would take us off of the board, go to the # previous location if (state[LOCATION] < 0 or state[LOCATION] >= len(state[BOARD])): state[LOCATION] = prev_state if ( len( sys.argv ) < 2 ): print "Usage:", re.split( '/', sys.argv[0] )[-1], "<.puz file>" else: puzzle = acrosslite( open( sys.argv[1] ).read() ) curses.wrapper( play_puzzle, puzzle )