import curses import random class ooPuzzle: """Encapsulates a oo puzzle state. No rendering information is stored or interpreted here. Attributes. X,Y : horizontal and vertical size of the puzzle pieces : dictionary mapping (x,y) to range(6) orients : dictionary mapping (x,y) to range(4) """ def __init__(self, X, Y, game_id=None, inverted=False, toroidal=False): """Create a new ooPuzzle instance. Arguments. X,Y : horizontal and vertical size of the puzzle game_id : passed to ooPuzzle.set_pieces_from_game_id inverted : bool for swapping to "dark mode" toroidal : bool for looping around the end of the puzzle NotImplemented: inverted, toroidal """ self.X, self.Y = X, Y self.inverted = inverted self.toroidal = toroidal self.pieces = {} self.orients = {} self.set_pieces_from_game_id(game_id) EDGES_TO_PIECE_ORIENT = { (0, 0, 0, 0) : (0, 0), (1, 0, 0, 0) : (1, 0), (0, 1, 0, 0) : (1, 1), (0, 0, 1, 0) : (1, 2), (0, 0, 0, 1) : (1, 3), (1, 1, 0, 0) : (2, 0), (0, 1, 1, 0) : (2, 1), (0, 0, 1, 1) : (2, 2), (1, 0, 0, 1) : (2, 3), (1, 0, 1, 0) : (3, 0), (0, 1, 0, 1) : (3, 1), (0, 1, 1, 1) : (4, 0), (1, 0, 1, 1) : (4, 1), (1, 1, 0, 1) : (4, 2), (1, 1, 1, 0) : (4, 3), (1, 1, 1, 1) : (5, 0) } PIECE_ORIENT_TO_EDGES = { (0, 0) : (0, 0, 0, 0), (0, 1) : (0, 0, 0, 0), (0, 2) : (0, 0, 0, 0), (0, 3) : (0, 0, 0, 0), (1, 0) : (1, 0, 0, 0), (1, 1) : (0, 1, 0, 0), (1, 2) : (0, 0, 1, 0), (1, 3) : (0, 0, 0, 1), (2, 0) : (1, 1, 0, 0), (2, 1) : (0, 1, 1, 0), (2, 2) : (0, 0, 1, 1), (2, 3) : (1, 0, 0, 1), (3, 0) : (1, 0, 1, 0), (3, 1) : (0, 1, 0, 1), (3, 2) : (1, 0, 1, 0), (3, 3) : (0, 1, 0, 1), (4, 0) : (0, 1, 1, 1), (4, 1) : (1, 0, 1, 1), (4, 2) : (1, 1, 0, 1), (4, 3) : (1, 1, 1, 0), (5, 0) : (1, 1, 1, 1), (5, 1) : (1, 1, 1, 1), (5, 2) : (1, 1, 1, 1), (5, 3) : (1, 1, 1, 1) } def set_pieces_from_edges(self, horiz_edges, vert_edges): """Convert edge dictionaries into a puzzle state. Adjacent edges in a solution must be either both filled or both unfilled, and so a bit of data is assigned to each pair. vert_edges is the dictionary for vertical pairs key: (x, row) x: the usual x-coordinate of the piece row: the y-coordinate or y+1, depending on whether you want the pair above or below the piece : row == 0 and row == self.Y are border edges so it's good to think of row as 1-indexed horiz_edges is the dictionary for horizontal pairs key: (col, y) col: the x-coord or x+1, same as row but left or right y: the usual y-coord """ for x in range(self.X): for y in range(self.Y): left = horiz_edges[x , y] right = horiz_edges[x+1, y] up = vert_edges[x, y ] down = vert_edges[x, y+1] piece, orient = \ self.EDGES_TO_PIECE_ORIENT[left, up, right, down] self.pieces [x, y] = piece self.orients[x, y] = orient #TODO: properly account for the borders if toroidal def set_pieces_from_game_id(self, game_id=None): """Convert game_id value into a solution puzzle state. game_id is an integer if toroidal: in range(2**( 2*X*Y )) if not toroidal: in range(2**( (X-1)*Y + X*(Y-1) )) if None, then a random game_id is generated """ # compute: # n_horiz, the number of horizontal edge pairs # n_vert, the number of vertical edge pairs shift = -int(not self.toroidal) n_col = self.X + shift n_row = self.Y + shift n_horiz = n_col * self.Y n_vert = self.X * n_row # generate and record game_id if game_id == None: game_id = random.randrange(0, 2**(n_horiz + n_vert)) self.game_id = game_id # prepare to record edges for self.set_pieces_from_edges vert_edges = {} horiz_edges = {} # set the border edges # if toroidal, these will be overwritten for x in range(self.X): vert_edges[x, 0] = 0 vert_edges[x, self.Y] = 0 for y in range(self.Y): horiz_edges[ 0, y] = 0 horiz_edges[self.X, y] = 0 # set the edges determined by game_id for i in range(n_vert): row, x = divmod(i, self.X) game_id, bit = divmod(game_id, 2) vert_edges[x, row+1] = bit for i in range(n_horiz): col, y = divmod(i, self.Y) game_id, bit = divmod(game_id, 2) horiz_edges[col+1, y] = bit # turn edges into pieces self.set_pieces_from_edges(horiz_edges, vert_edges) def rotate_cw(self, x, y, turns=1): """Rotates the piece at (x, y) by clockwise turns.""" o = self.orients[x, y] o = (o + turns) % 4 self.orients[x, y] = o def random_orients(self): """Randomly rotate the current pieces.""" for x in range(self.X): for y in range(self.Y): self.orients[x, y] = random.randrange(4) def check_right(self, x0, y): """Check the horizontal edge pair right of (x0, y).""" x1 = (x0 + 1) % self.X l_piece = self.pieces [x0, y] l_orient = self.orients[x0, y] r_piece = self.pieces [x1, y] r_orient = self.orients[x1, y] l_edge = self.PIECE_ORIENT_TO_EDGES[l_piece, l_orient][2] r_edge = self.PIECE_ORIENT_TO_EDGES[r_piece, r_orient][0] if not self.toroidal and x1 == 0: return not l_edge and not r_edge return l_edge == r_edge def check_down(self, x, y0): """Check the vertical edge pair below (x, y0).""" y1 = (y0 + 1) % self.Y u_piece = self.pieces [x, y0] u_orient = self.orients[x, y0] d_piece = self.pieces [x, y1] d_orient = self.orients[x, y1] u_edge = self.PIECE_ORIENT_TO_EDGES[u_piece, u_orient][3] d_edge = self.PIECE_ORIENT_TO_EDGES[d_piece, d_orient][1] if not self.toroidal and y1 == 0: return not u_edge and not d_edge return u_edge == d_edge def is_solved(self): """Check whether the puzzle is in a solved state.""" for x in range(self.X): for y in range(self.Y): if not self.check_right(x, y): return False if not self.check_down(x, y): return False return True class ooPlay: """Encapsulates an oo game instance. Renders and interacts with an ooPuzzle instance. """ def __init__(self, screen): """Create a new ooPlay instance. Arguments. scr : curses screen object used for display """ self.screen = screen self.Y, self.X = self.screen.getmaxyx() self.puzzle = ooPuzzle(self.X, self.Y-2) self.puzzle.random_orients() self.screen.clear() # draw the help area and board state self.help_ind = 0 border_line = self.X * "═" self.screen.addstr(self.Y - 2, 0, border_line) self.write() self.display() # start the main loop self.xpos, self.ypos = 0, 0 self.keyloop() PIECE_ORIENT_TO_STRING = \ [" ", "╸╵╺╷", "┙┕┍┑", "━│━│", "┝┯┥┷", "┿┿┿┿"] def display_pos(self, x, y): """Update one position on the board, refresh screen.""" piece = self.puzzle.pieces [x, y] orient = self.puzzle.orients[x, y] string = self.PIECE_ORIENT_TO_STRING[piece][orient] self.screen.addstr(y, x, string) self.screen.refresh() def display(self): """Update the state of the board, refresh screen.""" for x in range(self.puzzle.X): for y in range(self.puzzle.Y): piece = self.puzzle.pieces [x, y] orient = self.puzzle.orients[x, y] string = self.PIECE_ORIENT_TO_STRING[piece][orient] self.screen.addstr(y, x, string) self.screen.refresh() pause_length = 80 def write(self, string=None, pause=None): """Write string to the bottom line. if string is narrower than line: centers string within the line if string is wider than line: scrolls through string if a string is not given, then "h" is used if pause is True, there will be a pause for reading if pause is not given but a string is, there will be a pause if neither a pause nor a string is given, there will not be a pause ooPlay.pause_length is the number of milliseconds per character to pause """ if string == None: string = "H" if pause == None: pause = False if pause == None: pause = True width = self.X - 1 # writing a string that fits in the width if len(string) <= width: centered_string = string.center(width, " ") self.screen.addstr(self.Y - 1, 0, centered_string) self.screen.refresh() if pause: curses.napms(2 * self.pause_length * len(string)) curses.ungetch(0) # clear input return # scrolling through a wider string strings = [string[i:i + width] for i in range(len(string) - width + 1)] self.screen.addstr(self.Y - 1, 0, strings[0]) self.screen.refresh() curses.napms(self.pause_length * width) for s in strings: self.screen.addstr(self.Y - 1, 0, s) self.screen.refresh() curses.napms(self.pause_length) if pause: curses.napms(self.pause_length * width) curses.ungetch(0) # clear input def write_help(self): """Write one of the help messages.""" if self.help_ind == 0: self.write("Help on controls.") self.write("arrow keys: move cursor") self.write("space bar or return: rotates piece") self.write("q: quit game") self.write("n: new game") self.write("r: randomize rotations") self.write("t: toggle toroidal mode") self.write("i: toggle inverted mode") self.write("The next help is game explanation.") self.write() if self.help_ind == 1: self.write("Help on game.") self.write("If game is not inverted," + " the object is to have every line connect to another.") self.write("If game is inverted," + " the object is to have no two lines connected.") self.write("If game is not toroidal," + " the borders cannot have lines extending outwards.") self.write("If game is toroidal," + " the borders loop back and may connect to the opposite side.") self.write("The next help is on controls.") self.write() self.help_ind += 1 self.help_ind %= 2 def success(self): """Write and respond to the win screen.""" self.write("You won!") self.write("r n q", pause=False) while True: inp = self.screen.getch() if inp in map(ord, 'QqNnRr'): break return chr(inp) def keyloop(self): """Wait for and parse keypress.""" while True: self.screen.move(self.ypos, self.xpos) inp = self.screen.getch() # parse character input if 0 < inp < 256: inp = chr(inp) if inp in " \n": self.puzzle.rotate_cw(self.xpos, self.ypos) self.display_pos(self.xpos, self.ypos) if self.puzzle.is_solved(): inp = self.success() if inp in "Qq": self.write("Quit") return elif inp in "Rr": self.write("Randomize") self.puzzle.random_orients() self.display() self.write() elif inp in "Nn": self.write("New Game") self.puzzle.set_pieces_from_game_id() self.puzzle.random_orients() self.display() self.write() elif inp in "H": self.write_help() elif inp in "Ii": self.write("Inverted mode is not implemented.") self.write() elif inp in "Tt": self.write("Toroidal mode is not implemented.") self.write() elif inp in "k" and self.ypos > 0: self.ypos -= 1 elif inp in "j" and self.ypos < self.puzzle.Y - 1: self.ypos += 1 elif inp in "h" and self.xpos > 0: self.xpos -= 1 elif inp in "l" and self.xpos < self.puzzle.X - 1: self.xpos += 1 # parse arrow key input elif inp == curses.KEY_UP and self.ypos > 0: self.ypos -= 1 elif inp == curses.KEY_DOWN and self.ypos < self.puzzle.Y - 1: self.ypos += 1 elif inp == curses.KEY_LEFT and self.xpos > 0: self.xpos -= 1 elif inp == curses.KEY_RIGHT and self.xpos < self.puzzle.X - 1: self.xpos += 1 def main(): curses.wrapper(ooPlay) if __name__ == "__main__": main()