class Board: """The game board. This class maintains a Grid for the pieces on the board and a Grid for the frozen blocks on the board.""" def __init__(self, parent_node, ncolumns, nrows, pos, scale): self.parent_node = parent_node self.piece_grid = None # set_size will set these self.frozen_grid = None self.set_size(ncolumns, nrows, pos, scale) def set_size(self, ncolumns, nrows, pos, scale): self.ncolumns = ncolumns self.nrows = nrows self.pos = pos self.scale = float(scale) self.size = Point2D(ncolumns, nrows)*self.scale self.block_size = Point2D(self.scale, self.scale) if self.piece_grid: row_offset = nrows - self.piece_grid.nrows self.piece_grid.grow(ncolumns, nrows) self.frozen_grid.grow(ncolumns, nrows) pieces = set(value for cr, value in self.piece_grid.get_blocks()) for piece in pieces: c, r = piece.cr piece.cr = (c, r + row_offset) piece.update_blocks() piece.update_manip() for cr, value in self.frozen_grid.get_blocks(): value.pos = self.get_nw_point(cr) value.size = self.block_size else: self.piece_grid = Grid(ncolumns, nrows) # contains Piece references self.frozen_grid = Grid(ncolumns, nrows) # contains Node references def destroy(self): for cr, value in self.frozen_grid.get_blocks(): value.unlink() def get_nw_point(self, (c, r)): return self.pos + Point2D(c, r)*self.scale
class Game: """The main game controller. This class holds the Board and the list of Pieces, and causes the Pieces to fall with each clock tick. When the fall of a Piece is stopped by blocks below, the Piece "freezes": the blocks of the Piece are copied onto the board and the Piece object is destroyed. Pieces appear in a "starting zone" at the top where they cannot be manipulated; if a piece freezes in the starting zone, the game ends.""" def __init__(self, parent_node, app, scale, section_pattern, tick_interval, ticks_per_drop, pieces_per_drop, shapes): self.width, self.height = parent_node.size.x, parent_node.size.y self.app = app self.node = create_node(parent_node, 'div') self.section_node = create_node(self.node, 'div', sensitive=False) # Create a dummy game board; it will be resized in self.set_scale(). self.board = Board(self.node, 1, 1, Point2D(0, 0), scale) self.board_rect = create_node(self.node, 'rect', color='a0a0a0', strokewidth=4, sensitive=False) self.dissolving = None self.starting_zone = None self.starting_zone_node = create_node(parent_node, 'rect', opacity=0, fillcolor='ff0000', fillopacity=0.2) self.can_add_pieces = True # For debouncing taps on the starting zone. # Normally, the starting zone can be touched to request more pieces; # however, this feature is disabled because of an exhaust hose in the # c-base multitouch table that causes extraneous touches along the top. # set_handler(self.starting_zone_node, avg.CURSORDOWN, self.handle_down) self.pieces = [] self.interval_id = None self.ticks_to_next_drop = 1 self.section_nodes = None self.set_difficulty(scale, section_pattern, tick_interval, ticks_per_drop, pieces_per_drop, shapes) def set_difficulty(self, scale, section_pattern, tick_interval, ticks_per_drop, pieces_per_drop, shapes): self.set_scale(scale) self.set_section_pattern(section_pattern) self.tick_interval = tick_interval self.ticks_per_drop = ticks_per_drop self.pieces_per_drop = pieces_per_drop self.shapes = shapes def set_scale(self, scale): # Resize the game board. ncolumns, nrows = int(self.width/scale), int(self.height/scale) board_width = ncolumns*scale self.board.set_size(ncolumns, nrows, Point2D((self.width - board_width)/2, self.height % scale), scale) self.board_rect.pos = self.board.pos self.board_rect.size = self.board.size if self.dissolving: self.dissolving.grow(self.board.ncolumns, self.board.nrows) else: self.dissolving = Grid(self.board.ncolumns, self.board.nrows) # Set up the starting zone. self.starting_zone = Grid(self.board.ncolumns, STARTING_ZONE_NROWS, ['X'*self.board.ncolumns]*STARTING_ZONE_NROWS) self.starting_zone_node.pos = self.board.pos self.starting_zone_node.size = \ Point2D(self.board.ncolumns, STARTING_ZONE_NROWS)*scale def handle_down(self, event): if self.can_add_pieces: self.starting_zone_node.fillopacity = 0.4 self.add_piece(self.board.get_cr(event.pos)[0]) self.can_add_pieces = False # Ignore extraneous double-taps. set_timeout(300, self.reset_starting_zone) def reset_starting_zone(self): self.starting_zone_node.fillopacity = 0.2 self.can_add_pieces = True def set_section_pattern(self, section_pattern): if self.section_nodes: for node in self.section_nodes: node.unlink() self.section_nodes = [] self.sections = [] s = 0 for r in reversed(range(STARTING_ZONE_NROWS, self.board.nrows)): nsections = section_pattern[s] s = (s + 1) % len(section_pattern) # If the resolution is too low, 4 sections are no fun. if self.width < 1200 and nsections > 3: nsections = 3 section_min = 0 for i in range(nsections): section_max = int(self.board.ncolumns*float(i + 1)/nsections) section_ncolumns = section_max - section_min section = Grid(self.board.ncolumns, self.board.nrows) for j in range(section_min, section_max): section.put((j, r), 'X') self.sections.append(section) self.section_nodes.append(create_node(self.section_node, 'rect', pos=self.board.get_nw_point((section_min, r)), size=Point2D(section_ncolumns, 1)*self.board.scale, opacity=0.2, color='ffffff', sensitive=False)) section_min = section_max def destroy(self): self.pause() for piece in self.pieces: piece.destroy() for node in self.section_nodes: node.unlink() self.board.destroy() self.node.unlink() self.board_rect.unlink() self.section_node.unlink() self.starting_zone_node.unlink() def run(self): self.pause() self.interval_id = set_interval(self.tick_interval, self.tick) def pause(self): if self.interval_id: clear_interval(self.interval_id) self.interval_id = None def tick(self): """Advance the game by one step.""" self.fall_and_freeze() self.delete_dissolving() self.mark_dissolving() if (not list(self.dissolving.get_blocks()) and self.board.frozen_grid.overlaps_any(self.starting_zone, (0, 0))): self.app.end_game() return self.ticks_to_next_drop -= 1 if (self.ticks_to_next_drop <= 0 or not list(self.board.piece_grid.get_blocks())): self.ticks_to_next_drop = self.ticks_per_drop for p in range(self.pieces_per_drop): if not self.add_piece(): print 'end_game: add_piece failed' self.app.end_game() return self.app.tick() def fall_and_freeze(self): """Classify all pieces as suspended (by a player grab), stopped (by frozen blocks), or falling; move falling pieces down by one step and freeze all stopped pieces.""" # 1. Determine what supports each piece. Pieces can be supported by # other pieces, by frozen blocks, or by the bottom edge of the board. supported_pieces = dict((piece, set()) for piece in self.pieces) stopped = set() for piece in self.pieces: for cr, value in piece.get_blocks(): cr_below = add_cr(cr, (0, 1)) if not self.board.piece_grid.in_bounds(cr_below): stopped.add(piece) # at bottom edge else: piece_below = self.board.piece_grid.get(cr_below) frozen_below = self.board.frozen_grid.get(cr_below) if piece_below: # supported by a Piece supported_pieces[piece_below].add(piece) elif frozen_below: # supported by a frozen block stopped.add(piece) # 2. Stop pieces supported by the board, and pieces they support. stopped = transitive_closure(stopped, supported_pieces) # 3. Suspend pieces that are grabbed, and pieces they support. suspended = set(piece for piece in self.pieces if piece.is_grabbed()) suspended = transitive_closure(suspended, supported_pieces) # 4. Freeze all stopped pieces. for piece in stopped: if piece not in suspended: self.board.freeze_piece(piece) self.pieces.remove(piece) # 5. Move down all falling pieces. for piece in self.pieces: if piece not in suspended: piece.move_to(add_cr(piece.cr, (0, 1))) def delete_dissolving(self): """Delete any frozen blocks that are dissolving.""" self.board.delete_frozen(self.dissolving) self.dissolving.clear() def mark_dissolving(self): """Mark dissolving blocks (to be deleted on the next tick).""" for section in self.sections: if self.board.frozen_grid.overlaps_all(section, (0, 0)): self.dissolving.put_all(section, (0, 0)) self.board.highlight_dissolving(self.dissolving) def add_piece(self, c=None): """Place a new Piece with a randomly selected shape at a random rotation and position somewhere along the top of the game board.""" columns = (c is None) and range(self.board.ncolumns) or [c] # Permute the shapes, rotations, and possible positions, so that we # get a random result, but eventually try them all. for shape in shuffled(self.shapes): for rotation in shuffled(range(4)): for c in shuffled(columns): grid = shape.get_rotated(rotation) min_r = min(r for (c, r), value in grid.get_blocks()) cr = (c - grid.ncolumns/2, -min_r) if self.board.can_put_piece(None, cr, grid): self.pieces.append( Piece(self.node, self.board, cr, grid)) return True return False # couldn't find anywhere to put a new piece