From 092930b202e8c1ee39a56d2d444a4b570f9cf3e4 Mon Sep 17 00:00:00 2001 From: Giovani Rodriguez Date: Fri, 11 Jun 2021 22:48:10 -0400 Subject: [PATCH] refactor!: restructure project files and modules --- entity/Entity.py | 38 ---- entity/Piece.py | 169 ---------------- entity/Stack.py | 60 ------ entity/Well.py | 37 ---- main.py | 18 +- resource/font/gravity-font.bmp | Bin 0 -> 196662 bytes tetris/entity.py | 348 +++++++++++++++++++++++++++++++++ Tetris.py => tetris/game.py | 50 ++--- tetris/util.py | 23 +++ util/ConfigurationManager.py | 13 -- util/ControlsManger.py | 0 util/PieceGenerator.py | 54 ----- 12 files changed, 405 insertions(+), 405 deletions(-) delete mode 100644 entity/Entity.py delete mode 100644 entity/Piece.py delete mode 100644 entity/Stack.py delete mode 100644 entity/Well.py create mode 100644 resource/font/gravity-font.bmp create mode 100644 tetris/entity.py rename Tetris.py => tetris/game.py (72%) create mode 100644 tetris/util.py delete mode 100644 util/ConfigurationManager.py delete mode 100644 util/ControlsManger.py delete mode 100644 util/PieceGenerator.py diff --git a/entity/Entity.py b/entity/Entity.py deleted file mode 100644 index b3af22b..0000000 --- a/entity/Entity.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import List, Tuple -import pygame - -from util.ConfigurationManager import ConfigurationManager - -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) -> None: - self.elapsed_time += elapsed_time - - def draw(self, surface: pygame.Surface) -> None: - tile_size = ConfigurationManager.configuration["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) -> 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_points(self, points: List) -> bool: # TODO change name later - 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 diff --git a/entity/Piece.py b/entity/Piece.py deleted file mode 100644 index da3b586..0000000 --- a/entity/Piece.py +++ /dev/null @@ -1,169 +0,0 @@ -from typing import List, Tuple -from pygame import mixer -import pygame -import copy - -from entity.Entity import Entity -from util.ConfigurationManager import ConfigurationManager - -''' - 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.configuration["engine"]["piece-gravity-time"] - self.current_gravity_time = 0 - self.applying_gravity = True - - # Set - self.set_time = ConfigurationManager.configuration["engine"]["piece-set-time"] - self.current_set_time = 0 - self.applying_set = False - - def update(self, elapsed_time: int, tetris) -> None: - super().update(elapsed_time) - - if self.applying_gravity: - self.applying_gravity = self.__apply_gravity(elapsed_time, tetris.well, tetris.stack) - self.applying_set = not self.applying_gravity - - if self.applying_set: - self.applying_set = self.__apply_set(elapsed_time, tetris) - self.applying_gravity = not self.applying_set - - def draw(self, surface: pygame.Surface) -> None: - super().draw(surface) - - tile_size = ConfigurationManager.configuration["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): # TODO figure out how to annotate return type as Piece - 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.configuration["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.configuration["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.configuration["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, well, stack) -> bool: # TODO define well and stack type - tile_size = ConfigurationManager.configuration["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, tetris) -> bool: # TODO define tetris type - self.current_set_time += elapsed_time - - if self.current_set_time >= self.set_time: - self.current_set_time = 0 - if self.__entity_is_below(tetris.well) or self.__entity_is_below(tetris.stack): - self.__play_piece_set_sound() - tetris.stack.add_piece(self) # TODO do on tetris object level? - tetris.current_piece = None # TODO turn into a method - else: - return False - - return True - - def __entity_is_below(self, entity: Entity) -> bool: # TODO - tile_size = ConfigurationManager.configuration["engine"]["tile-size"] - mimic_points = self.__mimic_move((0, tile_size)) - - return entity and entity.collide_points(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)) \ No newline at end of file diff --git a/entity/Stack.py b/entity/Stack.py deleted file mode 100644 index fca3217..0000000 --- a/entity/Stack.py +++ /dev/null @@ -1,60 +0,0 @@ -from pygame import mixer - -from util.ConfigurationManager import ConfigurationManager -from entity.Piece import Piece -from entity.Well import Well -from entity.Entity import Entity - -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) -> None: - super().update(elapsed_time) - self.rows_completed_count += self.__complete_rows() - - def add_piece(self, piece: Piece) -> None: - self.points += piece.points - - 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.configuration["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.configuration["sound"]["row-completion"] - self.row_completion_sound.play(mixer.Sound(row_completion_sound_file)) \ No newline at end of file diff --git a/entity/Well.py b/entity/Well.py deleted file mode 100644 index 570a8b0..0000000 --- a/entity/Well.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import List, Tuple - -from entity.Entity import Entity -from util.ConfigurationManager import ConfigurationManager - -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.configuration["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 - - - - \ No newline at end of file diff --git a/main.py b/main.py index 421e3e2..62bc0f2 100644 --- a/main.py +++ b/main.py @@ -4,20 +4,18 @@ https://tetris.com/play-tetris ''' -# TODO review imports to make sure it is being done correctly - -from Tetris import Tetris -from util.ConfigurationManager import ConfigurationManager +from tetris.game import Game +from tetris.util import ConfigurationManager def main() -> None: ConfigurationManager.load() - tetris = Tetris() - tetris.initialize() + game = Game() + game.initialize() while True: - tetris.update() - tetris.draw() + game.update() + game.draw() -# start main function -main() \ No newline at end of file +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/resource/font/gravity-font.bmp b/resource/font/gravity-font.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d68393c2c668c5cc635806a5fd97e39fbc737893 GIT binary patch literal 196662 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 literal 0 HcmV?d00001 diff --git a/tetris/entity.py b/tetris/entity.py new file mode 100644 index 0000000..f62877a --- /dev/null +++ b/tetris/entity.py @@ -0,0 +1,348 @@ +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) \ No newline at end of file diff --git a/Tetris.py b/tetris/game.py similarity index 72% rename from Tetris.py rename to tetris/game.py index e2ad5d5..d58f60d 100644 --- a/Tetris.py +++ b/tetris/game.py @@ -1,14 +1,13 @@ import sys import pygame from pygame import mixer - -from util.ConfigurationManager import ConfigurationManager -from util.PieceGenerator import PieceGenerator -from entity.Well import Well -from entity.Stack import Stack +from tetris.util import ConfigurationManager +from tetris.entity import PieceGenerator +from tetris.entity import Well +from tetris.entity import Stack # TODO should be a singleton and refactor the whole file? -class Tetris: +class Game: def __init__(self): self.fps = -1 @@ -22,20 +21,19 @@ class Tetris: def initialize(self) -> None: pygame.init() - mixer.init() - win_width = ConfigurationManager.configuration["window"]["width"] - win_height = ConfigurationManager.configuration["window"]["height"] - win_icon = ConfigurationManager.configuration["window"]["icon"] - win_title = ConfigurationManager.configuration["window"]["title"] + win_width = ConfigurationManager.get("window", "width") + win_height = ConfigurationManager.get("window", "height") + win_icon = ConfigurationManager.get("window", "icon") + win_title = ConfigurationManager.get("window", "title") - self.fps = ConfigurationManager.configuration["engine"]["fps"] - self.tile_size = ConfigurationManager.configuration["engine"]["tile-size"] + self.fps = ConfigurationManager.get("engine", "fps") + self.tile_size = ConfigurationManager.get("engine", "tile-size") self.screen = pygame.display.set_mode((win_width, win_height)) self.clock = pygame.time.Clock() self.main_music = mixer.Channel(0) - self.well = Well((280, 80), ConfigurationManager.configuration["color"]["well-1"], ConfigurationManager.configuration["color"]["well-border-1"]) # TODO calculate position later and redo color config for well - self.stack = Stack(ConfigurationManager.configuration["color"]["stack-1"], ConfigurationManager.configuration["color"]["stack-border-1"]) + self.well = Well((280, 80), ConfigurationManager.get("color", "well-1"), ConfigurationManager.get("color", "well-border-1")) # TODO calculate position later and redo color config for well + self.stack = Stack(ConfigurationManager.get("color", "stack-1"), ConfigurationManager.get("color", "stack-border-1")) loaded_icon = pygame.image.load(win_icon) pygame.display.set_caption(win_title) @@ -43,7 +41,7 @@ class Tetris: self.is_pressing_down = False # TODO move into control util later - main_music_file = ConfigurationManager.configuration["sound"]["main-music"] + main_music_file = ConfigurationManager.get("sound", "main-music") self.main_music.set_volume(0.7) # TODO add volume to the config self.main_music.play(mixer.Sound(main_music_file), -1) @@ -56,7 +54,7 @@ class Tetris: self.current_piece.update(elapsed_time, self) else: self.current_piece = PieceGenerator.get_piece((360, 100)) # TODO calculate spawn position - if self.stack and self.current_piece.collide(self.stack): + if self.stack and self.current_piece.collide(self.stack): # game over pygame.quit() sys.exit() @@ -91,26 +89,26 @@ class Tetris: if event.key == pygame.K_DOWN: self.is_pressing_down = True if self.current_piece: - self.current_piece.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] / 8 - self.current_piece.set_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] / 8 + self.current_piece.gravity_time = ConfigurationManager.get("engine", "piece-gravity-time") / 8 + self.current_piece.set_time = ConfigurationManager.get("engine", "piece-gravity-time") / 8 if event.type == pygame.KEYUP: if event.key == pygame.K_DOWN: self.is_pressing_down = False if self.current_piece: - self.current_piece.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] - self.current_piece.set_time = ConfigurationManager.configuration["engine"]["piece-set-time"] + self.current_piece.gravity_time = ConfigurationManager.get("engine", "piece-gravity-time") + self.current_piece.set_time = ConfigurationManager.get("engine", "piece-set-time") if self.is_pressing_down: if self.current_piece: - self.current_piece.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] / 8 - self.current_piece.set_time = ConfigurationManager.configuration["engine"]["piece-set-time"] / 8 + self.current_piece.gravity_time = ConfigurationManager.get("engine", "piece-gravity-time") / 8 + self.current_piece.set_time = ConfigurationManager.get("engine", "piece-set-time") / 8 def draw(self) -> None: # TODO write not initialized exception # draw window bg - bg_color = pygame.Color(ConfigurationManager.configuration["color"]["window-bg"]) + bg_color = pygame.Color(ConfigurationManager.get("color", "window-bg")) self.screen.fill(bg_color) # draw all game objects @@ -121,6 +119,10 @@ class Tetris: if self.current_piece: self.current_piece.draw(self.screen) + self.sheet = pygame.image.load("resource/font/gravity-font.bmp").convert() + self.screen.blit(self.sheet, (0, 0), pygame.Rect(30, 30, 30, 30)) + # update display pygame.display.update() + \ No newline at end of file diff --git a/tetris/util.py b/tetris/util.py new file mode 100644 index 0000000..52b94d1 --- /dev/null +++ b/tetris/util.py @@ -0,0 +1,23 @@ +import random +import yaml +from typing import Tuple + +""" + TODO description +""" +class ConfigurationManager: + + CONFIG_FILE_LOCATION = "config.yaml" + configuration = [] + + @classmethod + def load(cls) -> None: + with open(cls.CONFIG_FILE_LOCATION, "r") as yaml_file: + cls.configuration = yaml.safe_load(yaml_file) + + @classmethod + def get(cls, key: str, sub_key: str = None): + if sub_key: + return cls.configuration[key][sub_key] + else: + return cls.configuration[key] \ No newline at end of file diff --git a/util/ConfigurationManager.py b/util/ConfigurationManager.py deleted file mode 100644 index 83b143d..0000000 --- a/util/ConfigurationManager.py +++ /dev/null @@ -1,13 +0,0 @@ -import yaml - -CONFIG_FILE_LOCATION = "config.yaml" - -# TODO add getter for configuration? -class ConfigurationManager: - - configuration = [] - - @classmethod - def load(cls) -> None: - with open(CONFIG_FILE_LOCATION, "r") as yaml_file: - cls.configuration = yaml.safe_load(yaml_file) \ No newline at end of file diff --git a/util/ControlsManger.py b/util/ControlsManger.py deleted file mode 100644 index e69de29..0000000 diff --git a/util/PieceGenerator.py b/util/PieceGenerator.py deleted file mode 100644 index 062c10e..0000000 --- a/util/PieceGenerator.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Tuple -import random - -from entity.Piece import Piece -from util.ConfigurationManager import ConfigurationManager - -""" - TODO Add link that goes through random piece generation -""" -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.configuration["color"]["piece-" + str(random_number)] - inner_border_color = None if random_number != 3 else ConfigurationManager.configuration["color"]["piece-inner-border-1"] - border_color = ConfigurationManager.configuration["color"]["piece-border-1"] - - return (base_color, inner_border_color, border_color)