import copy import random import pygame from typing import List, Tuple from pygame import mixer from tetris.util import ConfigurationManager from typing import TYPE_CHECKING if TYPE_CHECKING: from tetris.game import Game """ TODO description """ class Entity: def __init__(self, points: Tuple, color: str, border_color: str = None): self.points = points self.color = color self.border_color = border_color self.elapsed_time = 0 def update(self, elapsed_time: int) -> None: self.elapsed_time += elapsed_time def draw(self, surface: pygame.Surface) -> None: tile_size = ConfigurationManager.get("engine", "tile-size") for square in self.points: pygame.draw.polygon(surface, pygame.Color(self.color), square, 0) if self.border_color: pygame.draw.polygon(surface, pygame.Color(self.border_color), square, max(tile_size // 6, 1)) def collide(self, entity: "Entity") -> bool: # TODO figure out how to do type hint for entity param of type Entity for square_one in self.points: for square_two in entity.points: for i in range(4): # 4 vertices if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]: return True return False def _collide(self, points: List) -> bool: for square_one in self.points: for square_two in points: for i in range(4): # 4 vertices if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]: return True return False """ TODO description """ class Well(Entity): WIDTH = 10 # standard tetris well width, should not be changed HEIGHT = 20 # standard tetris well height, should not be changed def __init__(self, position: Tuple, color: str, border_color: str): super().__init__(self._get_points(position), color, border_color) def _get_points(self, position: Tuple) -> List: tile_size = ConfigurationManager.get("engine", "tile-size") shape = [] for i in range(self.WIDTH + 2): for j in range(self.HEIGHT + 2): if i == 0 or i == self.WIDTH + 1: shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1))) elif j == 0 or j == self.HEIGHT + 1: shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1))) points = [] for square in shape: sub_points = [] for vertex in square: point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]] sub_points.append(point) points.append(sub_points) return points ''' For information on the Tetris piece Tetromino go here: https://tetris.fandom.com/wiki/Tetromino ''' class Piece(Entity): def __init__(self, shape: Tuple, position: Tuple, color: str, inner_border_color: str, border_color: str): super().__init__(self._get_points(shape, position), color, border_color) self.inner_border_color = inner_border_color self.center = self._get_center(shape, position) self.piece_set_sound = mixer.Channel(2) self.previous_points = None self.previous_center = None # Gravity self.gravity_time = ConfigurationManager.get("engine", "piece-gravity-time") self.current_gravity_time = 0 self.applying_gravity = True # Set self.set_time = ConfigurationManager.get("engine", "piece-set-time") self.current_set_time = 0 self.applying_set = False def update(self, elapsed_time: int, game: "Game") -> None: super().update(elapsed_time) if self.applying_gravity: self.applying_gravity = self._apply_gravity(elapsed_time, game.well, game.stack) self.applying_set = not self.applying_gravity if self.applying_set: self.applying_set = self._apply_set(elapsed_time, game) self.applying_gravity = not self.applying_set def draw(self, surface: pygame.Surface) -> None: super().draw(surface) tile_size = ConfigurationManager.get("engine", "tile-size") for square in self.points: if self.inner_border_color: vertex_one = (square[0][0] + (tile_size // 10), square[0][1] + (tile_size // 10)) vertex_two = (square[1][0] - (tile_size // 10), square[1][1] + (tile_size // 10)) vertex_three = (square[2][0] - (tile_size // 10), square[2][1] - (tile_size // 10)) vertex_four = (square[3][0] + (tile_size // 10), square[3][1] - (tile_size // 10)) new_square = (vertex_one, vertex_two, vertex_three, vertex_four) pygame.draw.polygon(surface, pygame.Color(self.inner_border_color), new_square, max(tile_size // 6, 1)) def move(self, vector: Tuple) -> None: self.previous_points = copy.deepcopy(self.points) self.previous_center = copy.deepcopy(self.center) self.center[0] += vector[0] self.center[1] += vector[1] for square in self.points: for vertex in square: vertex[0] += vector[0] vertex[1] += vector[1] ''' For more information on a rotation of a piece go here: https://gamedev.stackexchange.com/questions/17974/how-to-rotate-blocks-in-tetris ''' def rotate(self) -> None: self.previous_points = copy.deepcopy(self.points) self.previous_center = copy.deepcopy(self.center) new_points = [] for square in self.points: for vertex in square: h = vertex[0] - self.center[0] k = vertex[1] - self.center[1] vertex[0] = (k * -1) + self.center[0] vertex[1] = h + self.center[1] new_points.append([square[-1]] + square[0:-1]) self.points = new_points def revert(self) -> None: if self.previous_points and self.previous_center: self.points = self.previous_points self.center = self.previous_center def _mimic_move(self, vector: Tuple) -> List: mimic_points = copy.deepcopy(self.points) for square in mimic_points: for vertex in square: vertex[0] += vector[0] vertex[1] += vector[1] return mimic_points def _play_piece_set_sound(self) -> None: piece_set_sound_file = ConfigurationManager.get("sound", "piece-set") self.piece_set_sound.play(mixer.Sound(piece_set_sound_file)) def _get_points(self, shape: Tuple, position: Tuple) -> List: tile_size = ConfigurationManager.get("engine", "tile-size") points = [] for square in shape[:-1]: sub_points = [] for vertex in square: point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]] sub_points.append(point) points.append(sub_points) return points def _get_center(self, shape: Tuple, position: Tuple) -> List: tile_size = ConfigurationManager.get("engine", "tile-size") center = shape[-1] # cast to int and avoid exception from pygame (center can be a floating point) return [int(center[0] * tile_size + position[0]), int(center[1] * tile_size + position[1])] def _apply_gravity(self, elapsed_time: int, well: Well, stack: "Stack") -> bool: tile_size = ConfigurationManager.get("engine", "tile-size") self.current_gravity_time += elapsed_time if self.current_gravity_time >= self.gravity_time: self.current_gravity_time = 0 if not self._entity_is_below(well) and not self._entity_is_below(stack): self.move((0, tile_size)) else: return False return True def _apply_set(self, elapsed_time: int, game: "Game") -> bool: self.current_set_time += elapsed_time if self.current_set_time >= self.set_time: self.current_set_time = 0 if self._entity_is_below(game.well) or self._entity_is_below(game.stack): self._play_piece_set_sound() game.stack.add_piece(self) # TODO do on tetris object level? game.current_piece = None # TODO turn into a method else: return False return True def _entity_is_below(self, entity: Entity) -> bool: tile_size = ConfigurationManager.get("engine", "tile-size") mimic_points = self._mimic_move((0, tile_size)) return entity and entity._collide(mimic_points) # shape attributes I_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((3, 0), (4, 0), (4, 1), (3, 1)), (2, 0)) J_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5)) L_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1.5, 0.5)) O_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1, 1)) S_SHAPE = (((0, 1), (1, 1), (1, 2), (0, 2)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5)) T_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5)) Z_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5)) """ TODO description """ class Stack(Entity): def __init__(self, color: str, border_color: str): super().__init__([], color, border_color) self.rows_completed_count = 0 self.row_completion_sound = mixer.Channel(1) def update(self, elapsed_time: int) -> None: super().update(elapsed_time) self.rows_completed_count += self._complete_rows() def add_piece(self, piece: Piece) -> None: self.points += piece.points # TODO refactor into multiple functions def _complete_rows(self) -> int: squares_by_row = {} for square in self.points: top_left_vertex = square[0] if top_left_vertex[1] not in squares_by_row: squares_by_row[top_left_vertex[1]] = [] if square not in squares_by_row[top_left_vertex[1]]: squares_by_row[top_left_vertex[1]].append(square) squares_to_exclude = [] rows_completed = [] for key in squares_by_row: if len(squares_by_row[key]) == Well.WIDTH: squares_to_exclude += squares_by_row[key] rows_completed.append(key) if len(squares_to_exclude) == 0: return 0 self._play_row_completion_sound() tile_size = ConfigurationManager.get("engine", "tile-size") new_points = [] for square in self.points: if square not in squares_to_exclude: for vertex in square: distance_to_move = 0 for row_completed in rows_completed: if vertex[1] <= row_completed: distance_to_move += 1 vertex[1] += tile_size * distance_to_move new_points.append(square) self.points = new_points return len(rows_completed) def _play_row_completion_sound(self) -> None: row_completion_sound_file = ConfigurationManager.get("sound", "row-completion") self.row_completion_sound.play(mixer.Sound(row_completion_sound_file)) """ TODO description """ class PieceGenerator: _bucket = [] @classmethod def get_piece(cls, position: Tuple) -> Piece: if len(cls._bucket) == 0: cls._generate_bucket() base_color, inner_border_color, border_color = cls._get_piece_color() return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_border_color, border_color) @classmethod def _generate_bucket(cls) -> None: piece_types = list(range(7)) while len(cls._bucket) != 7: random_number = random.randint(0, 6 - len(cls._bucket)) cls._bucket.append(piece_types.pop(random_number)) def _get_piece_shape(piece_number: int) -> Tuple: if piece_number == 0: return Piece.I_SHAPE if piece_number == 1: return Piece.J_SHAPE if piece_number == 2: return Piece.L_SHAPE if piece_number == 3: return Piece.O_SHAPE if piece_number == 4: return Piece.S_SHAPE if piece_number == 5: return Piece.T_SHAPE if piece_number == 6: return Piece.Z_SHAPE return None def _get_piece_color() -> Tuple: random_number = random.randint(1, 3) base_color = ConfigurationManager.get("color", "piece-" + str(random_number)) inner_border_color = None if random_number != 3 else ConfigurationManager.get("color", "piece-inner-border-1") border_color = ConfigurationManager.get("color", "piece-border-1") return (base_color, inner_border_color, border_color)