diff --git a/src/py2048/__init__.py b/src/py2048/__init__.py index e69de29..869490b 100644 --- a/src/py2048/__init__.py +++ b/src/py2048/__init__.py @@ -0,0 +1,5 @@ +from .color import ColorScheme +from .config import Config +from .utils import BASE_PATH, Direction + +__all__ = ["Direction", "ColorScheme", "Config", "BASE_PATH"] diff --git a/src/py2048/block.py b/src/py2048/block.py deleted file mode 100644 index ac581b4..0000000 --- a/src/py2048/block.py +++ /dev/null @@ -1,178 +0,0 @@ -import random -from typing import Union - -import pygame - -from .color import ColorScheme -from .config import Config -from .utils import Direction, grid_pos - - -class Block(pygame.sprite.Sprite): - def __init__( - self, x: int, y: int, group: pygame.sprite.Group, value: int | None = 2 - ): - """Initialize a block""" - super().__init__() - - self.value: int = ( - value - if value is not None - else 2 - if random.random() <= Config.BLOCK_VALUE_PROBABILITY - else 4 - ) - self.image = self._create_block_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() - - def _draw_background(self, surface: pygame.Surface) -> None: - """Draw a rounded rectangle with borders on the given surface.""" - rect = (0, 0, Config.BLOCK_SIZE, Config.BLOCK_SIZE) - pygame.draw.rect( - surface, self._get_color(), rect, border_radius=Config.BLOCK_BORDER_RADIUS - ) # background - pygame.draw.rect( - surface, - (0, 0, 0, 0), - rect, - border_radius=Config.BLOCK_BORDER_RADIUS, - width=Config.BLOCK_BORDER_WIDTH, - ) # border - - def _create_block_surface(self) -> pygame.Surface: - """Create a surface for the block.""" - block_surface = pygame.Surface( - (Config.BLOCK_SIZE, Config.BLOCK_SIZE), pygame.SRCALPHA - ) - self._draw_background(block_surface) - return block_surface - - def draw(self) -> None: - """Draw the value of the block""" - text = self.font.render(str(self.value), True, Config.COLORSCHEME.DARK_TEXT) - block_surface = self._create_block_surface() - - block_center: tuple[int, int] = (Config.BLOCK_SIZE // 2, Config.BLOCK_SIZE // 2) - - text_rect: pygame.Rect = text.get_rect(center=self.image.get_rect().center) - block_surface.blit(text, text_rect) - - self.image.blit(block_surface, (0, 0)) - - def move(self, direction: Direction) -> int: - """Move the block by `dx` and `dy`.""" - score = 0 - while True: - new_x, new_y = self._calc_new_pos(direction) - - if self._is_out_if_bounds(new_x, new_y): - return score - - if self._has_collision(new_x, new_y): - collided_block = self._get_collided_block(new_x, new_y) - if collided_block and self._can_merge(collided_block): - score += self._merge(collided_block) - else: - return score - - self.group.remove(self) - self.rect.topleft = new_x, new_y - self.group.add(self) - - def _calc_new_pos(self, direction: Direction) -> tuple[int, int]: - """Calculate the new position of the block.""" - dx, dy = direction * Config.BLOCK_SIZE - return self.rect.x + dx, self.rect.y + dy - - def _is_out_if_bounds(self, x: int, y: int) -> bool: - """Return whether the block is out of bounds.""" - board_left = Config.BOARD_X - board_right = Config.BOARD_X + Config.BOARD_WIDTH - Config.BLOCK_SIZE - board_top = Config.BOARD_Y - board_bottom = Config.BOARD_Y + Config.BOARD_HEIGHT - Config.BLOCK_SIZE - return not (board_left <= x <= board_right and board_top <= y <= board_bottom) - - def _has_collision(self, x: int, y: int) -> bool: - """Checks whether the block has a collision with any other block.""" - return any( - block.rect.collidepoint(x, y) for block in self.group if block != self - ) - - def _get_collided_block(self, x: int, y: int) -> Union["Block", None]: - """Get the block that collides with the given block.""" - - return next( - ( - block - for block in self.group - if block != self and block.rect.collidepoint(x, y) - ), - None, - ) - - def _can_merge(self, other: "Block") -> bool: - """Check if the block can merge with another block.""" - return self.value == other.value - - def _merge(self, other: "Block") -> int: - """Merge the block with another block.""" - 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 block""" - self.draw() - - def can_move(self) -> bool: - """Check if the block can move""" - for direction in Direction: - new_x, new_y = self._calc_new_pos(direction) - if not self._is_out_if_bounds(new_x, new_y) and self._has_collision( - new_x, new_y - ): - collided_block = self._get_collided_block(new_x, new_y) - if collided_block and self._can_merge(collided_block): - return True - return False - - def _get_color(self) -> ColorScheme: - """Change the color of the block based on its value""" - color_map = { - 2: Config.COLORSCHEME.BLOCK_2, - 4: Config.COLORSCHEME.BLOCK_4, - 8: Config.COLORSCHEME.BLOCK_8, - 16: Config.COLORSCHEME.BLOCK_16, - 32: Config.COLORSCHEME.BLOCK_32, - 64: Config.COLORSCHEME.BLOCK_64, - 128: Config.COLORSCHEME.BLOCK_128, - 256: Config.COLORSCHEME.BLOCK_256, - 512: Config.COLORSCHEME.BLOCK_512, - 1024: Config.COLORSCHEME.BLOCK_1024, - 2048: Config.COLORSCHEME.BLOCK_2048, - } - return color_map.get(self.value, Config.COLORSCHEME.BLOCK_ELSE) - - def __repr__(self) -> str: - """Return a string representation of the block""" - return f"Block({id(self)}): {self.pos} num={self.value}" - - def __str__(self) -> str: - """Return a string representation of the block""" - return self.__repr__() - - def __hash__(self) -> int: - """Return a hash of the block""" - return hash((self.rect.x, self.rect.y, self.value)) - - @property - def pos(self) -> tuple[int, int]: - """Return the position of the block""" - return grid_pos(self.rect.x), grid_pos(self.rect.y) diff --git a/src/py2048/color.py b/src/py2048/color.py index 5f99fc5..646a050 100644 --- a/src/py2048/color.py +++ b/src/py2048/color.py @@ -2,19 +2,19 @@ from enum import Enum class Original: - BLOCK_0 = "#cdc1b4" - BLOCK_2 = "#eee4da" - BLOCK_4 = "#eee1c9" - BLOCK_8 = "#f3b27a" - BLOCK_16 = "#f69664" - BLOCK_32 = "#f77c5f" - BLOCK_64 = "#f75f3b" - BLOCK_128 = "#edcf72" - BLOCK_256 = "#edcc61" - BLOCK_512 = "#edc850" - BLOCK_1024 = "#edc53f" - BLOCK_2048 = "#edc22e" - BLOCK_ELSE = "#ff0000" + TILE_0 = "#cdc1b4" + TILE_2 = "#eee4da" + TILE_4 = "#eee1c9" + TILE_8 = "#f3b27a" + TILE_16 = "#f69664" + TILE_32 = "#f77c5f" + TILE_64 = "#f75f3b" + TILE_128 = "#edcf72" + TILE_256 = "#edcc61" + TILE_512 = "#edc850" + TILE_1024 = "#edc53f" + TILE_2048 = "#edc22e" + TILE_ELSE = "#ff0000" LIGHT_TEXT = "#f9f6f2" DARK_TEXT = "#776e65" OTHER = "#000000" diff --git a/src/py2048/config.py b/src/py2048/config.py index 5dbc36c..3fe1a10 100644 --- a/src/py2048/config.py +++ b/src/py2048/config.py @@ -6,23 +6,22 @@ class Config: FONT_SIZE = 32 COLORSCHEME = ColorScheme.ORIGINAL.value - BLOCK_SIZE = 75 - BLOCK_BORDER_WIDTH = BLOCK_SIZE // 20 - BLOCK_BORDER_RADIUS = BLOCK_SIZE // 10 + 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 * BLOCK_SIZE - BOARD_HEIGHT = BOARD_SIZE * BLOCK_SIZE + BOARD_WIDTH = BOARD_SIZE * TILE_SIZE + BOARD_HEIGHT = BOARD_SIZE * TILE_SIZE - HEADER_WIDTH = BOARD_WIDTH + BLOCK_SIZE - HEADER_HEIGHT = BLOCK_SIZE + HEADER_WIDTH = BOARD_WIDTH + TILE_SIZE + HEADER_HEIGHT = TILE_SIZE - BOARD_X = BLOCK_SIZE // 2 - BOARD_Y = HEADER_HEIGHT + BLOCK_SIZE // 2 + BOARD_X = TILE_SIZE // 2 + BOARD_Y = HEADER_HEIGHT + TILE_SIZE // 2 SCREEN_WIDTH = HEADER_WIDTH - SCREEN_HEIGHT = BOARD_HEIGHT + BLOCK_SIZE + HEADER_HEIGHT + SCREEN_HEIGHT = BOARD_HEIGHT + TILE_SIZE + HEADER_HEIGHT SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT - - INITIAL_BLOCK_COUNT = 2 - BLOCK_VALUE_PROBABILITY = 0.9 diff --git a/src/py2048/game.py b/src/py2048/game.py index 29d624a..5abd3a5 100644 --- a/src/py2048/game.py +++ b/src/py2048/game.py @@ -3,17 +3,15 @@ import sys import pygame from loguru import logger -from .board import Board from .config import Config -from .logger import setup_logger -from .screens.header import Header -from .screens.menu import Menu -from .utils import Direction +from .objects import Board +from .screens import Header, Menu +from .utils import Direction, _setup_logger class Game: def __init__(self) -> None: - setup_logger() + _setup_logger() logger.info("Initializing game") pygame.init() diff --git a/src/py2048/logger.py b/src/py2048/logger.py deleted file mode 100644 index b21592c..0000000 --- a/src/py2048/logger.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path - -from loguru import logger - -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", - ) diff --git a/src/py2048/objects/__init__.py b/src/py2048/objects/__init__.py new file mode 100644 index 0000000..26433c2 --- /dev/null +++ b/src/py2048/objects/__init__.py @@ -0,0 +1,7 @@ +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"] diff --git a/src/py2048/board.py b/src/py2048/objects/board.py similarity index 52% rename from src/py2048/board.py rename to src/py2048/objects/board.py index 730d7c4..734f1e8 100644 --- a/src/py2048/board.py +++ b/src/py2048/objects/board.py @@ -3,9 +3,9 @@ import random import pygame from loguru import logger -from .block import Block -from .config import Config -from .utils import Direction +from py2048 import Config, Direction + +from .tile import Tile class Board(pygame.sprite.Group): @@ -18,11 +18,11 @@ class Board(pygame.sprite.Group): def initiate_game(self) -> None: """Initiate the game.""" - self.generate_initial_blocks() + self.generate_initial_tiles() def draw(self, surface: pygame.Surface) -> None: """Draw the board.""" - block: Block + tile: Tile self._draw_background(surface) super().draw(surface) @@ -33,69 +33,69 @@ class Board(pygame.sprite.Group): surface, Config.COLORSCHEME.BOARD_BG, self.rect, - border_radius=Config.BLOCK_BORDER_RADIUS, + border_radius=Config.TILE_BORDER_RADIUS, ) # background pygame.draw.rect( surface, Config.COLORSCHEME.BOARD_BG, self.rect, - width=Config.BLOCK_BORDER_WIDTH, - border_radius=Config.BLOCK_BORDER_RADIUS, + width=Config.TILE_BORDER_WIDTH, + border_radius=Config.TILE_BORDER_RADIUS, ) # border def move(self, direction: Direction) -> int: - """Move the blocks in the specified direction.""" + """Move the tiles in the specified direction.""" score = 0 - blocks = self.sprites() - block: Block + tiles = self.sprites() + tile: Tile match direction: case Direction.UP: - blocks.sort(key=lambda block: block.rect.y) + tiles.sort(key=lambda tile: tile.rect.y) case Direction.DOWN: - blocks.sort(key=lambda block: block.rect.y, reverse=True) + tiles.sort(key=lambda tile: tile.rect.y, reverse=True) case Direction.LEFT: - blocks.sort(key=lambda block: block.rect.x) + tiles.sort(key=lambda tile: tile.rect.x) case Direction.RIGHT: - blocks.sort(key=lambda block: block.rect.x, reverse=True) + tiles.sort(key=lambda tile: tile.rect.x, reverse=True) - for block in blocks: - score += block.move(direction) + for tile in tiles: + score += tile.move(direction) if not self._is_full(): - self.generate_random_block() + self.generate_random_tile() return score - def generate_initial_blocks(self) -> None: - """Generate the initial blocks.""" - self.generate_block(Config.INITIAL_BLOCK_COUNT) + def generate_initial_tiles(self) -> None: + """Generate the initial tiles.""" + self.generate_tile(Config.INITIAL_TILE_COUNT) - def generate_block(self, amount: int = 1, *pos: tuple[int, int]) -> None: - """Generate `amount` number of blocks or at the specified positions.""" + def generate_tile(self, amount: int = 1, *pos: tuple[int, int]) -> None: + """Generate `amount` number of tiles or at the specified positions.""" if pos: for coords in pos: - x, y = coords[0] * Config.BLOCK_SIZE, coords[1] * Config.BLOCK_SIZE - self.add(Block(x, y, self)) + x, y = coords[0] * Config.TILE_SIZE, coords[1] * Config.TILE_SIZE + self.add(Tile(x, y, self)) return for _ in range(amount): - self.generate_random_block() + self.generate_random_tile() - def generate_random_block(self) -> None: - """Generate a block with random coordinates aligned with the grid.""" + def generate_random_tile(self) -> None: + """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.BLOCK_SIZE + Config.BOARD_X - y = random.randint(0, 3) * Config.BLOCK_SIZE + Config.BOARD_Y - block = Block(x, y, self) + 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) - colliding_blocks = pygame.sprite.spritecollide( - block, self, False + colliding_tiles = pygame.sprite.spritecollide( + tile, self, False ) # check for collisions - if not colliding_blocks: - self.add(block) + if not colliding_tiles: + self.add(tile) return def _is_full(self) -> bool: @@ -104,9 +104,9 @@ class Board(pygame.sprite.Group): def _can_move(self) -> bool: """Check if any movement is possible on the board.""" - block: Block - for block in self.sprites(): - if block.can_move(): + tile: Tile + for tile in self.sprites(): + if tile.can_move(): return True return False diff --git a/src/py2048/screens/elements/button.py b/src/py2048/objects/button.py similarity index 58% rename from src/py2048/screens/elements/button.py rename to src/py2048/objects/button.py index 54ebbb9..4657ee3 100644 --- a/src/py2048/screens/elements/button.py +++ b/src/py2048/objects/button.py @@ -2,26 +2,32 @@ import sys import pygame from attrs import define, field + from py2048.color import ColorScheme from py2048.config import Config @define -class Button: - text: str = field() - font_family: str = field() - font_size: int = field() - font_color: ColorScheme = field() - position: tuple[int, int] = field() - width: int = field() - height: int = field() - action = field() - bg_color: ColorScheme = field() - hover_color: ColorScheme = field() - 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) +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() + + # 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 __attrs_post_init__(self) -> None: """Initialize the button.""" @@ -57,4 +63,9 @@ class Button: def _draw_rect(self, surface: pygame.Surface, color: ColorScheme) -> None: """Draw the button rectangle.""" - pygame.draw.rect(surface, self.bg_color, self.rect) + pygame.draw.rect( + surface, + self.bg_color, + self.rect, + border_radius=Config.TILE_BORDER_RADIUS, + ) diff --git a/src/py2048/screens/elements/label.py b/src/py2048/objects/label.py similarity index 100% rename from src/py2048/screens/elements/label.py rename to src/py2048/objects/label.py diff --git a/src/py2048/objects/sprite.py b/src/py2048/objects/sprite.py new file mode 100644 index 0000000..0bca812 --- /dev/null +++ b/src/py2048/objects/sprite.py @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..fdaafaf --- /dev/null +++ b/src/py2048/objects/tile.py @@ -0,0 +1,180 @@ +import random +from typing import Union + +import pygame + +from py2048 import ColorScheme, Config, Direction + +from .sprite import Sprite + + +def _grid_pos(pos: int) -> int: + """Return the position in the grid.""" + return pos // Config.TILE_SIZE + 1 + + +class Tile(Sprite): + def __init__( + self, x: int, y: int, group: pygame.sprite.Group, value: int | None = 2 + ): + super().__init__(x, y, group) + + self.value: int = ( + value + if value is not None + else 2 + if random.random() <= Config.TILE_VALUE_PROBABILITY + else 4 + ) + 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() + + 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) + pygame.draw.rect( + 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 + + def _create_surface(self) -> pygame.Surface: + """Create a surface for the tile.""" + sprite_surface = pygame.Surface( + (Config.TILE_SIZE, Config.TILE_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 + while True: + new_x, new_y = self._calc_new_pos(direction) + + if self._is_out_if_bounds(new_x, new_y): + return score + + 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) + else: + return score + + self.group.remove(self) + self.rect.topleft = new_x, new_y + self.group.add(self) + + def _calc_new_pos(self, direction: Direction) -> tuple[int, int]: + """Calculate the new position of the tile.""" + 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 + return not (board_left <= x <= board_right and board_top <= y <= board_bottom) + + def _has_collision(self, x: int, y: int) -> bool: + """Checks whether the tile has a collision with any other tile.""" + return any(tile.rect.collidepoint(x, y) for tile in self.group if tile != self) + + def _get_collided_tile(self, x: int, y: int) -> Union["Tile", None]: + """Get the tile that collides with the given tile.""" + + return next( + ( + tile + for tile in self.group + if tile != self and tile.rect.collidepoint(x, y) + ), + None, + ) + + def _can_merge(self, other: "Tile") -> bool: + """Check if the tile can merge with another tile.""" + return self.value == other.value + + def _merge(self, other: "Tile") -> int: + """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""" + for direction in Direction: + new_x, new_y = self._calc_new_pos(direction) + if not self._is_out_if_bounds(new_x, new_y) and 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): + return True + return False + + def _get_color(self) -> ColorScheme: + """Change the color of the tile based on its value""" + color_map = { + 2: Config.COLORSCHEME.TILE_2, + 4: Config.COLORSCHEME.TILE_4, + 8: Config.COLORSCHEME.TILE_8, + 16: Config.COLORSCHEME.TILE_16, + 32: Config.COLORSCHEME.TILE_32, + 64: Config.COLORSCHEME.TILE_64, + 128: Config.COLORSCHEME.TILE_128, + 256: Config.COLORSCHEME.TILE_256, + 512: Config.COLORSCHEME.TILE_512, + 1024: Config.COLORSCHEME.TILE_1024, + 2048: Config.COLORSCHEME.TILE_2048, + } + return color_map.get(self.value, Config.COLORSCHEME.TILE_ELSE) + + def __repr__(self) -> str: + """Return a string representation of the tile.""" + return f"Tile({id(self)}): {self.pos} num={self.value}" + + def __str__(self) -> str: + """Return a string representation of the tile.""" + return self.__repr__() + + def __hash__(self) -> int: + """Return a hash of the tile.""" + return hash((self.rect.x, self.rect.y, self.value)) + + @property + def pos(self) -> tuple[int, int]: + """Return the position of the tile.""" + return _grid_pos(self.rect.x), _grid_pos(self.rect.y) diff --git a/src/py2048/screens/__init__.py b/src/py2048/screens/__init__.py new file mode 100644 index 0000000..197931a --- /dev/null +++ b/src/py2048/screens/__init__.py @@ -0,0 +1,4 @@ +from .header import Header +from .menu import Menu + +__all__ = ["Menu", "Header"] diff --git a/src/py2048/screens/header.py b/src/py2048/screens/header.py index 073980c..ea9811f 100644 --- a/src/py2048/screens/header.py +++ b/src/py2048/screens/header.py @@ -1,7 +1,6 @@ import pygame -from py2048.config import Config - -from .elements.label import Label +from py2048 import Config +from py2048.objects import Label class Header: diff --git a/src/py2048/screens/menu.py b/src/py2048/screens/menu.py index 0120258..69a7ccc 100644 --- a/src/py2048/screens/menu.py +++ b/src/py2048/screens/menu.py @@ -1,37 +1,37 @@ import pygame from loguru import logger -from py2048.config import Config -from .elements.button import Button +from py2048 import Config +from py2048.objects import Button class Menu: def __init__(self): + buttons_data = { + "Play": self.play, + "AI": self.ai, + "Settings": self.settings, + "Exit": self.exit, + } + buttons_width, button_height = 120, 50 self.buttons = [ Button( - "Play", - Config.FONT_FAMILY, - Config.FONT_SIZE, - Config.COLORSCHEME.LIGHT_TEXT, - (Config.SCREEN_WIDTH / 2 - 50, Config.SCREEN_HEIGHT / 2 - 100), - 100, - 50, - self.play, - Config.COLORSCHEME.BOARD_BG, - Config.COLORSCHEME.BLOCK_0, - ), - Button( - "Exit", - Config.FONT_FAMILY, - Config.FONT_SIZE, - Config.COLORSCHEME.LIGHT_TEXT, - (Config.SCREEN_WIDTH / 2 - 50, Config.SCREEN_HEIGHT / 2), - 100, - 50, - self.exit, - Config.COLORSCHEME.BOARD_BG, - Config.COLORSCHEME.BLOCK_0, - ), + 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, + ), + width=buttons_width, + height=button_height, + action=action, + bg_color=Config.COLORSCHEME.BOARD_BG, + hover_color=Config.COLORSCHEME.TILE_0, + ) + for index, (text, action) in enumerate(buttons_data.items(), start=1) ] def _handle_events(self, event: pygame.event.Event) -> None: @@ -49,5 +49,11 @@ class Menu: def play(self) -> None: logger.debug("Play") + def ai(self) -> None: + logger.debug("AI") + + def settings(self) -> None: + logger.debug("Settings") + def exit(self) -> None: logger.debug("Exit") diff --git a/src/py2048/utils.py b/src/py2048/utils.py old mode 100755 new mode 100644 index 3de342c..25a82e3 --- a/src/py2048/utils.py +++ b/src/py2048/utils.py @@ -1,11 +1,21 @@ 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 grid_pos(pos: int) -> int: - """Return the position in the grid.""" - return pos // Config.BLOCK_SIZE + 1 + +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):