Merge pull request #7 from kristoferssolo/feature/pause

Implement pause feature
This commit is contained in:
Kristofers Solo 2024-01-16 20:02:09 +02:00 committed by GitHub
commit 63fdf72cc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 204 additions and 49 deletions

View File

@ -1,15 +1,17 @@
from typing import Any from typing import Any
import pygame import pygame
from loguru import logger
from utils import CONFIG, Figure, GameMode from utils import CONFIG, Figure, GameMode
from .base import BaseScreen from .base import BaseScreen, SceenElement
from .pause import Pause
from .preview import Preview from .preview import Preview
from .score import Score from .score import Score
from .tetris import Tetris from .tetris import Tetris
class Game(BaseScreen): class Game(BaseScreen, SceenElement):
""" """
Game class. Game class.
@ -32,15 +34,16 @@ class Game(BaseScreen):
def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None: def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None:
self.game_mode = game_mode self.game_mode = game_mode
self.settings = settings self.settings = settings
self._initialize_surface()
self._initialize_rect()
self._initialize_game_components() self._initialize_game_components()
self._start_background_music() self._start_background_music()
self.paused = False
def draw(self) -> None: def draw(self) -> None:
""" """Draw the score on the score surface."""
Raises: self._update_display_surface()
NotImplementedError: Not implemented yet. self._draw_background()
"""
raise NotImplementedError
def update(self) -> None: def update(self) -> None:
""" """
@ -51,12 +54,15 @@ class Game(BaseScreen):
def run(self) -> None: def run(self) -> None:
"""Run a single iteration of the game loop.""" """Run a single iteration of the game loop."""
self.draw()
self.tetris.run() self.tetris.run()
self.score.run() self.score.run()
self.preview.update(self.next_figure) self.preview.update(self.next_figure)
self.preview.run() self.preview.run()
if self.paused:
self.pause_screen.draw()
self.clock.tick(CONFIG.game.fps) self.clock.tick(CONFIG.game.fps)
def mute(self) -> None: def mute(self) -> None:
@ -64,6 +70,19 @@ class Game(BaseScreen):
self.music.set_volume(0) self.music.set_volume(0)
self.tetris.mute() 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: def _initialize_game_components(self) -> None:
"""Initialize game-related components.""" """Initialize game-related components."""
self.clock = pygame.time.Clock() 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.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode, self.settings)
self.score = Score(self.game_mode) self.score = Score(self.game_mode)
self.preview = Preview() self.preview = Preview()
self.pause_screen = Pause()
def _update_score(self, lines: int, score: int, level: int) -> None: 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 = pygame.mixer.Sound(CONFIG.music.background)
self.music.set_volume(self.settings["Volume"]["Music"]["level"]) self.music.set_volume(self.settings["Volume"]["Music"]["level"])
self.music.play(-1) 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

View File

@ -8,6 +8,7 @@ from utils import CONFIG, GameMode, read_settings
from .base import BaseScreen, SceenElement, TextScreen from .base import BaseScreen, SceenElement, TextScreen
from .button import Button from .button import Button
from .game import Game from .game import Game
from .tetris import get_keys
class Main(BaseScreen, SceenElement, TextScreen): class Main(BaseScreen, SceenElement, TextScreen):
@ -45,8 +46,10 @@ class Main(BaseScreen, SceenElement, TextScreen):
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
self.exit() self.exit()
elif event.type == pygame.KEYDOWN: 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() self.exit()
elif event.key in get_keys(self.settings["General"]["pause"]) and self.game:
self.game.pause()
if not self.game: if not self.game:
for button in self.buttons: for button in self.buttons:
@ -60,14 +63,13 @@ class Main(BaseScreen, SceenElement, TextScreen):
def run_game_loop(self) -> None: def run_game_loop(self) -> None:
"""Run a single iteration of the game loop.""" """Run a single iteration of the game loop."""
if not self.game: if self.game:
self.game.run()
else:
self.draw() self.draw()
self.handle_events() self.handle_events()
if self.game:
self.game.run()
self.update() self.update()
def exit(self) -> None: def exit(self) -> None:

71
src/game/screens/pause.py Normal file
View File

@ -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

View File

@ -91,12 +91,11 @@ class Tetris(BaseScreen):
def handle_events(self) -> None: def handle_events(self) -> None:
"""Handle player input events.""" """Handle player input events."""
keys: pygame.key.ScancodeWrapper = pygame.key.get_pressed() if not self.paused:
self._handle_movement_keys()
self._handle_movement_keys(keys) self._handle_rotation_keys()
self._handle_rotation_keys(keys) self._handle_down_key()
self._handle_down_key(keys) self._handle_drop_key()
self._handle_drop_key(keys)
def move_down(self) -> bool: def move_down(self) -> bool:
""" """
@ -161,6 +160,25 @@ class Tetris(BaseScreen):
""" """
return self.tetromino.drop() 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]: def create_new_tetromino(self, shape: Optional[Figure] = None) -> Optional[Tetromino]:
"""Create a new tetromino and perform necessary actions.""" """Create a new tetromino and perform necessary actions."""
self._play_landing_sound() self._play_landing_sound()
@ -204,15 +222,6 @@ class Tetris(BaseScreen):
return True return True
return False 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: def _draw_grid(self) -> None:
"""Draw the grid on the game surface.""" """Draw the grid on the game surface."""
for col in range(1, CONFIG.game.columns): for col in range(1, CONFIG.game.columns):
@ -299,8 +308,8 @@ class Tetris(BaseScreen):
def _level_up(self) -> None: def _level_up(self) -> None:
"""Level up.""" """Level up."""
self.level += 1 self.level += 1
self.initial_block_speed *= 0.5 self.initial_block_speed *= 0.8 # the larger the multiplier, the slower the game
self.increased_block_speed *= 0.5 self.increased_block_speed *= 0.8
self.timers.vertical.duration = self.initial_block_speed self.timers.vertical.duration = self.initial_block_speed
def _draw_components(self) -> None: def _draw_components(self) -> None:
@ -365,13 +374,14 @@ class Tetris(BaseScreen):
def _initialize_game_state(self) -> None: def _initialize_game_state(self) -> None:
"""Initialize the game state.""" """Initialize the game state."""
self.initial_block_speed = CONFIG.game.initial_speed 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.down_pressed = False
self.drop_pressed = False self.drop_pressed = False
self.level: int = 1 self.level: int = 1
self.score: int = 0 self.score: int = 0
self.lines: int = 0 self.lines: int = 0
self.game_over = False self.game_over = False
self.paused = False
def _initialize_sound(self) -> None: def _initialize_sound(self) -> None:
"""Initialize game sounds.""" """Initialize game sounds."""
@ -397,17 +407,17 @@ class Tetris(BaseScreen):
"""Fill the game surface with background color.""" """Fill the game surface with background color."""
self.surface.fill(CONFIG.colors.bg_float) 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. Handle movement keys.
See `settings.toml` for the default key bindings. 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_keys = get_keys(self.settings["Movement"]["right"])
right_key_pressed = any(keys[key] for key in right_keys) 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_keys = get_keys(self.settings["Movement"]["left"])
left_key_pressed = any(keys[key] for key in left_keys) left_key_pressed = is_key_pressed(left_keys)
if not self.timers.horizontal.active: if not self.timers.horizontal.active:
if left_key_pressed: if left_key_pressed:
@ -417,17 +427,18 @@ class Tetris(BaseScreen):
self.move_right() self.move_right()
self.timers.horizontal.activate() self.timers.horizontal.activate()
def _handle_rotation_keys(self, keys: pygame.key.ScancodeWrapper) -> None: def _handle_rotation_keys(self) -> None:
""" """
Handle rotation keys. Handle rotation keys.
See `settings.toml` for the default key bindings. 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"]] cw_keys = get_keys(self.settings["Rotation"]["cw"])
ccw_key_pressed = any(keys[key] for key in ccw_keys) 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 not self.timers.rotation.active:
if cw_key_pressed: if cw_key_pressed:
@ -438,14 +449,14 @@ class Tetris(BaseScreen):
self.rotate_reverse() self.rotate_reverse()
self.timers.rotation.activate() self.timers.rotation.activate()
def _handle_down_key(self, keys: pygame.key.ScancodeWrapper) -> None: def _handle_down_key(self) -> None:
""" """
Handle the down key. Handle the down key.
See `settings.toml` for the default key bindings. 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_keys = get_keys(self.settings["Movement"]["down"])
down_key_pressed = any(keys[key] for key in down_keys) down_key_pressed = is_key_pressed(down_keys)
if not self.down_pressed and down_key_pressed: if not self.down_pressed and down_key_pressed:
self.down_pressed = True self.down_pressed = True
self.timers.vertical.duration = self.increased_block_speed self.timers.vertical.duration = self.increased_block_speed
@ -454,14 +465,14 @@ class Tetris(BaseScreen):
self.down_pressed = False self.down_pressed = False
self.timers.vertical.duration = self.initial_block_speed 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. Handle the drop key.
See `settings.toml` for the default key bindings. See `settings.toml` for the default key bindings.
""" """
drop_keys = [pygame.key.key_code(key) for key in self.settings["Action"]["drop"]] drop_keys = get_keys(self.settings["Action"]["drop"])
drop_key_pressed = any(keys[key] for key in drop_keys) drop_key_pressed = is_key_pressed(drop_keys)
if not self.timers.drop.active and drop_key_pressed: if not self.timers.drop.active and drop_key_pressed:
self.drop() self.drop()
@ -494,3 +505,14 @@ class Tetris(BaseScreen):
(self.grid_surface.get_width(), y), (self.grid_surface.get_width(), y),
CONFIG.game.line_width, 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)

View File

@ -1,4 +1,4 @@
from typing import Any, Callable, NamedTuple, Optional from typing import Any, Callable, Iterator, Optional
import pygame import pygame
from attrs import define, field from attrs import define, field
@ -55,7 +55,8 @@ class Timer:
self.activate() self.activate()
class Timers(NamedTuple): @define
class Timers:
""" """
NamedTuple for grouping different timers. NamedTuple for grouping different timers.
@ -70,3 +71,17 @@ class Timers(NamedTuple):
horizontal: Timer horizontal: Timer
rotation: Timer rotation: Timer
drop: 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()