diff --git a/main.py b/main.py index dfb10c3..bd3c0af 100755 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from loguru import logger -from py2048.game import Game +from py2048 import Game @logger.catch diff --git a/src/py2048/__init__.py b/src/py2048/__init__.py index 869490b..6e9484b 100644 --- a/src/py2048/__init__.py +++ b/src/py2048/__init__.py @@ -1,5 +1,4 @@ -from .color import ColorScheme from .config import Config -from .utils import BASE_PATH, Direction +from .game import Game -__all__ = ["Direction", "ColorScheme", "Config", "BASE_PATH"] +__all__ = ["Config", "Game"] diff --git a/src/py2048/config.py b/src/py2048/config.py index 3fe1a10..12f8c5a 100644 --- a/src/py2048/config.py +++ b/src/py2048/config.py @@ -1,27 +1,12 @@ -from .color import ColorScheme +from .utils import Board, ColorScheme, Font, Header, Position, Screen, Size, Tile class Config: - FONT_FAMILY = "Roboto" - FONT_SIZE = 32 + FONT = Font() + COLORSCHEME = ColorScheme.ORIGINAL.value - TILE_SIZE = 75 - TILE_BORDER_WIDTH = TILE_SIZE // 20 - TILE_BORDER_RADIUS = TILE_SIZE // 10 - INITIAL_TILE_COUNT = 2 - TILE_VALUE_PROBABILITY = 0.9 - - BOARD_SIZE = 4 - BOARD_WIDTH = BOARD_SIZE * TILE_SIZE - BOARD_HEIGHT = BOARD_SIZE * TILE_SIZE - - HEADER_WIDTH = BOARD_WIDTH + TILE_SIZE - HEADER_HEIGHT = TILE_SIZE - - BOARD_X = TILE_SIZE // 2 - BOARD_Y = HEADER_HEIGHT + TILE_SIZE // 2 - - SCREEN_WIDTH = HEADER_WIDTH - SCREEN_HEIGHT = BOARD_HEIGHT + TILE_SIZE + HEADER_HEIGHT - SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT + TILE = Tile() + BOARD = Board() + HEADER = Header() + SCREEN = Screen() diff --git a/src/py2048/game.py b/src/py2048/game.py index 5abd3a5..4c240a9 100644 --- a/src/py2048/game.py +++ b/src/py2048/game.py @@ -6,21 +6,20 @@ from loguru import logger from .config import Config from .objects import Board from .screens import Header, Menu -from .utils import Direction, _setup_logger +from .utils import Direction, setup_logger class Game: def __init__(self) -> None: - _setup_logger() + setup_logger() logger.info("Initializing game") pygame.init() - self.screen: pygame.Surface = pygame.display.set_mode(Config.SCREEN_SIZE) + self.screen: pygame.Surface = pygame.display.set_mode(Config.SCREEN.size) pygame.display.set_caption("2048") self.board = Board() self.header = Header() - self.menu = Menu() - self.score = 0 + # self.menu = Menu() def run(self) -> None: """Run the game loop.""" @@ -36,9 +35,9 @@ class Game: def _render(self) -> None: """Render the game.""" self.screen.fill(Config.COLORSCHEME.BG) - # self.board.draw(self.screen) - # self.header.draw(self.screen, self.score) - self.menu.draw(self.screen) + self.board.draw(self.screen) + self.header.draw(self.screen, 0) + # self.menu.draw(self.screen) pygame.display.flip() def _hande_events(self) -> None: @@ -57,19 +56,19 @@ class Game: self.move_down() elif event.key == pygame.K_q: self.exit() - self.menu._handle_events(event) + # self.menu._handle_events(event) def move_up(self) -> None: - self.score += self.board.move(Direction.UP) + self.board.move(Direction.UP) def move_down(self) -> None: - self.score += self.board.move(Direction.DOWN) + self.board.move(Direction.DOWN) def move_left(self) -> None: - self.score += self.board.move(Direction.LEFT) + self.board.move(Direction.LEFT) def move_right(self) -> None: - self.score += self.board.move(Direction.RIGHT) + self.board.move(Direction.RIGHT) def exit(self) -> None: """Exit the game.""" diff --git a/src/py2048/objects/__init__.py b/src/py2048/objects/__init__.py index 26433c2..0002bf8 100644 --- a/src/py2048/objects/__init__.py +++ b/src/py2048/objects/__init__.py @@ -1,7 +1,6 @@ from .board import Board from .button import Button from .label import Label -from .sprite import Sprite from .tile import Tile -__all__ = ["Board", "Button", "Sprite", "Tile", "Label"] +__all__ = ["Board", "Button", "Label", "Tile"] diff --git a/src/py2048/objects/abc/__init__.py b/src/py2048/objects/abc/__init__.py new file mode 100644 index 0000000..409beb3 --- /dev/null +++ b/src/py2048/objects/abc/__init__.py @@ -0,0 +1,5 @@ +from .clickable_ui_element import ClickableUIElement +from .movable_ui_element import MovableUIElement +from .ui_element import UIElement + +__all__ = ["ClickableUIElement", "MovableUIElement", "UIElement"] diff --git a/src/py2048/objects/abc/clickable_ui_element.py b/src/py2048/objects/abc/clickable_ui_element.py new file mode 100644 index 0000000..b74e77a --- /dev/null +++ b/src/py2048/objects/abc/clickable_ui_element.py @@ -0,0 +1,28 @@ +from abc import ABC, ABCMeta, abstractmethod +from typing import Callable, Optional + +import pygame + + +class ClickableUIElement(ABC, metaclass=ABCMeta): + def __init__( + self, + /, + hover_color: str, + action: Optional[Callable[[], None]] = None, + ) -> None: + self.action = action + self.hover_color = hover_color + self.is_hovered = False + + @abstractmethod + def on_click(self) -> None: + """Handle the click event.""" + + @abstractmethod + def on_hover(self) -> None: + """Handle the hover event.""" + + @abstractmethod + def _draw_hover_background(self, surface: pygame.Surface) -> None: + """Draw the hover rectangle.""" diff --git a/src/py2048/objects/abc/movable_ui_element.py b/src/py2048/objects/abc/movable_ui_element.py new file mode 100644 index 0000000..2f20746 --- /dev/null +++ b/src/py2048/objects/abc/movable_ui_element.py @@ -0,0 +1,24 @@ +from abc import ABC, ABCMeta, abstractmethod + +import pygame + +from py2048.utils import Direction, Position + + +class MovableUIElement(ABC, metaclass=ABCMeta): + @abstractmethod + def move(self, direction: Direction) -> None: + """Move the element in the given direction.""" + + @abstractmethod + def update(self) -> None: + """Update the element.""" + + @abstractmethod + def __hash__(self) -> int: + """Return a hash of the sprite.""" + + @property + @abstractmethod + def pos(self) -> Position: + """Return the position of the element.""" diff --git a/src/py2048/objects/abc/ui_element.py b/src/py2048/objects/abc/ui_element.py new file mode 100644 index 0000000..2842890 --- /dev/null +++ b/src/py2048/objects/abc/ui_element.py @@ -0,0 +1,49 @@ +from abc import ABC, ABCMeta, abstractmethod +from typing import Optional + +import pygame +from loguru import logger + +from py2048 import Config +from py2048.utils import Position, Size + + +class UIElement(ABC, metaclass=ABCMeta): + def __init__( + self, + /, + *, + position: Position, + bg_color: str, + font_color: str, + size: Size = Size(50, 50), + text: str = "", + border_radius: int = 0, + border_width: int = 0, + ): + super().__init__() + self.text = text + self.size = size + self.bg_color = bg_color + self.font_color = font_color + self.border_radius = border_radius + self.border_width = border_width + self.position = position + self.x, self.y = self.position + self.font = pygame.font.SysFont(Config.FONT.family, Config.FONT.size) + + @abstractmethod + def draw(self, surface: pygame.Surface) -> None: + """Draw the element on the given surface.""" + + @abstractmethod + def _draw_background(self, surface: pygame.Surface) -> None: + """Draw a background for the given surface.""" + + @abstractmethod + def _draw_text(self) -> None: + """Draw the text of the element.""" + + @abstractmethod + def _create_surface(self) -> pygame.Surface: + """Create a surface for the element.""" diff --git a/src/py2048/objects/board.py b/src/py2048/objects/board.py index 734f1e8..2b7b7ea 100644 --- a/src/py2048/objects/board.py +++ b/src/py2048/objects/board.py @@ -3,7 +3,8 @@ import random import pygame from loguru import logger -from py2048 import Config, Direction +from py2048 import Config +from py2048.utils import Direction, Position from .tile import Tile @@ -11,9 +12,9 @@ from .tile import Tile class Board(pygame.sprite.Group): def __init__(self): super().__init__() - self.rect = pygame.Rect(0, 0, Config.BOARD_WIDTH, Config.BOARD_HEIGHT) - self.rect.x = Config.BOARD_X - self.rect.y = Config.BOARD_Y + self.rect = pygame.Rect(0, 0, *Config.BOARD.size) + self.score: int = 0 + self.rect.x, self.rect.y = Config.BOARD.pos self.initiate_game() def initiate_game(self) -> None: @@ -22,7 +23,6 @@ class Board(pygame.sprite.Group): def draw(self, surface: pygame.Surface) -> None: """Draw the board.""" - tile: Tile self._draw_background(surface) super().draw(surface) @@ -33,17 +33,17 @@ class Board(pygame.sprite.Group): surface, Config.COLORSCHEME.BOARD_BG, self.rect, - border_radius=Config.TILE_BORDER_RADIUS, + border_radius=Config.TILE.border.radius, ) # background pygame.draw.rect( surface, Config.COLORSCHEME.BOARD_BG, self.rect, - width=Config.TILE_BORDER_WIDTH, - border_radius=Config.TILE_BORDER_RADIUS, + width=Config.TILE.border.width, + border_radius=Config.TILE.border.radius, ) # border - def move(self, direction: Direction) -> int: + def move(self, direction: Direction) -> None: """Move the tiles in the specified direction.""" score = 0 tiles = self.sprites() @@ -60,23 +60,22 @@ class Board(pygame.sprite.Group): tiles.sort(key=lambda tile: tile.rect.x, reverse=True) for tile in tiles: - score += tile.move(direction) + tile.move(direction) + self.score += tile.value if not self._is_full(): self.generate_random_tile() - return score - def generate_initial_tiles(self) -> None: """Generate the initial tiles.""" - self.generate_tile(Config.INITIAL_TILE_COUNT) + self.generate_tile(Config.TILE.initial_count) - def generate_tile(self, amount: int = 1, *pos: tuple[int, int]) -> None: + def generate_tile(self, amount: int = 1, *pos: Position) -> None: """Generate `amount` number of tiles or at the specified positions.""" if pos: for coords in pos: - x, y = coords[0] * Config.TILE_SIZE, coords[1] * Config.TILE_SIZE - self.add(Tile(x, y, self)) + x, y = coords.x * Config.TILE.size, coords.y * Config.TILE.size + self.add(Tile(Position(x, y), self)) return for _ in range(amount): @@ -86,9 +85,9 @@ class Board(pygame.sprite.Group): """Generate a tile with random coordinates aligned with the grid.""" while True: # Generate random coordinates aligned with the grid - x = random.randint(0, 3) * Config.TILE_SIZE + Config.BOARD_X - y = random.randint(0, 3) * Config.TILE_SIZE + Config.BOARD_Y - tile = Tile(x, y, self) + x = random.randint(0, 3) * Config.TILE.size + Config.BOARD.pos.x + y = random.randint(0, 3) * Config.TILE.size + Config.BOARD.pos.y + tile = Tile(Position(x, y), self) colliding_tiles = pygame.sprite.spritecollide( tile, self, False @@ -100,7 +99,7 @@ class Board(pygame.sprite.Group): def _is_full(self) -> bool: """Check if the board is full.""" - return len(self.sprites()) == Config.BOARD_SIZE**2 + return len(self.sprites()) == Config.BOARD.len**2 def _can_move(self) -> bool: """Check if any movement is possible on the board.""" diff --git a/src/py2048/objects/button.py b/src/py2048/objects/button.py index 4657ee3..1ccb375 100644 --- a/src/py2048/objects/button.py +++ b/src/py2048/objects/button.py @@ -1,47 +1,54 @@ import sys +from typing import Callable, Optional import pygame from attrs import define, field -from py2048.color import ColorScheme -from py2048.config import Config +from py2048 import Config +from py2048.utils import Direction, Position + +from .abc import ClickableUIElement, UIElement -@define -class Button(pygame.sprite.Sprite): - def __init__(self, text: str, x: int, y: int, group: pygame.sprite.Group): - super().__init__() - self.text = text - self.image = self._create_button_surface() +class Button(UIElement, ClickableUIElement): + def __init__( + self, + /, + *, + hover_color: str, + action: Optional[Callable[[], None]] = None, + **kwargs, + ): + super().__init__(hover_color, action) + Static.__init__(self, **kwargs) - # text: str = field(kw_only=True) - # font_family: str = field(kw_only=True) - # font_size: int = field(kw_only=True) - # font_color: ColorScheme = field(kw_only=True) - # position: tuple[int, int] = field(kw_only=True) - # width: int = field(kw_only=True) - # height: int = field(kw_only=True) - # action = field(kw_only=True) - # bg_color: ColorScheme = field(kw_only=True) - # hover_color: ColorScheme = field(kw_only=True) - # font: pygame.Font = field(init=False) - # rendered_text: pygame.Surface = field(init=False) - # rect: pygame.Rect = field(init=False) - # is_hovered: bool = field(init=False, default=False) + def on_click(self) -> None: + pass - def __attrs_post_init__(self) -> None: - """Initialize the button.""" - self.font = pygame.font.SysFont(self.font_family, self.font_size) - self._draw_text() + def on_hover(self) -> None: + pass + + def _draw_background(self, surface: pygame.Surface) -> None: + """Draw a rectangle with borders on the given surface.""" + pygame.draw.rect( + surface, + self.bg_color, + (*self.position, *self.size), + border_radius=self.border_radius, + ) def _draw_text(self) -> None: - """Draw the text on the button.""" + """Draw the text of the element.""" self.rendered_text = self.font.render( self.text, True, self.font_color, self.bg_color ) - self.rect = pygame.Rect( - self.position[0], self.position[1], self.width, self.height - ) + self.rect = self.rendered_text.get_rect(topleft=self.position) + + def _create_surface(self) -> pygame.Surface: + """Create a surface for the element.""" + sprite_surface = pygame.Surface(self.size, pygame.SRCALPHA) + self._draw_background(sprite_surface) + return sprite_surface def check_hover(self, mouse_pos: tuple[int, int]) -> None: """Check if the mouse is hovering over the button.""" @@ -54,18 +61,18 @@ class Button(pygame.sprite.Sprite): def draw(self, surface: pygame.Surface) -> None: """Draw the button on the given surface.""" - if self.is_hovered: - self._draw_rect(surface, self.hover_color) - else: - self._draw_rect(surface, self.bg_color) - surface.blit(self.rendered_text, self.position) + self._draw_hover_background( + surface + ) if self.is_hovered else self._draw_background(surface) - def _draw_rect(self, surface: pygame.Surface, color: ColorScheme) -> None: - """Draw the button rectangle.""" + surface.blit(self.rendered_text, self.rect.topleft) + + def _draw_hover_background(self, surface: pygame.Surface) -> None: + """Draw the hover rectangle.""" pygame.draw.rect( surface, - self.bg_color, + self.hover_color, self.rect, - border_radius=Config.TILE_BORDER_RADIUS, + border_radius=self.border_radius, ) diff --git a/src/py2048/objects/label.py b/src/py2048/objects/label.py index 2115895..dc06e05 100644 --- a/src/py2048/objects/label.py +++ b/src/py2048/objects/label.py @@ -1,16 +1,16 @@ import pygame from attrs import define, field -from py2048.color import ColorScheme -from py2048.config import Config + +from py2048 import Config @define class Label: text: str position: tuple[int, int] - bg_color: ColorScheme + bg_color: str font_family: str - font_color: ColorScheme + font_color: str font_size: int font: pygame.Font = field(init=False) rendered_text: pygame.Surface = field(init=False) @@ -20,15 +20,15 @@ class Label: self.font = pygame.font.SysFont(self.font_family, self.font_size) self._draw_text() - def _draw_text(self) -> None: - self.rendered_text = self.font.render( - self.text, True, self.font_color, self.bg_color - ) - self.rect = self.rendered_text.get_rect(topleft=self.position) + def draw(self, surface: pygame.Surface) -> None: + surface.blit(self.rendered_text, self.position) def update(self, new_text: str) -> None: self.text = new_text self._draw_text() - def draw(self, surface: pygame.Surface) -> None: - surface.blit(self.rendered_text, self.position) + def _draw_text(self) -> None: + self.rendered_text = self.font.render( + self.text, True, self.font_color, self.bg_color + ) + self.rect = self.rendered_text.get_rect(topleft=self.position) diff --git a/src/py2048/objects/sprite.py b/src/py2048/objects/sprite.py deleted file mode 100644 index 0bca812..0000000 --- a/src/py2048/objects/sprite.py +++ /dev/null @@ -1,39 +0,0 @@ -from abc import ABC, ABCMeta, abstractmethod - -import pygame - -from py2048 import Config, Direction - - -class Sprite(ABC, pygame.sprite.Sprite, metaclass=ABCMeta): - def __init__(self, x: int, y: int, group: pygame.sprite.Group): - super().__init__() - self.image = self._create_surface() - self.rect = self.image.get_rect() - self.rect.topleft = x, y - self.font = pygame.font.SysFont(Config.FONT_FAMILY, Config.FONT_SIZE) - self.group = group - self.update() - - @abstractmethod - def draw(self, surface: pygame.Surface) -> None: - """Draw the sprite on the given surface.""" - - @abstractmethod - def update(self) -> None: - """Update the sprite.""" - - @abstractmethod - def move(self, direction: Direction) -> None: - """Move the tile by `dx` and `dy`.""" - - @abstractmethod - def _draw_background(self, surface: pygame.Surface) -> None: - """Draw a rounded rectangle with borders on the given surface.""" - - @abstractmethod - def _create_surface(self) -> pygame.Surface: - """Create a surface for the sprite.""" - sprite_surface = pygame.Surface((100, 100), pygame.SRCALPHA) - self._draw_background(sprite_surface) - return sprite_surface diff --git a/src/py2048/objects/tile.py b/src/py2048/objects/tile.py index fdaafaf..6e82aee 100644 --- a/src/py2048/objects/tile.py +++ b/src/py2048/objects/tile.py @@ -2,102 +2,123 @@ import random from typing import Union import pygame +from loguru import logger -from py2048 import ColorScheme, Config, Direction +from py2048 import Config +from py2048.utils import ColorScheme, Direction, Position, Size -from .sprite import Sprite +from .abc import MovableUIElement, UIElement def _grid_pos(pos: int) -> int: """Return the position in the grid.""" - return pos // Config.TILE_SIZE + 1 + return pos // Config.TILE.size + 1 -class Tile(Sprite): +class Tile(UIElement, MovableUIElement, pygame.sprite.Sprite): def __init__( - self, x: int, y: int, group: pygame.sprite.Group, value: int | None = 2 + self, + position: Position, + group: pygame.sprite.Group, ): - super().__init__(x, y, group) + pygame.sprite.Sprite.__init__(self) + self.value = 2 if random.random() <= Config.TILE.value_probability else 4 - self.value: int = ( - value - if value is not None - else 2 - if random.random() <= Config.TILE_VALUE_PROBABILITY - else 4 + super().__init__( + position=position, + text=f"{self.value}", + bg_color=Config.COLORSCHEME.TILE_0, + font_color=Config.COLORSCHEME.DARK_TEXT, + size=Size(Config.TILE.size, Config.TILE.size), + border_radius=Config.TILE.border.radius, + border_width=Config.TILE.border.width, ) + self.score: int = 0 + self.group = group + self.image = self._create_surface() self.rect = self.image.get_rect() - self.rect.topleft = x, y - self.font = pygame.font.SysFont(Config.FONT_FAMILY, Config.FONT_SIZE) - self.group = group + self.rect.topleft = self.position self.update() + def draw(self, surface: pygame.Surface) -> None: + """Draw the value of the tile.""" + self._draw_background(surface) + self._draw_text() + + def update(self) -> None: + """Update the sprite.""" + self._draw_background(self.image) + self.text = f"{self.value}" + self._draw_text() + self.image.blit(self.image, (0, 0)) + def _draw_background(self, surface: pygame.Surface) -> None: """Draw a rounded rectangle with borders on the given surface.""" - rect = (0, 0, Config.TILE_SIZE, Config.TILE_SIZE) + rect = (0, 0, *self.size) pygame.draw.rect( - surface, self._get_color(), rect, border_radius=Config.TILE_BORDER_RADIUS + surface, self._get_color(), rect, border_radius=Config.TILE.border.radius ) # background pygame.draw.rect( surface, (0, 0, 0, 0), rect, - border_radius=Config.TILE_BORDER_RADIUS, - width=Config.TILE_BORDER_WIDTH, + border_radius=Config.TILE.border.radius, + width=Config.TILE.border.width, ) # border - def _create_surface(self) -> pygame.Surface: - """Create a surface for the tile.""" - sprite_surface = pygame.Surface( - (Config.TILE_SIZE, Config.TILE_SIZE), pygame.SRCALPHA + def _draw_text(self) -> None: + """Draw the text of the sprite.""" + self.rendered_text = self.font.render(self.text, True, self.font_color) + self.image.blit( + self.rendered_text, + self.rendered_text.get_rect(center=self.image.get_rect().center), ) + + def _create_surface(self) -> pygame.Surface: + """Create a surface for the sprite.""" + sprite_surface = pygame.Surface(self.size, pygame.SRCALPHA) self._draw_background(sprite_surface) return sprite_surface - def draw(self, surface: pygame.Surface) -> None: - """Draw the value of the tile.""" - text = self.font.render(str(self.value), True, Config.COLORSCHEME.DARK_TEXT) - sprite_surface = self._create_surface() - - sprite_center: tuple[int, int] = (Config.TILE_SIZE // 2, Config.TILE_SIZE // 2) - - text_rect: pygame.Rect = text.get_rect(center=self.image.get_rect().center) - sprite_surface.blit(text, text_rect) - - self.image.blit(sprite_surface, (0, 0)) - - def move(self, direction: Direction) -> int: - """Move the tile by `dx` and `dy`.""" - score = 0 + def move(self, direction: Direction) -> None: + """ + Move the tile by `dx` and `dy`. + If the tile collides with another tile, it will merge with it if possible. + Before moving, reset the score of the tile. + """ while True: new_x, new_y = self._calc_new_pos(direction) if self._is_out_if_bounds(new_x, new_y): - return score + return if self._has_collision(new_x, new_y): collided_tile = self._get_collided_tile(new_x, new_y) if collided_tile and self._can_merge(collided_tile): - score += self._merge(collided_tile) + self._merge(collided_tile) else: - return score + return self.group.remove(self) self.rect.topleft = new_x, new_y self.group.add(self) + def get_score(self) -> int: + """Return the score of the tile.""" + return self.score + def _calc_new_pos(self, direction: Direction) -> tuple[int, int]: """Calculate the new position of the tile.""" - dx, dy = direction * Config.TILE_SIZE + dx, dy = direction * Config.TILE.size return self.rect.x + dx, self.rect.y + dy def _is_out_if_bounds(self, x: int, y: int) -> bool: """Return whether the tile is out of bounds.""" - board_left = Config.BOARD_X - board_right = Config.BOARD_X + Config.BOARD_WIDTH - Config.TILE_SIZE - board_top = Config.BOARD_Y - board_bottom = Config.BOARD_Y + Config.BOARD_HEIGHT - Config.TILE_SIZE + board_left = Config.BOARD.pos.x + board_right = Config.BOARD.pos.x + Config.BOARD.size.width - Config.TILE.size + board_top = Config.BOARD.pos.y + board_bottom = Config.BOARD.pos.y + Config.BOARD.size.height - Config.TILE.size return not (board_left <= x <= board_right and board_top <= y <= board_bottom) def _has_collision(self, x: int, y: int) -> bool: @@ -120,18 +141,13 @@ class Tile(Sprite): """Check if the tile can merge with another tile.""" return self.value == other.value - def _merge(self, other: "Tile") -> int: + def _merge(self, other: "Tile") -> None: """Merge the tile with another tile.""" self.group.remove(other) self.group.remove(self) self.value += other.value self.update() self.group.add(self) - return self.value - - def update(self) -> None: - """Update the sprite.""" - self.draw() def can_move(self) -> bool: """Check if the tile can move""" @@ -145,8 +161,8 @@ class Tile(Sprite): return True return False - def _get_color(self) -> ColorScheme: - """Change the color of the tile based on its value""" + def _get_color(self) -> str: + """Change the color of the tile based on its value.""" color_map = { 2: Config.COLORSCHEME.TILE_2, 4: Config.COLORSCHEME.TILE_4, @@ -175,6 +191,6 @@ class Tile(Sprite): return hash((self.rect.x, self.rect.y, self.value)) @property - def pos(self) -> tuple[int, int]: + def pos(self) -> Position: """Return the position of the tile.""" - return _grid_pos(self.rect.x), _grid_pos(self.rect.y) + return Position(_grid_pos(self.rect.x), _grid_pos(self.rect.y)) diff --git a/src/py2048/screens/__init__.py b/src/py2048/screens/__init__.py index 197931a..f4dd85f 100644 --- a/src/py2048/screens/__init__.py +++ b/src/py2048/screens/__init__.py @@ -1,4 +1,4 @@ from .header import Header from .menu import Menu -__all__ = ["Menu", "Header"] +__all__ = ["Header", "Menu"] diff --git a/src/py2048/screens/header.py b/src/py2048/screens/header.py index ea9811f..c21930a 100644 --- a/src/py2048/screens/header.py +++ b/src/py2048/screens/header.py @@ -1,19 +1,25 @@ import pygame + from py2048 import Config from py2048.objects import Label +from py2048.utils import Position class Header: def __init__(self) -> None: - self.rect = pygame.Rect(0, 0, Config.HEADER_WIDTH, Config.HEADER_HEIGHT) + self.rect = pygame.Rect(0, 0, *Config.HEADER.size) def draw(self, screen: pygame.Surface, score: int) -> None: """Draw the header.""" - score = Label( + self.score = Label( text=f"{score}", - position=(10, 10), + position=Position(10, 10), bg_color=Config.COLORSCHEME.BOARD_BG, - font_family=Config.FONT_FAMILY, + font_family=Config.FONT.family, font_color=Config.COLORSCHEME.DARK_TEXT, - font_size=Config.FONT_SIZE, + font_size=Config.FONT.size, ).draw(screen) + + def update(self, score: int) -> None: + """Update the header.""" + self.Label.text = f"{score}" diff --git a/src/py2048/screens/menu.py b/src/py2048/screens/menu.py index 69a7ccc..3c8f237 100644 --- a/src/py2048/screens/menu.py +++ b/src/py2048/screens/menu.py @@ -3,6 +3,7 @@ from loguru import logger from py2048 import Config from py2048.objects import Button +from py2048.utils import Position class Menu: @@ -14,22 +15,21 @@ class Menu: "Exit": self.exit, } buttons_width, button_height = 120, 50 + self.buttons = [ Button( - text=text, - font_family=Config.FONT_FAMILY, - font_size=Config.FONT_SIZE, - font_color=Config.COLORSCHEME.LIGHT_TEXT, - position=( - Config.SCREEN_WIDTH / 2 - 50, - Config.SCREEN_HEIGHT / (len(buttons_data) + 1) * index - - button_height, + position=Position( + Config.SCREEN.size.width / 2 - button_height // 2, + Config.SCREEN.size.height / len(buttons_data) * index + - button_height // 2, ), - width=buttons_width, - height=button_height, - action=action, bg_color=Config.COLORSCHEME.BOARD_BG, + font_color=Config.COLORSCHEME.LIGHT_TEXT, hover_color=Config.COLORSCHEME.TILE_0, + size=(buttons_width, button_height), + text=text, + border_radius=Config.TILE.border.radius, + action=action, ) for index, (text, action) in enumerate(buttons_data.items(), start=1) ] diff --git a/src/py2048/utils.py b/src/py2048/utils.py deleted file mode 100644 index 25a82e3..0000000 --- a/src/py2048/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -from enum import Enum -from pathlib import Path - -from loguru import logger - -from .config import Config - -BASE_PATH = Path(__file__).resolve().parent.parent.parent - - -def _setup_logger() -> None: - logger.add( - BASE_PATH.joinpath(".logs", "game.log"), - format="{time} | {level} | {message}", - level="DEBUG" if BASE_PATH.joinpath("debug").exists() else "INFO", - rotation="1 MB", - compression="zip", - ) - - -class Direction(Enum): - UP = (0, -1) - DOWN = (0, 1) - LEFT = (-1, 0) - RIGHT = (1, 0) - - def __mul__(self, num: int) -> tuple[int, int]: - """Multiply the direction by a constant.""" - return self.value[0] * num, self.value[1] * num - - def __imul__(self, num: int) -> tuple[int, int]: - """Multiply the direction by a constant.""" - return self.value[0] * num, self.value[1] * num diff --git a/src/py2048/utils/__init__.py b/src/py2048/utils/__init__.py new file mode 100644 index 0000000..2eba2da --- /dev/null +++ b/src/py2048/utils/__init__.py @@ -0,0 +1,34 @@ +from pathlib import Path + +from loguru import logger + +from .collections import Board, Font, Header, Position, Screen, Size, Tile +from .color import ColorScheme +from .enums import Direction + +BASE_PATH = Path(__file__).resolve().parent.parent.parent.parent + + +def setup_logger() -> None: + logger.add( + BASE_PATH.joinpath(".logs", "game.log"), + format="{time} | {level} | {message}", + level="DEBUG" if BASE_PATH.joinpath("debug").exists() else "INFO", + rotation="1 MB", + compression="zip", + ) + + +__all__ = [ + "BASE_PATH", + "Board", + "ColorScheme", + "Direction", + "Font", + "Position", + "Size", + "Tile", + "setup_logger", + "Header", + "Screen", +] diff --git a/src/py2048/utils/collections.py b/src/py2048/utils/collections.py new file mode 100644 index 0000000..0167ada --- /dev/null +++ b/src/py2048/utils/collections.py @@ -0,0 +1,52 @@ +from typing import NamedTuple + +from attr import Factory, define, field + + +class Position(NamedTuple): + x: int + y: int + + +class Size(NamedTuple): + width: int + height: int + + +@define +class Font: + family: str = "Roboto" + size: int = 32 + + +@define +class Border: + width: int + radius: int + + +@define +class Tile: + size: int = 75 + border: Border = Border(size // 20, size // 10) + initial_count: int = 2 + value_probability: float = 0.9 + + +@define +class Board: + len: int = 4 + size: Size = Size(len * Tile().size, len * Tile().size) + pos: Position = Position(Tile().size // 2, Tile().size + Tile().size // 2) + + +@define +class Header: + size: Size = Size(Board().size.width + Tile().size, Tile().size) + + +@define +class Screen: + size: Size = Size( + Header().size.width, Board().size.height + Tile().size + Header().size.height + ) diff --git a/src/py2048/color.py b/src/py2048/utils/color.py similarity index 100% rename from src/py2048/color.py rename to src/py2048/utils/color.py diff --git a/src/py2048/utils/enums.py b/src/py2048/utils/enums.py new file mode 100644 index 0000000..32dedf9 --- /dev/null +++ b/src/py2048/utils/enums.py @@ -0,0 +1,18 @@ +from enum import Enum + +from .collections import Position + + +class Direction(Enum): + UP = Position(0, -1) + DOWN = Position(0, 1) + LEFT = Position(-1, 0) + RIGHT = Position(1, 0) + + def __mul__(self, num: int) -> Position: + """Multiply the direction by a constant.""" + return Position(self.value.x * num, self.value.y * num) + + def __imul__(self, num: int) -> tuple[int, int]: + """Multiply the direction by a constant.""" + return Position(self.value.x * num, self.value.y * num)