diff --git a/main.py b/main.py index 1feab9b..7103c7b 100755 --- a/main.py +++ b/main.py @@ -61,16 +61,17 @@ def main(args: argparse.ArgumentParser) -> None: elif args.verbose: CONFIG.log_level = "info" - import ai + # import ai import game if args.train is not None: - ai.log.debug("Training the AI") - # ai.train(*args.train) - game.Game(GameMode.AI_TRAINING).run() + # ai.log.debug("Training the AI") + # # ai.train(*args.train) + # game.Menu(GameMode.AI_TRAINING).run() + pass else: game.log.debug("Running the game") - game.Game(GameMode.PLAYER).run() + game.Main(GameMode.PLAYER).run() if __name__ == "__main__": diff --git a/src/game/__init__.py b/src/game/__init__.py index d5c65ab..4fab904 100644 --- a/src/game/__init__.py +++ b/src/game/__init__.py @@ -1,5 +1,5 @@ from .log import log -from .screens import Game, Menu, Tetris +from .screens import Game, Main, Tetris __all__ = [ "log", diff --git a/src/game/screens/__init__.py b/src/game/screens/__init__.py index cce636f..2980d96 100644 --- a/src/game/screens/__init__.py +++ b/src/game/screens/__init__.py @@ -1,7 +1,7 @@ from .game import Game -from .menu import Menu +from .main import Main from .preview import Preview from .score import Score from .tetris import Tetris -__all__ = ["Tetris", "Game", "Preview", "Score", "Menu"] +__all__ = ["Tetris", "Game", "Preview", "Score", "Main"] diff --git a/src/game/screens/base.py b/src/game/screens/base.py index 3bbd40e..4407392 100644 --- a/src/game/screens/base.py +++ b/src/game/screens/base.py @@ -1,7 +1,5 @@ from abc import ABC, ABCMeta, abstractmethod -import pygame - class BaseScreen(ABC, metaclass=ABCMeta): """Base screen class.""" @@ -37,7 +35,7 @@ class SceenElement(ABC, metaclass=ABCMeta): """Initialize the rectangle.""" @abstractmethod - def _update_diplaysurface(self) -> None: + def _update_display_surface(self) -> None: """Update the display surface.""" diff --git a/src/game/screens/button.py b/src/game/screens/button.py new file mode 100644 index 0000000..197ac3e --- /dev/null +++ b/src/game/screens/button.py @@ -0,0 +1,20 @@ +from abc import ABC, ABCMeta, abstractmethod +from typing import Callable, Optional + +import pygame + + +class Button(ABC, metaclass=ABCMeta): + """Base button class.""" + + def __init__(self, text: str, action: Optional[Callable[[], None]]) -> None: + self.action = action + self.text = text + + @abstractmethod + def on_click(self) -> None: + """Handle click event.""" + + @abstractmethod + def on_hover(self) -> None: + """Handle hover event.""" diff --git a/src/game/screens/game.py b/src/game/screens/game.py index 14d0656..172400b 100644 --- a/src/game/screens/game.py +++ b/src/game/screens/game.py @@ -4,12 +4,12 @@ import pygame from utils import CONFIG, Figure, GameMode from game.log import log +from game.sprites.tetromino import Tetromino from .base import BaseScreen from .preview import Preview from .score import Score from .tetris import Tetris -from .tetromino import Tetromino class Game(BaseScreen): @@ -27,26 +27,20 @@ class Game(BaseScreen): music: Pygame music that plays in the background. """ - def __init__(self, mode: GameMode) -> None: - log.info("Initializing the game") - self.game_mode = mode - self._initialize_pygeme() + def __init__(self) -> None: self._initialize_game_components() self._start_background_music() def draw(self) -> None: """Update the display.""" - pygame.display.update() + + def update(self) -> None: + pass def run(self) -> None: - """Run the main game loop.""" - while True: - self.run_game_loop() - - def run_game_loop(self) -> None: """Run a single iteration of the game loop.""" self.draw() - self.handle_events() + self.handle_events() # FIX: self.tetris.run() self.score.run() @@ -56,33 +50,20 @@ class Game(BaseScreen): self.draw() self.clock.tick(CONFIG.fps) - def handle_events(self) -> None: - """Handle Pygame events.""" - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self.exit() - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_q: - self.exit() - - def exit(self) -> None: - """Exit the game.""" - pygame.quit() - sys.exit() + def mute(self) -> None: + """Mute the game.""" + self.music.set_volume(0) + self.tetris.mute() def _initialize_game_components(self) -> None: """Initialize game-related components.""" + self.clock = pygame.time.Clock() self.next_figure: Figure = self._generate_next_figure() 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.tetris.mute() - def _update_score(self, lines: int, score: int, level: int) -> None: """ Update the game score. @@ -114,14 +95,6 @@ class Game(BaseScreen): self.next_figure = self._generate_next_figure() return next_figure - 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) - self.display_surface.fill(CONFIG.colors.bg) - self.clock = pygame.time.Clock() - def _start_background_music(self) -> None: """Start playing background music.""" self.music = pygame.mixer.Sound(CONFIG.music.background) diff --git a/src/game/screens/main.py b/src/game/screens/main.py new file mode 100644 index 0000000..9e82109 --- /dev/null +++ b/src/game/screens/main.py @@ -0,0 +1,116 @@ +import sys + +import pygame +from utils import CONFIG, GameMode + +from game.log import log + +from .base import BaseScreen, SceenElement, TextScreen +from .game import Game +from .menu_button import MenuButton + + +class Main(BaseScreen, SceenElement, TextScreen): + def __init__(self, mode: GameMode) -> None: + log.info("Initializing the game") + self._initialize_pygame() + self._initialize_surface() + self._initialize_rect() + self._initialize_font() + self._set_buttons() + self._initialize_increment_height() + self.game_mode = mode # TODO: use this + + def draw(self) -> None: + """Update the display.""" + self._draw_background() + self._draw_text() + self._draw_border() + pygame.display.update() + + def update(self) -> None: + pass + + def handle_events(self) -> None: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.exit() + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + self.exit() + + def run(self) -> None: + while True: + self.draw() + self.handle_events() + + def exit(self) -> None: + """Exit the game.""" + pygame.quit() + sys.exit() + + def _set_buttons(self) -> None: + self.buttons: list[MenuButton] = [ + MenuButton("Play", None), + MenuButton("AI", None), + MenuButton("Settings", None), + MenuButton("Quit", self.exit), + ] + + def _initialize_pygame(self) -> None: + """Initialize Pygame and set up the display.""" + pygame.init() + pygame.display.set_caption(CONFIG.window.title) + + def _draw_background(self) -> None: + self.display_surface.fill(CONFIG.colors.bg) + + def _draw_border(self) -> None: + pass + + def _initialize_surface(self) -> None: + self.display_surface = pygame.display.set_mode(CONFIG.window.size) + + def _initialize_rect(self) -> None: + """Initialize the rectangle.""" + self.rect = self.display_surface.get_rect(topright=(0, 0)) + + def _update_display_surface(self) -> None: + """Do nothing. Not needed in this class.""" + + def _initialize_increment_height(self) -> None: + """Initialize the increment height for positioning text elements/buttons.""" + self.increment_height = ( + self.display_surface.get_height() - CONFIG.window.size.height / 2 + ) / len(self.buttons) + + def _display_text(self, text: str, pos: tuple[float, float]) -> None: + """ + Display a single text element on the score surface. + + Args: + text: A tuple containing the label and value of the text element. + pos: The position (x, y) where the text should be displayed. + """ + text_surface = self.font.render(text, True, CONFIG.colors.fg) + text_rect = text_surface.get_rect(center=pos) + self.display_surface.blit(text_surface, text_rect) + + 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 _draw_text(self) -> None: + """Draw the text and buttons on the surface.""" + + x, y = self.display_surface.get_width() / 2, 100 + self._display_text("Tetris", (x, y)) + + for idx, button in enumerate(self.buttons): + x = self.display_surface.get_width() / 2 + y = ( + self.increment_height / 4 + + idx * self.increment_height + + CONFIG.window.size.height / 4 + ) # TODO: tweak a bit more + button.draw(self.display_surface, (x, y)) diff --git a/src/game/screens/menu.py b/src/game/screens/menu.py deleted file mode 100644 index bb46071..0000000 --- a/src/game/screens/menu.py +++ /dev/null @@ -1,7 +0,0 @@ -from game.log import log - -from .base import BaseScreen - - -class Menu(BaseScreen): - pass diff --git a/src/game/screens/menu_button.py b/src/game/screens/menu_button.py new file mode 100644 index 0000000..5d745ca --- /dev/null +++ b/src/game/screens/menu_button.py @@ -0,0 +1,88 @@ +from typing import Callable, Optional + +import pygame +from utils import CONFIG + +from .base import BaseScreen, SceenElement, TextScreen +from .button import Button + + +class MenuButton(Button, BaseScreen, SceenElement, TextScreen): + def __init__(self, text: str, action: Optional[Callable[[], None]]) -> None: + super().__init__(text, action) + self._initialize_surface() + self._initialize_font() + + def on_click(self) -> None: + """Handle click event.""" + if self.action: + self.action() + + def on_hover(self) -> None: + """Handle hover event.""" + self._draw_border() + + def run(self) -> None: + """Display the button on the game surface.""" + self.draw() + + def update(self) -> None: + """Update the button.""" + pass + + def draw(self, surface: pygame.Surface, pos: tuple[float, float]) -> None: + """Draw the button on the button surface.""" + self._initialize_rect(pos) + self._update_display_surface() + self._draw_background() + self._draw_text() + self._draw_border() + + def _initialize_surface(self) -> None: + """Initialize the button surface.""" + self.surface = pygame.Surface(CONFIG.window.button.size) + self.display_surface = pygame.display.get_surface() + + def _initialize_rect(self, pos: tuple[float, float]) -> None: + """Initialize the button rectangle.""" + self.rect = self.surface.get_rect(center=pos) + + def _draw_text(self) -> None: + """Draw the text on the text surface.""" + x = self.surface.get_width() / 2 + y = self.surface.get_height() / 2 + self._display_text(self.text, (x, y)) + + def _display_text(self, text: str, pos: tuple[float, float]) -> None: + """ + Display a single text element on the button surface. + + Args: + text: The text to be displayed. + pos: The position (x, y) where the text should be displayed. + """ + text_surface = self.font.render(text, True, CONFIG.colors.fg_sidebar) + text_rect = text_surface.get_rect(center=pos) + self.surface.blit(text_surface, text_rect) + + 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 _draw_background(self) -> None: + """Fill the background of the button.""" + self.surface.fill(CONFIG.colors.bg_sidebar) + + def _draw_border(self) -> None: + """Draw the border of the button.""" + pygame.draw.rect( + self.display_surface, + CONFIG.colors.border_highlight, + self.rect, + CONFIG.game.line_width * 2, + CONFIG.game.border_radius, + ) + + def _update_display_surface(self) -> None: + """Update the display surface.""" + self.display_surface.blit(self.surface, self.rect) diff --git a/src/game/screens/preview.py b/src/game/screens/preview.py index fe6e56d..e0b98a7 100644 --- a/src/game/screens/preview.py +++ b/src/game/screens/preview.py @@ -11,7 +11,7 @@ class Preview(BaseScreen, SceenElement): Attributes: surface: Pygame surface representing the preview. rect: Pygame rectangle representing the preview. - dispaly_surface: Pygame display surface. + display_surface: Pygame display surface. increment_height: Height of each figure in the preview. """ @@ -34,7 +34,7 @@ class Preview(BaseScreen, SceenElement): def draw(self) -> None: """Draw the preview on the preview surface.""" - self._update_diplaysurface() + self._update_display_surface() self._draw_background() self._draw_border() self._draw_figure() @@ -42,7 +42,7 @@ class Preview(BaseScreen, SceenElement): def _draw_border(self) -> None: """Draw the border around the preview surface.""" pygame.draw.rect( - self.dispaly_surface, + self.display_surface, CONFIG.colors.border_highlight, self.rect, CONFIG.game.line_width * 2, @@ -70,7 +70,7 @@ class Preview(BaseScreen, SceenElement): def _initialize_surface(self) -> None: """Initialize the preview surface.""" self.surface = pygame.Surface(CONFIG.sidebar.preview) - self.dispaly_surface = pygame.display.get_surface() + self.display_surface = pygame.display.get_surface() def _initialize_rect(self) -> None: """Initialize the preview rectangle.""" @@ -81,6 +81,6 @@ class Preview(BaseScreen, SceenElement): ) ) - def _update_diplaysurface(self) -> None: + def _update_display_surface(self) -> None: """Update the display surface.""" - self.dispaly_surface.blit(self.surface, self.rect) + self.display_surface.blit(self.surface, self.rect) diff --git a/src/game/screens/score.py b/src/game/screens/score.py index 5b7964a..3c17114 100644 --- a/src/game/screens/score.py +++ b/src/game/screens/score.py @@ -10,7 +10,7 @@ class Score(BaseScreen, SceenElement, TextScreen): Attributes: surface: Pygame surface representing the score. - dispaly_surface: Pygame display surface. + display_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. @@ -26,7 +26,6 @@ class Score(BaseScreen, SceenElement, TextScreen): def run(self) -> None: """Display the score on the game surface.""" - self._update_diplaysurface() self.draw() def update(self, lines: int, score: int, level: int) -> None: @@ -46,6 +45,7 @@ class Score(BaseScreen, SceenElement, TextScreen): def draw(self) -> None: """Draw the score on the score surface.""" + self._update_display_surface() self._draw_background() self._draw_text() self._draw_border() @@ -62,8 +62,8 @@ class Score(BaseScreen, SceenElement, TextScreen): 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: A tuple containing the label and value of the text element. + pos: 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 @@ -74,7 +74,7 @@ class Score(BaseScreen, SceenElement, TextScreen): def _draw_border(self) -> None: """Draw the border of the score surface.""" pygame.draw.rect( - self.dispaly_surface, + self.display_surface, CONFIG.colors.border_highlight, self.rect, CONFIG.game.line_width * 2, @@ -88,12 +88,12 @@ class Score(BaseScreen, SceenElement, TextScreen): def _initialize_surface(self) -> None: """Initialize the score surface.""" self.surface = pygame.Surface(CONFIG.sidebar.score) - self.dispaly_surface = pygame.display.get_surface() + self.display_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) + bottomright=CONFIG.window.size - CONFIG.window.padding ) def _initialize_font(self) -> None: @@ -104,6 +104,6 @@ class Score(BaseScreen, SceenElement, TextScreen): """Initialize the increment height for positioning text elements.""" self.increment_height = self.surface.get_height() / 3 - def _update_diplaysurface(self) -> None: + def _update_display_surface(self) -> None: """Update the display surface.""" - self.dispaly_surface.blit(self.surface, self.rect) + self.display_surface.blit(self.surface, self.rect) diff --git a/src/game/screens/tetris.py b/src/game/screens/tetris.py index dc5e406..c629eb4 100644 --- a/src/game/screens/tetris.py +++ b/src/game/screens/tetris.py @@ -9,7 +9,7 @@ from game.sprites.block import Block from game.sprites.tetromino import Tetromino from game.timer import Timer, Timers -from .base import BaseScreen +from .base import BaseScreen, SceenElement class Tetris(BaseScreen): @@ -46,7 +46,7 @@ class Tetris(BaseScreen): get_next_figure: Callable[[], Figure], update_score: Callable[[int, int, int], None], ) -> None: - self._initialize_game_surface() + self._initialize_surface() self._initialize_sprites() self.get_next_figure = get_next_figure @@ -70,7 +70,7 @@ class Tetris(BaseScreen): def draw(self) -> None: """Draw the game surface and its components.""" self.update() - self._fill_game_surface() + self._draw_background() self.sprites.draw(self.surface) self._draw_border() self._draw_grid() @@ -249,10 +249,13 @@ class Tetris(BaseScreen): self._draw_border() self._draw_grid() - def _initialize_game_surface(self) -> None: + def _initialize_surface(self) -> None: """Initialize the game surface.""" self.surface = pygame.Surface(CONFIG.game.size) self.dispaly_surface = pygame.display.get_surface() + + def _initialize_rect(self) -> None: + """Initialize the rectangle.""" self.rect = self.surface.get_rect(topleft=CONFIG.game.pos) def _initialize_sprites(self) -> None: @@ -308,7 +311,7 @@ class Tetris(BaseScreen): """Update the display surface.""" self.dispaly_surface.blit(self.surface, CONFIG.game.pos) - def _fill_game_surface(self) -> None: + def _draw_background(self) -> None: """Fill the game surface with background color.""" self.surface.fill(CONFIG.colors.bg_float) diff --git a/src/game/sprites/block.py b/src/game/sprites/block.py index 4fddf4b..d7edd98 100644 --- a/src/game/sprites/block.py +++ b/src/game/sprites/block.py @@ -2,7 +2,7 @@ from typing import Any import numpy as np import pygame -from utils import CONFIG, Rotation, Size +from utils import CONFIG, Field, Rotation, Size class Block(pygame.sprite.Sprite): @@ -24,7 +24,7 @@ class Block(pygame.sprite.Sprite): self, /, *, - group: pygame.sprite.Group[Any], + group: pygame.sprite.Group, pos: pygame.Vector2, color: str, ) -> None: @@ -40,7 +40,7 @@ class Block(pygame.sprite.Sprite): self.pos.y * CONFIG.game.cell.width, ) - def vertical_collision(self, x: int, field: np.ndarray[int, Any]) -> bool: + def vertical_collision(self, x: int, field: np.ndarray[Field, Any]) -> bool: """ Checks for vertical collision with the game field. @@ -53,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[int, Any]) -> bool: + def horizontal_collision(self, y: int, field: np.ndarray[Field, Any]) -> bool: """ Checks for horizontal collision with the game field. diff --git a/src/game/sprites/tetromino.py b/src/game/sprites/tetromino.py index 71b4cb6..227cb79 100644 --- a/src/game/sprites/tetromino.py +++ b/src/game/sprites/tetromino.py @@ -2,10 +2,11 @@ from typing import Any, Callable, Optional import numpy as np import pygame -from utils import CONFIG, Direction, Figure, Rotation, Size +from utils import CONFIG, Direction, Field, Figure, Rotation, Size + +from game.log import log from .block import Block -from .log import log class Tetromino: @@ -29,9 +30,9 @@ class Tetromino: def __init__( self, - group: pygame.sprite.Group[Any], + group: pygame.sprite.Group, create_new: Callable[[], None], - field: np.ndarray[int, Any], + field: np.ndarray[Field, Any], shape: Optional[Figure] = None, ) -> None: self.figure: Figure = self._generate_figure(shape) @@ -156,7 +157,7 @@ class Tetromino: for pos in new_positions ) - def _initialize_blocks(self, group: pygame.sprite.Group[Any]) -> list[Block]: + def _initialize_blocks(self, group: pygame.sprite.Group) -> list[Block]: """ Initializes Tetromino blocks. diff --git a/src/utils/config.py b/src/utils/config.py index dd8bfd8..e606eaa 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -41,6 +41,11 @@ class Font: size: int = 32 +@define +class Button: + size: Size = Size(200, 50) + + @define class Window: title: str = "Tetris" @@ -49,6 +54,7 @@ class Window: Game().size.width + SideBar().size.width + padding * 3, Game().size.height + padding * 2, ) + button: Button = Button() @define diff --git a/src/utils/size.py b/src/utils/size.py index 50114ac..9cdacf0 100644 --- a/src/utils/size.py +++ b/src/utils/size.py @@ -5,12 +5,7 @@ class Size(NamedTuple): width: int | float height: int | float - def add(self, other: Union["Size", int, float]) -> "Size": - if isinstance(other, Size): - return Size(self.width + other.width, self.height + other.height) - return Size(self.width + other, self.height + other) - - def sub(self, other: Union["Size", int, float]) -> "Size": + def __sub__(self, other: Union["Size", int, float]) -> "Size": if isinstance(other, Size): return Size(self.width - other.width, self.height - other.height) return Size(self.width - other, self.height - other)