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

View File

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

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:
"""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)

View File

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