diff --git a/src/game/block.py b/src/game/block.py index ecd2f50..b465e3c 100644 --- a/src/game/block.py +++ b/src/game/block.py @@ -4,6 +4,20 @@ from utils import CONFIG, Size class Block(pygame.sprite.Sprite): + """ + Initializes a Block object. + + Args: + group: Sprite group to which the block belongs. + pos: Initial position of the block. + color: Color of the block. + + Attributes: + image: Image representing the block. + pos: Position of the block. + rect: Rectangle representing the block. + """ + def __init__( self, /, @@ -13,20 +27,67 @@ class Block(pygame.sprite.Sprite): color: str, ) -> None: super().__init__(group) - self.image = pygame.Surface(CONFIG.game.cell) - self.image.fill(color) - - self.pos = pygame.Vector2(pos) + CONFIG.game.offset - self.rect = self.image.get_rect(topleft=self.pos * CONFIG.game.cell.width) + self._initialize_image(color) + self._initialize_positions(pos) def update(self) -> None: + """Updates the block's position on the screen.""" self.rect.topleft = self.pos * CONFIG.game.cell.width def vertical_collision(self, x: int, field: np.ndarray) -> bool: + """ + Checks for vertical collision with the game field. + + Args: + x: The x-coordinate to check for collision. + field: 2D array representing the game field. + + Returns: + True if there is a vertical collision, False otherwise. + """ return not 0 <= x < CONFIG.game.columns or field[int(self.pos.y), x] def horizontal_collision(self, y: int, field: np.ndarray) -> bool: + """ + Checks for horizontal collision with the game field. + + Args: + y: The y-coordinate to check for collision. + field: 2D array representing the game field. + + Returns: + True if there is a horizontal collision, False otherwise. + """ return y >= CONFIG.game.rows or (y >= 0 and field[y, int(self.pos.x)]) def rotate(self, pivot: pygame.Vector2) -> pygame.Vector2: + """ + Rotates the block around a given pivot point. + + Args: + pivot: The pivot point for rotation. + + Returns: + The new position of the block after rotation. + """ return pivot + (self.pos - pivot).rotate(90) + + def _initialize_image(self, color: str) -> None: + """ + Initializes the image of the block with a specified color. + + Args: + color: Color of the block. + """ + self.image = pygame.Surface(CONFIG.game.cell) + self.image.fill(color) + + def _initialize_positions(self, pos: pygame.Vector2) -> None: + """ + Initializes the position of the block. + + Args: + pos: Initial position of the block. + """ + self.pos = pygame.Vector2(pos) + CONFIG.game.offset + self.rect = self.image.get_rect(topleft=self.pos * CONFIG.game.cell.width) diff --git a/src/game/game.py b/src/game/game.py index 557eadb..c93c2b9 100644 --- a/src/game/game.py +++ b/src/game/game.py @@ -11,55 +11,62 @@ from .timer import Timer, Timers class Game: + """ + Game class for managing the game state. + + Args: + get_next_figure: A function to get the next figure. + update_score: A function to update the score. + + Attributes: + surface: Surface representing the game. + dispaly_surface: Surface representing the display. + rect: Rect representing the game surface. + sprites: Sprite group for managing blocks. + get_next_figure: A function to get the next figure. + update_score: A function to update the score. + grid_surface: Surface representing the grid. + field: 2D array representing the game field. + tetromino: The current tetromino. + timers: Game timers. + initial_block_speed: Initial block speed. + increased_block_speed: Increased block speed. + down_pressed: True if the down key is pressed, False otherwise. + level: Current game level. + score: Current game score. + lines: Number of lines cleared. + landing_sound: Sound effect for landing blocks. + """ + def __init__( self, get_next_figure: Callable[[], Figure], update_score: Callable[[int, int, int], None], ) -> None: - self.surface = pygame.Surface(CONFIG.game.size) - self.dispaly_surface = pygame.display.get_surface() - self.rect = self.surface.get_rect(topleft=CONFIG.game.pos) + self._initialize_game_surface() + self._initialize_sprites() - self.sprites: pygame.sprite.Group[Block] = pygame.sprite.Group() - - self.get_next_shape = get_next_figure + self.get_next_figure = get_next_figure self.update_score = update_score - self._create_grid_surface() + self._initialize_grid_surface() + self._initialize_field_and_tetromino() + self.tetromino: Tetromino - self.field = self._generate_empty_field() - - self.tetromino = Tetromino( - self.sprites, - self.create_new_tetromino, - self.field, - ) - - self.initial_block_speed = CONFIG.game.initial_speed - self.increased_block_speed = self.initial_block_speed * 0.3 - self.down_pressed = False - self.timers = Timers( - Timer(self.initial_block_speed, True, self.move_down), - Timer(CONFIG.game.movment_delay), - Timer(CONFIG.game.rotation_delay), - ) - self.timers.vertical.activate() - - self.level = 1 - self.score = 0 - self.lines = 0 - - self.landing_sound = pygame.mixer.Sound(CONFIG.music.landing) - self.landing_sound.set_volume(CONFIG.music.volume) + self._initialize_game_state() + self._initialize_timers() + self.timers: Timers def run(self) -> None: - self.dispaly_surface.blit(self.surface, CONFIG.game.pos) + """Run a single iteration of the game loop.""" + self._update_display_surface() self.draw() self._timer_update() self.handle_event() def draw(self) -> None: - self.surface.fill(CONFIG.colors.bg_float) + """Draw the game surface and its components.""" + self._fill_game_surface() self.update() self.sprites.draw(self.surface) self._draw_border() @@ -69,51 +76,28 @@ class Game: self.sprites.update() def handle_event(self) -> None: - keys = pygame.key.get_pressed() + """Handle player input events.""" + keys: list[bool] = pygame.key.get_pressed() - left_keys = keys[pygame.K_LEFT] or keys[pygame.K_a] or keys[pygame.K_h] - right_keys = keys[pygame.K_RIGHT] or keys[pygame.K_d] or keys[pygame.K_l] - down_keys = keys[pygame.K_DOWN] or keys[pygame.K_s] or keys[pygame.K_j] - rotate_keys = ( - keys[pygame.K_SPACE] - or keys[pygame.K_r] - or keys[pygame.K_UP] - or keys[pygame.K_w] - or keys[pygame.K_k] - ) - - if not self.timers.horizontal.active: - if left_keys: - self.move_left() - self.timers.horizontal.activate() - elif right_keys: - self.move_right() - self.timers.horizontal.activate() - - if not self.timers.rotation.active: - if rotate_keys: - self.tetromino.rotate() - self.timers.rotation.activate() - - if not self.down_pressed and down_keys: - self.down_pressed = True - self.timers.vertical.duration = self.increased_block_speed - - if self.down_pressed and not down_keys: - self.down_pressed = False - self.timers.vertical.duration = self.initial_block_speed + self._handle_movement_keys(keys) + self._handle_rotation_keys(keys) + self._handle_down_key(keys) def move_down(self) -> None: + """Move the current tetromino down.""" self.tetromino.move_down() def move_left(self) -> None: + """Move the current tetromino to the left.""" self.tetromino.move_horizontal(Direction.LEFT) def move_right(self) -> None: + """Move the current tetromino to the right.""" self.tetromino.move_horizontal(Direction.RIGHT) def create_new_tetromino(self) -> None: - self.landing_sound.play() + """Create a new tetromino and perform necessary actions.""" + self._play_landing_sound() if self.game_over(): self.restart() @@ -122,58 +106,40 @@ class Game: self.sprites, self.create_new_tetromino, self.field, - self.get_next_shape(), + self.get_next_figure(), ) def game_over(self) -> bool: + """ + Check if the game is over. + + Returns: + True if the game is over, False otherwise. + """ for block in self.sprites: if block.pos.y < 0: log.info("Game over!") return True + return False def restart(self) -> None: - self.sprites.empty() - self.field = self._generate_empty_field() - self.tetromino = Tetromino( - self.sprites, - self.create_new_tetromino, - self.field, - self.get_next_shape(), - ) - self.level = 1 - self.score = 0 - self.lines = 0 - self.update_score(self.lines, self.score, self.level) - - def _create_grid_surface(self) -> None: - self.grid_surface = self.surface.copy() - self.grid_surface.fill("#00ff00") - self.grid_surface.set_colorkey("#00ff00") - self.grid_surface.set_alpha(100) + """Restart the game.""" + self._reset_game_state() + self._initialize_field_and_tetromino() def _draw_grid(self) -> None: + """Draw the grid on the game surface.""" for col in range(1, CONFIG.game.columns): x = col * CONFIG.game.cell.width - pygame.draw.line( - self.grid_surface, - CONFIG.colors.border_highlight, - (x, 0), - (x, self.grid_surface.get_height()), - CONFIG.game.line_width, - ) - for row in range(1, CONFIG.game.rows): - y = row * CONFIG.game.cell.width - pygame.draw.line( - self.grid_surface, - CONFIG.colors.border_highlight, - (0, y), - (self.grid_surface.get_width(), y), - CONFIG.game.line_width, - ) + self._draw_vertical_grid_line(x) + for row in range(1, CONFIG.game.rows): + y = row * CONFIG.game.cell.width + self._draw_horizontal_grid_line(y) self.surface.blit(self.grid_surface, (0, 0)) def _draw_border(self) -> None: + """Draw the border of the game surface.""" pygame.draw.rect( self.dispaly_surface, CONFIG.colors.border_highlight, @@ -183,10 +149,12 @@ class Game: ) def _timer_update(self) -> None: + """Update the timers.""" for timer in self.timers: timer.update() def _check_finished_rows(self) -> None: + """Check and handle finished rows.""" delete_rows: list[int] = [] for idx, row in enumerate(self.field): if all(row): @@ -195,41 +163,193 @@ class Game: self._delete_rows(delete_rows) def _delete_rows(self, delete_rows: list[int]) -> None: + """Delete the specified rows.""" if not delete_rows: return self._calculate_score(len(delete_rows)) for row in delete_rows: - for block in self.field[row]: - block.kill() - + self._remove_blocks_in_row(row) self._move_rows_down(row) self._rebuild_field() + def _remove_blocks_in_row(self, row: int) -> None: + """Remove blocks in the specified row.""" + for block in self.field[row]: + block.kill() + def _move_rows_down(self, deleted_row: int) -> None: + """Move rows down after deleting a row.""" for row in self.field: for block in row: if block and block.pos.y < deleted_row: block.pos.y += 1 def _rebuild_field(self) -> None: + """Rebuild the game field after deleting rows.""" self.field = self._generate_empty_field() for block in self.sprites: self.field[int(block.pos.y), int(block.pos.x)] = block def _generate_empty_field(self) -> np.ndarray: + """Generate an empty game field.""" return np.full((CONFIG.game.rows, CONFIG.game.columns), None, dtype=Field) def _calculate_score(self, rows_deleted: int) -> None: + """Calculate and update the game score.""" self.lines += rows_deleted self.score += CONFIG.game.score.get(rows_deleted, 0) * self.level + self._check_level_up() + self.update_score(self.lines, self.score, self.level) - # every 10 lines increase level + def _check_level_up(self) -> None: + """Check if the player should level up.""" + # incerement level every 10 lines if self.lines // 10 + 1 > self.level: - self.level += 1 - self.initial_block_speed *= 0.75 - self.increased_block_speed *= 0.75 + self._level_up() + + def _level_up(self) -> None: + """Level up.""" + self.level += 1 + self.initial_block_speed *= 0.75 + self.increased_block_speed *= 0.75 + self.timers.vertical.duration = self.initial_block_speed + + def _draw_components(self) -> None: + """Draw additional components like borders and grid.""" + self.sprites.draw(self.surface) + self._draw_border() + self._draw_grid() + + def _initialize_game_surface(self) -> None: + """Initialize the game surface.""" + self.surface = pygame.Surface(CONFIG.game.size) + self.dispaly_surface = pygame.display.get_surface() + self.rect = self.surface.get_rect(topleft=CONFIG.game.pos) + + def _initialize_sprites(self) -> None: + """Initialize the game sprites.""" + self.sprites: pygame.sprite.Group[Block] = pygame.sprite.Group() + + def _initialize_grid_surface(self) -> None: + """Initialize the grid surface.""" + self.grid_surface = self.surface.copy() + self.grid_surface.fill("#00ff00") + self.grid_surface.set_colorkey("#00ff00") + self.grid_surface.set_alpha(100) + + def _initialize_field_and_tetromino(self) -> None: + """Initialize the game field and tetromino.""" + self.field = self._generate_empty_field() + + self.tetromino = Tetromino( + self.sprites, + self.create_new_tetromino, + self.field, + ) + + def _initialize_timers(self) -> None: + """Initialize game timers.""" + self.timers = Timers( + Timer(self.initial_block_speed, True, self.move_down), + Timer(CONFIG.game.movment_delay), + Timer(CONFIG.game.rotation_delay), + ) + self.timers.vertical.activate() + + def _initialize_game_state(self) -> None: + """Initialize the game state.""" + self.initial_block_speed = CONFIG.game.initial_speed + self.increased_block_speed = self.initial_block_speed * 0.4 + self.down_pressed = False + self.level = 1 + self.score = 0 + self.lines = 0 + self._initialize_sound() + + def _initialize_sound(self) -> None: + """Initialize game sounds.""" + self.landing_sound = pygame.mixer.Sound(CONFIG.music.landing) + self.landing_sound.set_volume(CONFIG.music.volume * 2) + + def _play_landing_sound(self) -> None: + """Play the landing sound effect.""" + self.landing_sound.play() + + def _update_display_surface(self) -> None: + """Update the display surface.""" + self.dispaly_surface.blit(self.surface, CONFIG.game.pos) + + def _fill_game_surface(self) -> None: + """Fill the game surface with background color.""" + self.surface.fill(CONFIG.colors.bg_float) + + def _handle_movement_keys(self, keys: list[bool]) -> None: + """Handle movement keys [K_LEFT, K_RIGHT, K_a, K_d, K_h, K_l].""" + left_keys = keys[pygame.K_LEFT] or keys[pygame.K_a] or keys[pygame.K_h] + right_keys = keys[pygame.K_RIGHT] or keys[pygame.K_d] or keys[pygame.K_l] + + if not self.timers.horizontal.active: + if left_keys: + self.move_left() + self.timers.horizontal.activate() + elif right_keys: + self.move_right() + self.timers.horizontal.activate() + + def _handle_rotation_keys(self, keys: list[bool]) -> None: + """Handle rotation keys [K_SPACE, K_r, K_UP, K_w, K_k].""" + rotate_keys = ( + keys[pygame.K_SPACE] + or keys[pygame.K_r] + or keys[pygame.K_UP] + or keys[pygame.K_w] + or keys[pygame.K_k] + ) + if not self.timers.rotation.active: + if rotate_keys: + self.tetromino.rotate() + self.timers.rotation.activate() + + def _handle_down_key(self, keys: list[bool]) -> None: + """Handle the down key [K_DOWN, K_s, K_j].""" + down_keys = keys[pygame.K_DOWN] or keys[pygame.K_s] or keys[pygame.K_j] + if not self.down_pressed and down_keys: + self.down_pressed = True + self.timers.vertical.duration = self.increased_block_speed + + if self.down_pressed and not down_keys: + self.down_pressed = False self.timers.vertical.duration = self.initial_block_speed + def _reset_game_state(self) -> None: + """Reset the game state.""" + self.sprites.empty() + self.field = self._generate_empty_field() + + self.level = 1 + self.score = 0 + self.lines = 0 + self.update_score(self.lines, self.score, self.level) + + def _draw_vertical_grid_line(self, x: int | float) -> None: + """Draw a vertical grid line.""" + pygame.draw.line( + self.grid_surface, + CONFIG.colors.border_highlight, + (x, 0), + (x, self.grid_surface.get_height()), + CONFIG.game.line_width, + ) + + def _draw_horizontal_grid_line(self, y: int | float) -> None: + """Draw a horizontal grid line.""" + pygame.draw.line( + self.grid_surface, + CONFIG.colors.border_highlight, + (0, y), + (self.grid_surface.get_width(), y), + CONFIG.game.line_width, + ) diff --git a/src/game/main.py b/src/game/main.py index 8a706fa..59e397d 100644 --- a/src/game/main.py +++ b/src/game/main.py @@ -11,22 +11,37 @@ from .tetromino import Tetromino class Main: + """ + Main class for the game. + + Attributes: + display_surface: Pygame display surface. + clock: Pygame clock. + music: Pygame music. + game: Game object. + score: Score object. + preview: Preview object. + next_figures: List of upcoming figures. + music: Pygame music that plays in the background. + """ + def __init__(self) -> None: + log.info("Initializing the game") self._initialize_pygeme() self._initialize_game_components() self._start_background_music() def draw(self) -> None: + """Update the display.""" pygame.display.update() def run(self) -> None: + """Run the main game loop.""" while True: self._run_game_loop() - def _update_score(self, lines: int, score: int, level: int) -> None: - self.score.update(lines, score, level) - def handle_events(self) -> None: + """Handle Pygame events.""" for event in pygame.event.get(): if event.type == pygame.QUIT: self.exit() @@ -35,18 +50,46 @@ class Main: self.exit() def exit(self) -> None: + """Exit the game.""" pygame.quit() sys.exit() + def _update_score(self, lines: int, score: int, level: int) -> None: + """ + Update the game score. + + Args: + lines: Number of lines cleared. + score: Current score. + level: Current game level. + """ + self.score.update(lines, score, level) + def _generate_next_figures(self, amount: int = 3) -> list[Figure]: + """ + Generate the next set of random figures. + + Args: + amount: Number of figures to generate (default is 3). + + Returns: + List of randomly generated figures. + """ return [Figure.random() for _ in range(amount)] def _get_next_figure(self) -> Figure: + """ + Get the next figure in the sequence. + + Returns: + The next figure in the sequence. + """ next_shape = self.next_figures.pop(0) self.next_figures.append(Figure.random()) return next_shape def _initialize_pygeme(self) -> None: + """Initialize Pygame and set up the display.""" pygame.init() pygame.display.set_caption(CONFIG.window.title) self.display_surface = pygame.display.set_mode(CONFIG.window.size) @@ -54,6 +97,7 @@ class Main: self.clock = pygame.time.Clock() def _initialize_game_components(self) -> None: + """Initialize game-related components.""" self.next_figures = self._generate_next_figures() self.game = Game(self._get_next_figure, self._update_score) @@ -61,11 +105,13 @@ class Main: self.preview = Preview() def _start_background_music(self) -> None: + """Start playing background music.""" self.music = pygame.mixer.Sound(CONFIG.music.background) self.music.set_volume(CONFIG.music.volume) self.music.play(-1) def _run_game_loop(self) -> None: + """Run a single iteration of the game loop.""" self.draw() self.handle_events() diff --git a/src/game/preview.py b/src/game/preview.py index c8fa8d7..964d010 100644 --- a/src/game/preview.py +++ b/src/game/preview.py @@ -3,28 +3,34 @@ from utils import CONFIG, Figure, Size class Preview: - def __init__(self) -> None: - self.surface = pygame.Surface(CONFIG.sidebar.preview) - self.rect = self.surface.get_rect( - topright=( - CONFIG.window.size.width - CONFIG.window.padding, - CONFIG.window.padding, - ) - ) - self.dispaly_surface = pygame.display.get_surface() + """ + Class representing the preview of upcoming figures on the sidebar. - self.increment_height = self.surface.get_height() / 3 + Attributes: + surface: Pygame surface representing the preview. + rect: Pygame rectangle representing the preview. + dispaly_surface: Pygame display surface. + increment_height: Height of each figure in the preview. + """ + + def __init__(self) -> None: + self._initialize_surface() + self._initialize_rect() + + self._initialize_increment_height() def run(self, next_figures: list[Figure]) -> None: - self.dispaly_surface.blit(self.surface, self.rect) - self.draw(next_figures) + """ + Run the preview by updating the display and drawing next figures. - def draw(self, next_figures: list[Figure]) -> None: - self.surface.fill(CONFIG.colors.bg_sidebar) - self._draw_border() - self._draw_figures(next_figures) + Args: + next_figures (list[Figure]): List of upcoming figures. + """ + self.dispaly_surface.blit(self.surface, self.rect) + self._draw_preview(next_figures) def _draw_border(self) -> None: + """Draw the border around the preview surface.""" pygame.draw.rect( self.dispaly_surface, CONFIG.colors.border_highlight, @@ -34,9 +40,58 @@ class Preview: ) def _draw_figures(self, figures: list[Figure]) -> None: + """ + Draw the upcoming figures on the preview surface. + + Args: + figures (list[Figure]): List of upcoming figures. + """ for idx, figure in enumerate(figures): - figure_surface = figure.value.image - x = self.surface.get_width() / 2 - y = self.increment_height / 2 + idx * self.increment_height - rect = figure_surface.get_rect(center=(x, y)) - self.surface.blit(figure_surface, rect) + self._draw_figure(figure, idx) + + def _draw_figure(self, figure: Figure, idx: int) -> None: + """ + Draw a single upcoming figure on the preview surface. + + Args: + figure (Figure): The upcoming figure to draw. + idx (int): Index of the figure in the list. + """ + figure_surface = figure.value.image + x = self.surface.get_width() / 2 + y = self.increment_height / 2 + idx * self.increment_height + rect = figure_surface.get_rect(center=(x, y)) + self.surface.blit(figure_surface, rect) + + def _draw_preview(self, next_figures: list[Figure]) -> None: + """ + Draw the preview with the background, border, and next figures. + + Args: + next_figures (list[Figure]): List of upcoming figures. + """ + self._draw_background() + self._draw_border() + self._draw_figures(next_figures) + + def _draw_background(self) -> None: + """Draw the background of the preview.""" + self.surface.fill(CONFIG.colors.bg_sidebar) + + def _initialize_surface(self) -> None: + """Initialize the preview surface.""" + self.surface = pygame.Surface(CONFIG.sidebar.preview) + self.dispaly_surface = pygame.display.get_surface() + + def _initialize_rect(self) -> None: + """Initialize the preview rectangle.""" + self.rect = self.surface.get_rect( + topright=( + CONFIG.window.size.width - CONFIG.window.padding, + CONFIG.window.padding, + ) + ) + + def _initialize_increment_height(self) -> None: + """Initialize the increment height for positioning text elements.""" + self.increment_height = self.surface.get_height() / 3 diff --git a/src/game/score.py b/src/game/score.py index 92fbb8c..3e67833 100644 --- a/src/game/score.py +++ b/src/game/score.py @@ -3,42 +3,66 @@ from utils import CONFIG, Size class Score: + """ + Class representing the score on the sidebar. + + Attributes: + surface: Pygame surface representing the score. + dispaly_surface: Pygame display surface. + rect: Pygame rectangle representing the score. + font: Pygame font used to display the score. + text: Text to be displayed on the score. + increment_height: Height of each text element in the score. + """ + def __init__(self) -> None: - self.surface = pygame.Surface(CONFIG.sidebar.score) - self.dispaly_surface = pygame.display.get_surface() - self.rect = self.surface.get_rect( - bottomright=CONFIG.window.size.sub(CONFIG.window.padding) - ) - - self.font = pygame.font.Font(CONFIG.font.family, CONFIG.font.size) - + self._initialize_surface() + self._initialize_rect() + self._initialize_font() self.update(1, 0, 0) - - self.increment_height = self.surface.get_height() / 3 + self._initialize_increment_height() def run(self) -> None: + """Display the score on the game surface.""" self.dispaly_surface.blit(self.surface, self.rect) self.draw() def update(self, lines: int, score: int, level: int) -> None: - self.text = ( + """ + Update the score information. + + Args: + lines (int): Number of lines cleared. + score (int): Current game score. + level (int): Current game level. + """ + self.text: tuple[tuple[str, int], tuple[str, int], tuple[str, int]] = ( ("Score", score), ("Level", level), ("Lines", lines), ) def draw(self) -> None: - self.surface.fill(CONFIG.colors.bg_sidebar) + """Draw the score on the score surface.""" + self._draw_background() self._draw_text() self._draw_border() def _draw_text(self) -> None: + """Draw the text on the score surface.""" for idx, text in enumerate(self.text): x = self.surface.get_width() / 2 y = self.increment_height / 2 + idx * self.increment_height self._display_text(text, (x, y)) def _display_text(self, text: tuple[str, int], pos: tuple[int, int]) -> None: + """ + Display a single text element on the score surface. + + Args: + text (tuple[str, int]): A tuple containing the label and value of the text element. + pos (tuple[int, int]): The position (x, y) where the text should be displayed. + """ text_surface = self.font.render( f"{text[0]}: {text[1]}", True, CONFIG.colors.fg_sidebar ) @@ -46,6 +70,7 @@ class Score: self.surface.blit(text_surface, text_rect) def _draw_border(self) -> None: + """Draw the border of the score surface.""" pygame.draw.rect( self.dispaly_surface, CONFIG.colors.border_highlight, @@ -53,3 +78,26 @@ class Score: CONFIG.game.line_width * 2, CONFIG.game.border_radius, ) + + def _draw_background(self) -> None: + """Fill the background of the score display.""" + self.surface.fill(CONFIG.colors.bg_sidebar) + + def _initialize_surface(self) -> None: + """Initialize the score surface.""" + self.surface = pygame.Surface(CONFIG.sidebar.score) + self.dispaly_surface = pygame.display.get_surface() + + def _initialize_rect(self) -> None: + """Initialize the score rectangle.""" + self.rect = self.surface.get_rect( + bottomright=CONFIG.window.size.sub(CONFIG.window.padding) + ) + + def _initialize_font(self) -> None: + """Initialize the font used to display the score.""" + self.font = pygame.font.Font(CONFIG.font.family, CONFIG.font.size) + + def _initialize_increment_height(self) -> None: + """Initialize the increment height for positioning text elements.""" + self.increment_height = self.surface.get_height() / 3 diff --git a/src/game/tetromino.py b/src/game/tetromino.py index 6701fc8..b42fb1b 100644 --- a/src/game/tetromino.py +++ b/src/game/tetromino.py @@ -9,25 +9,44 @@ from .log import log class Tetromino: + """ + Class representing a Tetromino. + + Args: + group: Sprite group for managing blocks. + create_new: Callback function to create a new Tetromino. + field: 2D array representing the game field. + shape: Initial shape of the Tetromino (default is None). + + Attributes: + figure: Tetromino figure. + block_positions: List of block positions. + color: Color of the Tetromino. + create_new: Callback function to create a new Tetromino. + field: 2D array representing the game field. + blocks: List of Tetromino blocks. + """ + def __init__( self, group: pygame.sprite.Group, - func: Callable[[], None], + create_new: Callable[[], None], field: np.ndarray, shape: Optional[Figure] = None, ) -> None: - self.figure: Figure = shape if shape else Figure.random() + self.figure: Figure = self._generate_figure(shape) self.block_positions: list[pygame.Vector2] = self.figure.value.shape self.color: str = self.figure.value.color - self.create_new = func + self.create_new = create_new self.field = field - - self.blocks = [ - Block(group=group, pos=pos, color=self.color) - for pos in self.block_positions - ] + self.blocks = self._initialize_blocks(group) def move_down(self) -> None: + """ + Moves the Tetromino down. + + If there is a collision, the Tetromino is placed on the field, and a new one is created. + """ if not self._check_horizontal_collision(self.blocks, Direction.DOWN): for block in self.blocks: block.pos.y += 1 @@ -37,11 +56,22 @@ class Tetromino: self.create_new() def move_horizontal(self, direction: Direction) -> None: + """ + Moves the Tetromino horizontally. + + Args: + direction: Direction to move (LEFT or RIGHT). + """ if not self._check_vertical_collision(self.blocks, direction): for block in self.blocks: block.pos.x += direction.value def rotate(self) -> None: + """ + Rotates the Tetromino clockwise. + + Does not rotate if the Tetromino is an O-shaped (square) figure. + """ if self.figure == Figure.O: return @@ -51,14 +81,22 @@ class Tetromino: block.rotate(pivot) for block in self.blocks ] - if not self._are_new_positions_valid(new_positions): - return - - self._update_block_positions(new_positions) + if self._are_new_positions_valid(new_positions): + self._update_block_positions(new_positions) def _check_vertical_collision( self, blocks: list[Block], direction: Direction ) -> bool: + """ + Checks for vertical collision. + + Args: + blocks: List of blocks to check for collision. + direction: Direction of movement. + + Returns: + True if there is a vertical collision, False otherwise. + """ return any( block.vertical_collision(int(block.pos.x + direction.value), self.field) for block in self.blocks @@ -67,21 +105,71 @@ class Tetromino: def _check_horizontal_collision( self, blocks: list[Block], direction: Direction ) -> bool: + """ + Checks for horizontal collision. + + Args: + blocks: List of blocks to check for collision. + direction: Direction of movement. + + Returns: + True if there is a horizontal collision, False otherwise. + """ return any( block.horizontal_collision(int(block.pos.y + direction.value), self.field) for block in self.blocks ) - def _are_new_positions_valid(self, new_positions: list[pygame.Vector2]) -> bool: - for pos in new_positions: - if not ( - 0 <= pos.x < CONFIG.game.columns and 0 <= pos.y <= CONFIG.game.rows - ): - return False - if self.field[int(pos.y), int(pos.x)]: - return False - return True - def _update_block_positions(self, new_positions: list[pygame.Vector2]) -> None: + """ + Updates the positions of Tetromino blocks. + + Args: + new_positions: New positions for the blocks. + """ for block, new_pos in zip(self.blocks, new_positions): block.pos = new_pos + + def _are_new_positions_valid(self, new_positions: list[pygame.Vector2]) -> bool: + """ + Checks if the new positions are valid within the game field. + + Args: + new_positions: New positions to check. + + Returns: + True if all positions are valid, False otherwise. + """ + return all( + 0 <= pos.x < CONFIG.game.columns + and 0 <= pos.y <= CONFIG.game.rows + and not self.field[int(pos.y), int(pos.x)] + for pos in new_positions + ) + + def _initialize_blocks(self, group: pygame.sprite.Group) -> list[Block]: + """ + Initializes Tetromino blocks. + + Args: + group: Sprite group for managing blocks. + + Returns: + List of initialized blocks. + """ + return [ + Block(group=group, pos=pos, color=self.color) + for pos in self.block_positions + ] + + def _generate_figure(self, shape: Optional[Figure]) -> Figure: + """ + Generates a Tetromino figure. + + Args: + shape: Initial shape of the Tetromino (default is None). + + Returns: + Generated Tetromino figure. + """ + return shape if shape else Figure.random() diff --git a/src/game/timer.py b/src/game/timer.py index e614327..ec6dcde 100644 --- a/src/game/timer.py +++ b/src/game/timer.py @@ -6,6 +6,22 @@ from attrs import define, field @define class Timer: + """ + Timer class for managing timed events. + + Args: + duration: Duration of the timer in milliseconds. + repeated: Whether the timer should repeat after each completion. + func: Callback function to execute when the timer completes. + + Attributes: + duration: Duration of the timer in milliseconds. + repeated: Whether the timer should repeat after each completion. + func: Callback function to execute when the timer completes. + start_time: Time when the timer was last activated. + active: Indicates whether the timer is currently active. + """ + duration: float = field(converter=float) repeated: bool = field(default=False) func: Optional[Callable[[], None]] = field(default=None) @@ -13,14 +29,21 @@ class Timer: active: bool = False def activate(self) -> None: + """Activates the timer, setting the start time to the current time.""" self.active = True self.start_time = pygame.time.get_ticks() def deactivate(self) -> None: + """Deactivates the timer, resetting the start time to 0.""" self.active = False self.start_time = 0 def update(self) -> None: + """ + Updates the timer, checking if it has completed its duration. + + If completed, executes the callback function (if provided) and either deactivates or reactivates the timer. + """ current_time = pygame.time.get_ticks() if current_time - self.start_time >= self.duration and self.active: if self.func and self.start_time: @@ -33,6 +56,15 @@ class Timer: class Timers(NamedTuple): + """ + NamedTuple for grouping different timers. + + Args and Attributes: + vertical: Timer for vertical movement. + horizontal: Timer for horizontal movement. + rotation: Timer for rotation. + """ + vertical: Timer horizontal: Timer rotation: Timer diff --git a/src/utils/config.py b/src/utils/config.py index cdfeb25..80ad0db 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -21,7 +21,7 @@ class Game: size: Size = Size(columns * cell.width, rows * cell.width) pos: Vec2 = Vec2(padding, padding) offset: Vec2 = Vec2(columns // 2, -1) - initial_speed: int = 400 + initial_speed: float | int = 400 movment_delay: int = 200 rotation_delay: int = 200 score: dict[int, int] = {1: 40, 2: 100, 3: 300, 4: 1200} diff --git a/src/utils/figure.py b/src/utils/figure.py index 5fba86b..29e6d2c 100644 --- a/src/utils/figure.py +++ b/src/utils/figure.py @@ -18,7 +18,10 @@ class FigureConfig(NamedTuple): def _load_image(filename: str) -> pygame.Surface: - return pygame.image.load(BASE_PATH / "assets" / "figures" / filename) + return pygame.image.load( + BASE_PATH / "assets" / "figures" / filename + ) # TODO: add `.convert_alpha()`` + # TODO: change colors of images class Figure(Enum):