diff --git a/main.py b/main.py index cf0ea0f..1feab9b 100755 --- a/main.py +++ b/main.py @@ -67,10 +67,10 @@ def main(args: argparse.ArgumentParser) -> None: if args.train is not None: ai.log.debug("Training the AI") # ai.train(*args.train) - game.Main(GameMode.AI_TRAINING).run() + game.Game(GameMode.AI_TRAINING).run() else: game.log.debug("Running the game") - game.Main(GameMode.PLAYER).run() + game.Game(GameMode.PLAYER).run() if __name__ == "__main__": diff --git a/src/game/__init__.py b/src/game/__init__.py index 5029dd7..d5c65ab 100644 --- a/src/game/__init__.py +++ b/src/game/__init__.py @@ -1,20 +1,9 @@ -from .block import Block -from .game import Game from .log import log -from .main import Main -from .preview import Preview -from .score import Score -from .tetromino import Tetromino -from .timer import Timer, Timers +from .screens import Game, Menu, Tetris __all__ = [ "log", "Main", "Game", - "Score", - "Block", - "Tetromino", "Preview", - "Timer", - "Timers", ] diff --git a/src/game/screens/__init__.py b/src/game/screens/__init__.py new file mode 100644 index 0000000..cce636f --- /dev/null +++ b/src/game/screens/__init__.py @@ -0,0 +1,7 @@ +from .game import Game +from .menu import Menu +from .preview import Preview +from .score import Score +from .tetris import Tetris + +__all__ = ["Tetris", "Game", "Preview", "Score", "Menu"] diff --git a/src/game/screens/base.py b/src/game/screens/base.py new file mode 100644 index 0000000..3bbd40e --- /dev/null +++ b/src/game/screens/base.py @@ -0,0 +1,57 @@ +from abc import ABC, ABCMeta, abstractmethod + +import pygame + + +class BaseScreen(ABC, metaclass=ABCMeta): + """Base screen class.""" + + @abstractmethod + def update(self, *args, **kwargs) -> None: + """Update the screen.""" + + @abstractmethod + def draw(self, *args, **kwargs) -> None: + """Draw the screen.""" + + @abstractmethod + def run(self, *args, **kwargs) -> None: + """Run the screen.""" + + +class SceenElement(ABC, metaclass=ABCMeta): + @abstractmethod + def _draw_background(self) -> None: + """Draw the background.""" + + @abstractmethod + def _draw_border(self) -> None: + """Draw the border.""" + + @abstractmethod + def _initialize_surface(self) -> None: + """Initialize the surface.""" + + @abstractmethod + def _initialize_rect(self) -> None: + """Initialize the rectangle.""" + + @abstractmethod + def _update_diplaysurface(self) -> None: + """Update the display surface.""" + + +class TextScreen(ABC, metaclass=ABCMeta): + """Base screen class for text.""" + + @abstractmethod + def _initialize_font(self) -> None: + """Initialize the font.""" + + @abstractmethod + def _draw_text(self) -> None: + """Draw the text on the surface.""" + + @abstractmethod + def _display_text(self, *args, **kwargs) -> None: + """Display the text.""" diff --git a/src/game/main.py b/src/game/screens/game.py similarity index 90% rename from src/game/main.py rename to src/game/screens/game.py index 3dd5acc..14d0656 100644 --- a/src/game/main.py +++ b/src/game/screens/game.py @@ -3,16 +3,18 @@ import sys import pygame from utils import CONFIG, Figure, GameMode -from .game import Game -from .log import log +from game.log import log + +from .base import BaseScreen from .preview import Preview from .score import Score +from .tetris import Tetris from .tetromino import Tetromino -class Main: +class Game(BaseScreen): """ - Main class for the game. + Game class. Attributes: display_surface: Pygame display surface. @@ -46,11 +48,12 @@ class Main: self.draw() self.handle_events() - self.game.run() + self.tetris.run() self.score.run() - self.preview.run(self.next_figure) + self.preview.update(self.next_figure) + self.preview.run() - pygame.display.update() + self.draw() self.clock.tick(CONFIG.fps) def handle_events(self) -> None: @@ -71,14 +74,14 @@ class Main: """Initialize game-related components.""" self.next_figure: Figure = self._generate_next_figure() - self.game = Game(self._get_next_figure, self._update_score) + self.tetris = Tetris(self._get_next_figure, self._update_score) self.score = Score() self.preview = Preview() def mute(self) -> None: """Mute the game.""" self.music.set_volume(0) - self.game.mute() + self.tetris.mute() def _update_score(self, lines: int, score: int, level: int) -> None: """ diff --git a/src/game/screens/menu.py b/src/game/screens/menu.py new file mode 100644 index 0000000..bb46071 --- /dev/null +++ b/src/game/screens/menu.py @@ -0,0 +1,7 @@ +from game.log import log + +from .base import BaseScreen + + +class Menu(BaseScreen): + pass diff --git a/src/game/preview.py b/src/game/screens/preview.py similarity index 73% rename from src/game/preview.py rename to src/game/screens/preview.py index 982b2d5..fe6e56d 100644 --- a/src/game/preview.py +++ b/src/game/screens/preview.py @@ -1,8 +1,10 @@ import pygame from utils import CONFIG, Figure, Size +from .base import BaseScreen, SceenElement -class Preview: + +class Preview(BaseScreen, SceenElement): """ Class representing the preview of upcoming figures on the sidebar. @@ -17,15 +19,25 @@ class Preview: self._initialize_surface() self._initialize_rect() - def run(self, next_figure: Figure) -> None: + def run(self) -> None: + """Run the preview by updating the display and drawing next figure.""" + self.draw() + + def update(self, next_figure: Figure) -> None: """ - Run the preview by updating the display and drawing next figures. + Update the preview information. Args: - next_figures (list[Figure]): List of upcoming figures. + next_figures: Next figure. """ - self.dispaly_surface.blit(self.surface, self.rect) - self._draw_preview(next_figure) + self.next_figure = next_figure + + def draw(self) -> None: + """Draw the preview on the preview surface.""" + self._update_diplaysurface() + self._draw_background() + self._draw_border() + self._draw_figure() def _draw_border(self) -> None: """Draw the border around the preview surface.""" @@ -37,7 +49,7 @@ class Preview: CONFIG.game.border_radius, ) - def _draw_figure(self, figure: Figure) -> None: + def _draw_figure(self) -> None: """ Draw a single upcoming figure on the preview surface. @@ -45,23 +57,12 @@ class Preview: figure (Figure): The upcoming figure to draw. idx (int): Index of the figure in the list. """ - figure_surface = figure.value.image + figure_surface = self.next_figure.value.image x = self.surface.get_width() / 2 y = self.surface.get_height() / 2 rect = figure_surface.get_rect(center=(x, y)) self.surface.blit(figure_surface, rect) - def _draw_preview(self, next_figure: Figure) -> None: - """ - Draw the preview with the background, border, and next figure. - - Args: - next_figures (list[Figure]): List of upcoming figures. - """ - self._draw_background() - self._draw_border() - self._draw_figure(next_figure) - def _draw_background(self) -> None: """Draw the background of the preview.""" self.surface.fill(CONFIG.colors.bg_sidebar) @@ -79,3 +80,7 @@ class Preview: CONFIG.window.padding, ) ) + + def _update_diplaysurface(self) -> None: + """Update the display surface.""" + self.dispaly_surface.blit(self.surface, self.rect) diff --git a/src/game/score.py b/src/game/screens/score.py similarity index 91% rename from src/game/score.py rename to src/game/screens/score.py index 3e67833..5b7964a 100644 --- a/src/game/score.py +++ b/src/game/screens/score.py @@ -1,8 +1,10 @@ import pygame from utils import CONFIG, Size +from .base import BaseScreen, SceenElement, TextScreen -class Score: + +class Score(BaseScreen, SceenElement, TextScreen): """ Class representing the score on the sidebar. @@ -24,7 +26,7 @@ class Score: def run(self) -> None: """Display the score on the game surface.""" - self.dispaly_surface.blit(self.surface, self.rect) + self._update_diplaysurface() self.draw() def update(self, lines: int, score: int, level: int) -> None: @@ -55,7 +57,7 @@ class Score: 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: + def _display_text(self, text: tuple[str, int], pos: tuple[float, float]) -> None: """ Display a single text element on the score surface. @@ -101,3 +103,7 @@ class Score: def _initialize_increment_height(self) -> None: """Initialize the increment height for positioning text elements.""" self.increment_height = self.surface.get_height() / 3 + + def _update_diplaysurface(self) -> None: + """Update the display surface.""" + self.dispaly_surface.blit(self.surface, self.rect) diff --git a/src/game/game.py b/src/game/screens/tetris.py similarity index 94% rename from src/game/game.py rename to src/game/screens/tetris.py index 3014894..dc5e406 100644 --- a/src/game/game.py +++ b/src/game/screens/tetris.py @@ -1,16 +1,18 @@ -from typing import Callable, Optional +from typing import Any, Callable, Optional import numpy as np import pygame from utils import CONFIG, Direction, Field, Figure, Rotation -from .block import Block -from .log import log -from .tetromino import Tetromino -from .timer import Timer, Timers +from game.log import log +from game.sprites.block import Block +from game.sprites.tetromino import Tetromino +from game.timer import Timer, Timers + +from .base import BaseScreen -class Game: +class Tetris(BaseScreen): """ Game class for managing the game state. @@ -79,7 +81,7 @@ class Game: def handle_event(self) -> None: """Handle player input events.""" - keys: list[bool] = pygame.key.get_pressed() + keys: pygame.key.ScancodeWrapper = pygame.key.get_pressed() self._handle_movement_keys(keys) self._handle_rotation_keys(keys) @@ -215,9 +217,11 @@ class Game: for block in self.sprites: self.field[int(block.pos.y), int(block.pos.x)] = block - def _generate_empty_field(self) -> np.ndarray: + def _generate_empty_field(self) -> np.ndarray[Field, Any]: """Generate an empty game field.""" - return np.full((CONFIG.game.rows, CONFIG.game.columns), None, dtype=Field) + return np.full( + (CONFIG.game.rows, CONFIG.game.columns), Field.EMPTY, dtype=Field + ) def _calculate_score(self, rows_deleted: int) -> None: """Calculate and update the game score.""" @@ -308,7 +312,7 @@ class Game: """Fill the game surface with background color.""" self.surface.fill(CONFIG.colors.bg_float) - def _handle_movement_keys(self, keys: list[bool]) -> None: + def _handle_movement_keys(self, keys: pygame.key.ScancodeWrapper) -> None: """ Handle movement keys. @@ -326,7 +330,7 @@ class Game: self.move_right() self.timers.horizontal.activate() - def _handle_rotation_keys(self, keys: list[bool]) -> None: + def _handle_rotation_keys(self, keys: pygame.key.ScancodeWrapper) -> None: """ Handle rotation keys. @@ -353,7 +357,7 @@ class Game: self.rotate_reverse() self.timers.rotation.activate() - def _handle_down_key(self, keys: list[bool]) -> None: + def _handle_down_key(self, keys: pygame.key.ScancodeWrapper) -> 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: @@ -364,7 +368,7 @@ class Game: self.down_pressed = False self.timers.vertical.duration = self.initial_block_speed - def _handle_drop_key(self, keys: list[bool]) -> None: + def _handle_drop_key(self, keys: pygame.key.ScancodeWrapper) -> None: """Handle the drop key [K_SPACE].""" drop_keys = keys[pygame.K_SPACE] diff --git a/src/game/sprites/__init__.py b/src/game/sprites/__init__.py new file mode 100644 index 0000000..2ac237a --- /dev/null +++ b/src/game/sprites/__init__.py @@ -0,0 +1,4 @@ +from .block import Block +from .tetromino import Tetromino + +__all__ = ["Block", "Tetromino"] diff --git a/src/game/block.py b/src/game/sprites/block.py similarity index 82% rename from src/game/block.py rename to src/game/sprites/block.py index 56c5b41..4fddf4b 100644 --- a/src/game/block.py +++ b/src/game/sprites/block.py @@ -1,3 +1,5 @@ +from typing import Any + import numpy as np import pygame from utils import CONFIG, Rotation, Size @@ -22,7 +24,7 @@ class Block(pygame.sprite.Sprite): self, /, *, - group: pygame.sprite.Group, + group: pygame.sprite.Group[Any], pos: pygame.Vector2, color: str, ) -> None: @@ -32,9 +34,13 @@ class Block(pygame.sprite.Sprite): def update(self) -> None: """Updates the block's position on the screen.""" - self.rect.topleft = self.pos * CONFIG.game.cell.width + if self.rect: + self.rect.topleft = ( + self.pos.x * CONFIG.game.cell.width, + self.pos.y * CONFIG.game.cell.width, + ) - def vertical_collision(self, x: int, field: np.ndarray) -> bool: + def vertical_collision(self, x: int, field: np.ndarray[int, Any]) -> bool: """ Checks for vertical collision with the game field. @@ -47,7 +53,7 @@ class Block(pygame.sprite.Sprite): """ return not 0 <= x < CONFIG.game.columns or field[int(self.pos.y), x] - def horizontal_collision(self, y: int, field: np.ndarray) -> bool: + def horizontal_collision(self, y: int, field: np.ndarray[int, Any]) -> bool: """ Checks for horizontal collision with the game field. @@ -91,4 +97,5 @@ class Block(pygame.sprite.Sprite): 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) + if self.image: + self.rect = self.image.get_rect(topleft=self.pos * CONFIG.game.cell.width) diff --git a/src/game/tetromino.py b/src/game/sprites/tetromino.py similarity index 96% rename from src/game/tetromino.py rename to src/game/sprites/tetromino.py index 0de04b4..71b4cb6 100644 --- a/src/game/tetromino.py +++ b/src/game/sprites/tetromino.py @@ -1,4 +1,4 @@ -from typing import Callable, Optional +from typing import Any, Callable, Optional import numpy as np import pygame @@ -29,9 +29,9 @@ class Tetromino: def __init__( self, - group: pygame.sprite.Group, + group: pygame.sprite.Group[Any], create_new: Callable[[], None], - field: np.ndarray, + field: np.ndarray[int, Any], shape: Optional[Figure] = None, ) -> None: self.figure: Figure = self._generate_figure(shape) @@ -156,7 +156,7 @@ class Tetromino: for pos in new_positions ) - def _initialize_blocks(self, group: pygame.sprite.Group) -> list[Block]: + def _initialize_blocks(self, group: pygame.sprite.Group[Any]) -> list[Block]: """ Initializes Tetromino blocks.