def _draw_tiles(self, grid: Grid): if not self._grid_rect: self._setup_grid(grid) utils.draw_rounded_rect(self.window, theme.GRID_COLOR, self._grid_rect, border_radius=8) x, y = self._grid_rect.topleft for i in range(self._grid_rows): rect = None x = self._grid_rect.left for j in range(self._grid_cols): color = theme.CELL_COLOR tile = grid.get_cell(Position(j, i)) text = None rect = Rect(x + self._grid_spacing, y + self._grid_spacing, self._tile_side, self._tile_side) if tile: color = theme.TILE_COLOR_BASE text = theme.H2_FONT.render(str(tile.value), True, theme.COLOR_DARK) utils.draw_rounded_rect(self.window, color, rect, 4) if text: text_x = rect.left + (rect.width - text.get_width()) // 2 text_y = rect.top + (rect.height - text.get_height()) // 2 self.window.blit(text, (text_x, text_y)) x = rect.right y = rect.bottom pygame.display.update()
class Logic: def __init__(self, params: Namespace): self._grid = Grid(width=params.width, height=params.height) self._start_tiles = params.start_tiles self._merged_total = 0 self._params = params @property def grid(self) -> Grid: return self._grid @property def start_tiles(self) -> int: return self._start_tiles @property def merged_total(self) -> int: return self._merged_total def random_tile(self) -> Optional[Tile]: """Produce a new ``Tile`` with random value and position. If there are no empty cells in the grid returns ``None``. :return: Tile object or None if no available cells found. """ if self._grid.has_available_cells(): value = 4 if random.random() < 0.1 else 2 return Tile(self._grid.get_empty_cell(), value) def save_state(self) -> LogicState: return LogicState(grid=self._grid, params=self._params, merged_total=self.merged_total) def load_state(self, state: LogicState): class InvalidStateException(Exception): pass try: self._start_tiles = state.params.start_tiles self._merged_total = state.merged_total self._params = state.params self._grid = state.grid except AttributeError as e: raise InvalidStateException(f"Logic state is corrupted! {e}") def setup(self) -> bool: """Clears the grid and inserts ``start_tiles`` number of tiles. :return: bool False if couldn't insert the number of tiles given, True otherwise """ self._merged_total = 0 self._grid.empty() for _ in range(self._start_tiles): if not self.insert_random_tile(): return False return True def insert_random_tile(self): # todo: add a docstring tile = self.random_tile() if tile: self._grid.insert_tile(tile) return True return False def moves_available(self): """Pretty expensive check""" for direction in Direction: for tile in self._grid.tiles: if self._farthest_position(tile, direction) != tile.position: return True if self._is_merge_available(tile, direction): return True return False def move(self, direction: Direction): """Moves all the tiles in the given direction and merges them if needed. :param direction: :return: None """ # Remove tiles metadata from the previous move self._clean_tiles_metadata() for position in self._traversals(direction): tile = self._grid.get_cell(position) if not tile: continue # Move the tile to the farthest empty position before the first obstacle self._move_tile(tile, self._farthest_position(tile, direction)) # If the nearest obstacle in that direction is a tile # check for merge and merge if possible if self._is_merge_available(tile, direction): closest = self._next_merge(tile, direction) merged = self._merge_tiles(tile, closest) self._merged_total += merged.value self.insert_random_tile() def _next_merge(self, tile: Tile, direction: Direction) -> Optional[Tile]: # TODO: add a docstring position = _next_position(tile.position, direction) if self._grid.is_within(position) and self._grid.is_cell_filled( position): candidate = self._grid.get_cell(position) if candidate.value == tile.value and not candidate.merged_from: return candidate def _is_merge_available(self, tile: Tile, direction: Direction) -> bool: return self._next_merge(tile, direction) is not None def _farthest_position(self, tile: Tile, direction: Direction) -> Position: """Returns ``Position`` object of the farthest position before the first obstacle. :param direction: direction towards the destination position :return: Position object """ current = tile.position position = _next_position(current, direction) while self._grid.is_within(position) and self._grid.is_cell_empty( position): current = position position = _next_position(current, direction) return current def _move_tile(self, tile: Tile, position: Position): self._grid.remove_tile(tile) tile.save_position() tile.position = position self._grid.insert_tile(tile) def _clean_tiles_metadata(self): for tile in self._grid.tiles: tile.merged_from = None def _merge_tiles(self, a: Tile, b: Tile): """Replaces tiles in the grid with a new ``Tile`` object at `b`'s position. The merged tile's value is a sum of `a` and `b` values. :param a: first Tile :param b: second Tile :return: new Tile object """ merged = Tile(b.position, a.value + b.value) merged.merged_from = [a, b] self._grid.remove_tile(a) self._grid.insert_tile(merged) return merged def _traversals(self, direction: Direction) -> Iterator[Position]: """Get positions to travers in the given direction. Positions are sorted in the bottommost or rightmost order for vertical or horizontal directions accordingly. :param direction: the direction to travers :return: iterator that yields Position objects representing nodes to travers in the given direction. """ xs = list(range(self._grid.width)) ys = list(range(self._grid.height)) if direction.value.x == 1: xs.reverse() if direction.value.y == 1: ys.reverse() for x, y in product(xs, ys): yield Position(x, y)