feat(game): add main menu

This commit is contained in:
Kristofers Solo 2024-01-06 05:07:46 +02:00
parent 0afe1ed3cb
commit 7d5bf8e658
16 changed files with 285 additions and 91 deletions

11
main.py
View File

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

View File

@ -1,5 +1,5 @@
from .log import log
from .screens import Game, Menu, Tetris
from .screens import Game, Main, Tetris
__all__ = [
"log",

View File

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

View File

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

View File

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

View File

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

116
src/game/screens/main.py Normal file
View File

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

View File

@ -1,7 +0,0 @@
from game.log import log
from .base import BaseScreen
class Menu(BaseScreen):
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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