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) : (1, 1, 1, 1), (4, 1) : (1, 1, 1, 1), (4, 2) : (1, 1, 1, 1), (4, 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.pieces[x, y] = random.randrange(4) def is_solved(self): """Checks whether the puzzle is in a solved state.""" # check internal edge pairs for x in range(self.X - 1): for y in range(self.Y - 1): # check horizontal edge pair right of (x, y) l_piece = self.pieces [x, y] l_orient = self.orients[x, y] r_piece = self.pieces [x+1, y] r_orient = self.orients[x+1, 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 l_edge != r_edge: return False # check vertical edge pair below (x, y) u_piece = self.pieces [x, y] u_orient = self.orients[x, y] d_piece = self.pieces [x, y+1] d_orient = self.orients[x, y+1] 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 u_edge != d_edge: return False # check boundary edge pairs for y in range(self.Y): l_piece = self.pieces [self.X-1, y] l_orient = self.orients[self.X-1, y] r_piece = self.pieces [0, y] r_orient = self.orients[0, 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 self.toroidal: if l_edge != r_edge: return False else: if l_edge or r_edge: return False for x in range(self.X): u_piece = self.pieces [x, self.Y-1] u_orient = self.orients[x, self.Y-1] d_piece = self.pieces [x, 0] d_orient = self.orients[x, 0] 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 self.toroidal: if u_edge != d_edge: return False else: if u_edge or d_edge: 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 border_line = self.X * "═" self.screen.addstr(self.Y - 2, 0, border_line) self.display() # start the main loop self.xpos, self.ypos = 0, 0 self.keyloop() PIECE_ORIENT_TO_STRING = \ [" ", "╸╵╺╷", "┙┕┍┑", "━│━│", "┝┯┥┷", "┿┿┿┿"] def display(self): """Display 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() def success(self): """Display the win screen.""" curses.flash() 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() if self.puzzle.is_solved(): self.success() return elif inp in "Qq": return # 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()