diff --git a/src/game/screens/game.py b/src/game/screens/game.py index db11dc0..1dcd05d 100644 --- a/src/game/screens/game.py +++ b/src/game/screens/game.py @@ -1,15 +1,17 @@ from typing import Any import pygame +from loguru import logger from utils import CONFIG, Figure, GameMode -from .base import BaseScreen +from .base import BaseScreen, SceenElement +from .pause import Pause from .preview import Preview from .score import Score from .tetris import Tetris -class Game(BaseScreen): +class Game(BaseScreen, SceenElement): """ Game class. @@ -32,15 +34,16 @@ class Game(BaseScreen): def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None: self.game_mode = game_mode self.settings = settings + self._initialize_surface() + self._initialize_rect() self._initialize_game_components() self._start_background_music() + self.paused = False def draw(self) -> None: - """ - Raises: - NotImplementedError: Not implemented yet. - """ - raise NotImplementedError + """Draw the score on the score surface.""" + self._update_display_surface() + self._draw_background() def update(self) -> None: """ @@ -51,12 +54,15 @@ class Game(BaseScreen): def run(self) -> None: """Run a single iteration of the game loop.""" - + self.draw() self.tetris.run() self.score.run() self.preview.update(self.next_figure) self.preview.run() + if self.paused: + self.pause_screen.draw() + self.clock.tick(CONFIG.game.fps) def mute(self) -> None: @@ -64,6 +70,19 @@ class Game(BaseScreen): self.music.set_volume(0) self.tetris.mute() + def pause(self) -> None: + """Pause the game.""" + if self.paused: + logger.debug("Unpause") + self.paused = False + self.tetris.unfreeze() + self.music.play(-1, fade_ms=100) + else: + logger.debug("Pause") + self.paused = True + self.tetris.freeze() + self.music.fadeout(100) + def _initialize_game_components(self) -> None: """Initialize game-related components.""" self.clock = pygame.time.Clock() @@ -72,6 +91,7 @@ class Game(BaseScreen): self.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode, self.settings) self.score = Score(self.game_mode) self.preview = Preview() + self.pause_screen = Pause() def _update_score(self, lines: int, score: int, level: int) -> None: """ @@ -110,3 +130,28 @@ class Game(BaseScreen): self.music = pygame.mixer.Sound(CONFIG.music.background) self.music.set_volume(self.settings["Volume"]["Music"]["level"]) self.music.play(-1) + + def _initialize_surface(self) -> None: + """Initialize the pause screen surface.""" + self.surface = pygame.Surface(CONFIG.window.size) + self.display_surface = pygame.display.get_surface() + + def _initialize_rect(self) -> None: + """Initialize the score rectangle.""" + self.rect = self.surface.get_rect(topleft=(0, 0)) + + def _draw_background(self) -> None: + """Draw the background.""" + self.surface.fill(CONFIG.colors.bg) + + def _update_display_surface(self) -> None: + """Update the display surface.""" + self.display_surface.blit(self.surface, self.rect) + + def _draw_border(self) -> None: + """Draw the border. + + Raises: + NotImplementedError: Not implemented yet. + """ + raise NotImplementedError diff --git a/src/game/screens/main.py b/src/game/screens/main.py index 8d38995..e6f0e0e 100644 --- a/src/game/screens/main.py +++ b/src/game/screens/main.py @@ -8,6 +8,7 @@ from utils import CONFIG, GameMode, read_settings from .base import BaseScreen, SceenElement, TextScreen from .button import Button from .game import Game +from .tetris import get_keys class Main(BaseScreen, SceenElement, TextScreen): @@ -45,8 +46,10 @@ class Main(BaseScreen, SceenElement, TextScreen): if event.type == pygame.QUIT: self.exit() elif event.type == pygame.KEYDOWN: - if event.key in [pygame.key.key_code(key) for key in self.settings["General"]["quit"]]: + if event.key in get_keys(self.settings["General"]["quit"]): self.exit() + elif event.key in get_keys(self.settings["General"]["pause"]) and self.game: + self.game.pause() if not self.game: for button in self.buttons: @@ -60,14 +63,13 @@ class Main(BaseScreen, SceenElement, TextScreen): def run_game_loop(self) -> None: """Run a single iteration of the game loop.""" - if not self.game: + if self.game: + self.game.run() + else: self.draw() self.handle_events() - if self.game: - self.game.run() - self.update() def exit(self) -> None: diff --git a/src/game/screens/pause.py b/src/game/screens/pause.py new file mode 100644 index 0000000..1bb3f34 --- /dev/null +++ b/src/game/screens/pause.py @@ -0,0 +1,71 @@ +import pygame +from utils import CONFIG + +from .base import BaseScreen, SceenElement, TextScreen + + +class Pause(BaseScreen, SceenElement, TextScreen): + def __init__(self) -> None: + self._initialize_surface() + self._initialize_rect() + self._initialize_font() + + def run(self) -> None: + """ + Raises: + NotImplementedError: Not implemented yet. + """ + raise NotImplementedError + + def update(self) -> None: + self._update_display_surface() + + def draw(self) -> None: + """Update the display.""" + self.update() + self._draw_background() + self._draw_text() + + def _draw_text(self) -> None: + """Draw the text.""" + self._display_text("Paused") + + def _display_text(self, text: str) -> None: + """Display the text.""" + text_surface = self.font.render(text, True, CONFIG.colors.fg_float) + text_rect = text_surface.get_rect(center=self.rect.center) + self.text_surface.blit(text_surface, text_rect) + + def _draw_background(self) -> None: + """Draw the background.""" + self.surface.fill(CONFIG.colors.bg_float) + self.surface.set_alpha(100) + self.text_surface.set_colorkey("#000000") + self.text_surface.set_alpha(255) + + def _initialize_surface(self) -> None: + """Initialize the pause screen surface.""" + self.surface = pygame.Surface(CONFIG.window.size) + self.display_surface = pygame.display.get_surface() + self.text_surface = pygame.Surface(CONFIG.window.size) + + def _initialize_rect(self) -> None: + """Initialize the score rectangle.""" + self.rect = self.surface.get_rect(topleft=(0, 0)) + + def _initialize_font(self) -> None: + """Initialize the font used to display the text.""" + self.font = pygame.font.Font(CONFIG.font.family, CONFIG.font.size * 2) + + def _update_display_surface(self) -> None: + """Update the display surface.""" + self.display_surface.blit(self.surface, self.rect) + self.display_surface.blit(self.text_surface, self.rect) + + def _draw_border(self) -> None: + """Draw the border. + + Raises: + NotImplementedError: Not implemented yet. + """ + raise NotImplementedError diff --git a/src/game/screens/tetris.py b/src/game/screens/tetris.py index 5764d21..b45e8bf 100644 --- a/src/game/screens/tetris.py +++ b/src/game/screens/tetris.py @@ -91,12 +91,11 @@ class Tetris(BaseScreen): def handle_events(self) -> None: """Handle player input events.""" - keys: pygame.key.ScancodeWrapper = pygame.key.get_pressed() - - self._handle_movement_keys(keys) - self._handle_rotation_keys(keys) - self._handle_down_key(keys) - self._handle_drop_key(keys) + if not self.paused: + self._handle_movement_keys() + self._handle_rotation_keys() + self._handle_down_key() + self._handle_drop_key() def move_down(self) -> bool: """ @@ -161,6 +160,25 @@ class Tetris(BaseScreen): """ return self.tetromino.drop() + def restart(self) -> None: + """Restart the game.""" + logger.info(f"Restarting the game. Score was {self.score}") + self._reset_game_state() + + def freeze(self) -> None: + """Freeze all timers.""" + self.timers.freeze() + self.paused = True + + def unfreeze(self) -> None: + """Unfreeze all timers.""" + self.timers.unfreeze() + self.paused = False + + def mute(self) -> None: + """Mute the game.""" + self.landing_sound.set_volume(0) + def create_new_tetromino(self, shape: Optional[Figure] = None) -> Optional[Tetromino]: """Create a new tetromino and perform necessary actions.""" self._play_landing_sound() @@ -204,15 +222,6 @@ class Tetris(BaseScreen): return True return False - def restart(self) -> None: - """Restart the game.""" - logger.info(f"Restarting the game. Score was {self.score}") - self._reset_game_state() - - def mute(self) -> None: - """Mute the game.""" - self.landing_sound.set_volume(0) - def _draw_grid(self) -> None: """Draw the grid on the game surface.""" for col in range(1, CONFIG.game.columns): @@ -299,8 +308,8 @@ class Tetris(BaseScreen): def _level_up(self) -> None: """Level up.""" self.level += 1 - self.initial_block_speed *= 0.5 - self.increased_block_speed *= 0.5 + self.initial_block_speed *= 0.8 # the larger the multiplier, the slower the game + self.increased_block_speed *= 0.8 self.timers.vertical.duration = self.initial_block_speed def _draw_components(self) -> None: @@ -365,13 +374,14 @@ class Tetris(BaseScreen): 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.increased_block_speed = self.initial_block_speed * 0.5 self.down_pressed = False self.drop_pressed = False self.level: int = 1 self.score: int = 0 self.lines: int = 0 self.game_over = False + self.paused = False def _initialize_sound(self) -> None: """Initialize game sounds.""" @@ -397,17 +407,17 @@ class Tetris(BaseScreen): """Fill the game surface with background color.""" self.surface.fill(CONFIG.colors.bg_float) - def _handle_movement_keys(self, keys: pygame.key.ScancodeWrapper) -> None: + def _handle_movement_keys(self) -> None: """ Handle movement keys. See `settings.toml` for the default key bindings. """ - right_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["right"]] - right_key_pressed = any(keys[key] for key in right_keys) + right_keys = get_keys(self.settings["Movement"]["right"]) + right_key_pressed = is_key_pressed(right_keys) - left_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["left"]] - left_key_pressed = any(keys[key] for key in left_keys) + left_keys = get_keys(self.settings["Movement"]["left"]) + left_key_pressed = is_key_pressed(left_keys) if not self.timers.horizontal.active: if left_key_pressed: @@ -417,17 +427,18 @@ class Tetris(BaseScreen): self.move_right() self.timers.horizontal.activate() - def _handle_rotation_keys(self, keys: pygame.key.ScancodeWrapper) -> None: + def _handle_rotation_keys(self) -> None: """ Handle rotation keys. See `settings.toml` for the default key bindings. """ - cw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["cw"]] - cw_key_pressed = any(keys[key] for key in cw_keys) - ccw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["ccw"]] - ccw_key_pressed = any(keys[key] for key in ccw_keys) + cw_keys = get_keys(self.settings["Rotation"]["cw"]) + cw_key_pressed = is_key_pressed(cw_keys) + + ccw_keys = get_keys(self.settings["Rotation"]["ccw"]) + ccw_key_pressed = is_key_pressed(ccw_keys) if not self.timers.rotation.active: if cw_key_pressed: @@ -438,14 +449,14 @@ class Tetris(BaseScreen): self.rotate_reverse() self.timers.rotation.activate() - def _handle_down_key(self, keys: pygame.key.ScancodeWrapper) -> None: + def _handle_down_key(self) -> None: """ Handle the down key. See `settings.toml` for the default key bindings. """ - down_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["down"]] - down_key_pressed = any(keys[key] for key in down_keys) + down_keys = get_keys(self.settings["Movement"]["down"]) + down_key_pressed = is_key_pressed(down_keys) if not self.down_pressed and down_key_pressed: self.down_pressed = True self.timers.vertical.duration = self.increased_block_speed @@ -454,14 +465,14 @@ class Tetris(BaseScreen): self.down_pressed = False self.timers.vertical.duration = self.initial_block_speed - def _handle_drop_key(self, keys: pygame.key.ScancodeWrapper) -> None: + def _handle_drop_key(self) -> None: """ Handle the drop key. See `settings.toml` for the default key bindings. """ - drop_keys = [pygame.key.key_code(key) for key in self.settings["Action"]["drop"]] - drop_key_pressed = any(keys[key] for key in drop_keys) + drop_keys = get_keys(self.settings["Action"]["drop"]) + drop_key_pressed = is_key_pressed(drop_keys) if not self.timers.drop.active and drop_key_pressed: self.drop() @@ -494,3 +505,14 @@ class Tetris(BaseScreen): (self.grid_surface.get_width(), y), CONFIG.game.line_width, ) + + +def get_keys(keys: dict[str, str]) -> list[int]: + """Get the key codes for the specified keys.""" + return [pygame.key.key_code(key) for key in keys] + + +def is_key_pressed(keys: list[int]) -> bool: + """Check if any of the specified keys is pressed.""" + keys_pressed = pygame.key.get_pressed() + return any(keys_pressed[key] for key in keys) diff --git a/src/game/timer.py b/src/game/timer.py index 1df5ce1..1d65ad7 100644 --- a/src/game/timer.py +++ b/src/game/timer.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, NamedTuple, Optional +from typing import Any, Callable, Iterator, Optional import pygame from attrs import define, field @@ -55,7 +55,8 @@ class Timer: self.activate() -class Timers(NamedTuple): +@define +class Timers: """ NamedTuple for grouping different timers. @@ -70,3 +71,17 @@ class Timers(NamedTuple): horizontal: Timer rotation: Timer drop: Timer + + def __iter__(self) -> Iterator[Timer]: + """Returns an iterator over the timers.""" + return iter((self.vertical, self.horizontal, self.rotation, self.drop)) + + def freeze(self) -> None: + """Freezes all timers.""" + for timer in self: + timer.deactivate() + + def unfreeze(self) -> None: + """Unfreezes all timers.""" + for timer in self: + timer.activate()