refactor(game): rename Block to Tile and organize files

This commit is contained in:
Kristofers Solo 2024-01-02 16:30:27 +02:00
parent 61976e40aa
commit c03be8f3cf
16 changed files with 375 additions and 310 deletions

View File

@ -0,0 +1,5 @@
from .color import ColorScheme
from .config import Config
from .utils import BASE_PATH, Direction
__all__ = ["Direction", "ColorScheme", "Config", "BASE_PATH"]

View File

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

View File

@ -2,19 +2,19 @@ from enum import Enum
class Original: class Original:
BLOCK_0 = "#cdc1b4" TILE_0 = "#cdc1b4"
BLOCK_2 = "#eee4da" TILE_2 = "#eee4da"
BLOCK_4 = "#eee1c9" TILE_4 = "#eee1c9"
BLOCK_8 = "#f3b27a" TILE_8 = "#f3b27a"
BLOCK_16 = "#f69664" TILE_16 = "#f69664"
BLOCK_32 = "#f77c5f" TILE_32 = "#f77c5f"
BLOCK_64 = "#f75f3b" TILE_64 = "#f75f3b"
BLOCK_128 = "#edcf72" TILE_128 = "#edcf72"
BLOCK_256 = "#edcc61" TILE_256 = "#edcc61"
BLOCK_512 = "#edc850" TILE_512 = "#edc850"
BLOCK_1024 = "#edc53f" TILE_1024 = "#edc53f"
BLOCK_2048 = "#edc22e" TILE_2048 = "#edc22e"
BLOCK_ELSE = "#ff0000" TILE_ELSE = "#ff0000"
LIGHT_TEXT = "#f9f6f2" LIGHT_TEXT = "#f9f6f2"
DARK_TEXT = "#776e65" DARK_TEXT = "#776e65"
OTHER = "#000000" OTHER = "#000000"

View File

@ -6,23 +6,22 @@ class Config:
FONT_SIZE = 32 FONT_SIZE = 32
COLORSCHEME = ColorScheme.ORIGINAL.value COLORSCHEME = ColorScheme.ORIGINAL.value
BLOCK_SIZE = 75 TILE_SIZE = 75
BLOCK_BORDER_WIDTH = BLOCK_SIZE // 20 TILE_BORDER_WIDTH = TILE_SIZE // 20
BLOCK_BORDER_RADIUS = BLOCK_SIZE // 10 TILE_BORDER_RADIUS = TILE_SIZE // 10
INITIAL_TILE_COUNT = 2
TILE_VALUE_PROBABILITY = 0.9
BOARD_SIZE = 4 BOARD_SIZE = 4
BOARD_WIDTH = BOARD_SIZE * BLOCK_SIZE BOARD_WIDTH = BOARD_SIZE * TILE_SIZE
BOARD_HEIGHT = BOARD_SIZE * BLOCK_SIZE BOARD_HEIGHT = BOARD_SIZE * TILE_SIZE
HEADER_WIDTH = BOARD_WIDTH + BLOCK_SIZE HEADER_WIDTH = BOARD_WIDTH + TILE_SIZE
HEADER_HEIGHT = BLOCK_SIZE HEADER_HEIGHT = TILE_SIZE
BOARD_X = BLOCK_SIZE // 2 BOARD_X = TILE_SIZE // 2
BOARD_Y = HEADER_HEIGHT + BLOCK_SIZE // 2 BOARD_Y = HEADER_HEIGHT + TILE_SIZE // 2
SCREEN_WIDTH = HEADER_WIDTH 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 SCREEN_SIZE = SCREEN_WIDTH, SCREEN_HEIGHT
INITIAL_BLOCK_COUNT = 2
BLOCK_VALUE_PROBABILITY = 0.9

View File

@ -3,17 +3,15 @@ import sys
import pygame import pygame
from loguru import logger from loguru import logger
from .board import Board
from .config import Config from .config import Config
from .logger import setup_logger from .objects import Board
from .screens.header import Header from .screens import Header, Menu
from .screens.menu import Menu from .utils import Direction, _setup_logger
from .utils import Direction
class Game: class Game:
def __init__(self) -> None: def __init__(self) -> None:
setup_logger() _setup_logger()
logger.info("Initializing game") logger.info("Initializing game")
pygame.init() pygame.init()

View File

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

View File

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

View File

@ -3,9 +3,9 @@ import random
import pygame import pygame
from loguru import logger from loguru import logger
from .block import Block from py2048 import Config, Direction
from .config import Config
from .utils import Direction from .tile import Tile
class Board(pygame.sprite.Group): class Board(pygame.sprite.Group):
@ -18,11 +18,11 @@ class Board(pygame.sprite.Group):
def initiate_game(self) -> None: def initiate_game(self) -> None:
"""Initiate the game.""" """Initiate the game."""
self.generate_initial_blocks() self.generate_initial_tiles()
def draw(self, surface: pygame.Surface) -> None: def draw(self, surface: pygame.Surface) -> None:
"""Draw the board.""" """Draw the board."""
block: Block tile: Tile
self._draw_background(surface) self._draw_background(surface)
super().draw(surface) super().draw(surface)
@ -33,69 +33,69 @@ class Board(pygame.sprite.Group):
surface, surface,
Config.COLORSCHEME.BOARD_BG, Config.COLORSCHEME.BOARD_BG,
self.rect, self.rect,
border_radius=Config.BLOCK_BORDER_RADIUS, border_radius=Config.TILE_BORDER_RADIUS,
) # background ) # background
pygame.draw.rect( pygame.draw.rect(
surface, surface,
Config.COLORSCHEME.BOARD_BG, Config.COLORSCHEME.BOARD_BG,
self.rect, self.rect,
width=Config.BLOCK_BORDER_WIDTH, width=Config.TILE_BORDER_WIDTH,
border_radius=Config.BLOCK_BORDER_RADIUS, border_radius=Config.TILE_BORDER_RADIUS,
) # border ) # border
def move(self, direction: Direction) -> int: def move(self, direction: Direction) -> int:
"""Move the blocks in the specified direction.""" """Move the tiles in the specified direction."""
score = 0 score = 0
blocks = self.sprites() tiles = self.sprites()
block: Block tile: Tile
match direction: match direction:
case Direction.UP: case Direction.UP:
blocks.sort(key=lambda block: block.rect.y) tiles.sort(key=lambda tile: tile.rect.y)
case Direction.DOWN: 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: case Direction.LEFT:
blocks.sort(key=lambda block: block.rect.x) tiles.sort(key=lambda tile: tile.rect.x)
case Direction.RIGHT: 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: for tile in tiles:
score += block.move(direction) score += tile.move(direction)
if not self._is_full(): if not self._is_full():
self.generate_random_block() self.generate_random_tile()
return score return score
def generate_initial_blocks(self) -> None: def generate_initial_tiles(self) -> None:
"""Generate the initial blocks.""" """Generate the initial tiles."""
self.generate_block(Config.INITIAL_BLOCK_COUNT) self.generate_tile(Config.INITIAL_TILE_COUNT)
def generate_block(self, amount: int = 1, *pos: tuple[int, int]) -> None: def generate_tile(self, amount: int = 1, *pos: tuple[int, int]) -> None:
"""Generate `amount` number of blocks or at the specified positions.""" """Generate `amount` number of tiles or at the specified positions."""
if pos: if pos:
for coords in pos: for coords in pos:
x, y = coords[0] * Config.BLOCK_SIZE, coords[1] * Config.BLOCK_SIZE x, y = coords[0] * Config.TILE_SIZE, coords[1] * Config.TILE_SIZE
self.add(Block(x, y, self)) self.add(Tile(x, y, self))
return return
for _ in range(amount): for _ in range(amount):
self.generate_random_block() self.generate_random_tile()
def generate_random_block(self) -> None: def generate_random_tile(self) -> None:
"""Generate a block with random coordinates aligned with the grid.""" """Generate a tile with random coordinates aligned with the grid."""
while True: while True:
# Generate random coordinates aligned with the grid # Generate random coordinates aligned with the grid
x = random.randint(0, 3) * Config.BLOCK_SIZE + Config.BOARD_X x = random.randint(0, 3) * Config.TILE_SIZE + Config.BOARD_X
y = random.randint(0, 3) * Config.BLOCK_SIZE + Config.BOARD_Y y = random.randint(0, 3) * Config.TILE_SIZE + Config.BOARD_Y
block = Block(x, y, self) tile = Tile(x, y, self)
colliding_blocks = pygame.sprite.spritecollide( colliding_tiles = pygame.sprite.spritecollide(
block, self, False tile, self, False
) # check for collisions ) # check for collisions
if not colliding_blocks: if not colliding_tiles:
self.add(block) self.add(tile)
return return
def _is_full(self) -> bool: def _is_full(self) -> bool:
@ -104,9 +104,9 @@ class Board(pygame.sprite.Group):
def _can_move(self) -> bool: def _can_move(self) -> bool:
"""Check if any movement is possible on the board.""" """Check if any movement is possible on the board."""
block: Block tile: Tile
for block in self.sprites(): for tile in self.sprites():
if block.can_move(): if tile.can_move():
return True return True
return False return False

View File

@ -2,26 +2,32 @@ import sys
import pygame import pygame
from attrs import define, field from attrs import define, field
from py2048.color import ColorScheme from py2048.color import ColorScheme
from py2048.config import Config from py2048.config import Config
@define @define
class Button: class Button(pygame.sprite.Sprite):
text: str = field() def __init__(self, text: str, x: int, y: int, group: pygame.sprite.Group):
font_family: str = field() super().__init__()
font_size: int = field() self.text = text
font_color: ColorScheme = field() self.image = self._create_button_surface()
position: tuple[int, int] = field()
width: int = field() # text: str = field(kw_only=True)
height: int = field() # font_family: str = field(kw_only=True)
action = field() # font_size: int = field(kw_only=True)
bg_color: ColorScheme = field() # font_color: ColorScheme = field(kw_only=True)
hover_color: ColorScheme = field() # position: tuple[int, int] = field(kw_only=True)
font: pygame.Font = field(init=False) # width: int = field(kw_only=True)
rendered_text: pygame.Surface = field(init=False) # height: int = field(kw_only=True)
rect: pygame.Rect = field(init=False) # action = field(kw_only=True)
is_hovered: bool = field(init=False, default=False) # 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: def __attrs_post_init__(self) -> None:
"""Initialize the button.""" """Initialize the button."""
@ -57,4 +63,9 @@ class Button:
def _draw_rect(self, surface: pygame.Surface, color: ColorScheme) -> None: def _draw_rect(self, surface: pygame.Surface, color: ColorScheme) -> None:
"""Draw the button rectangle.""" """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,
)

View File

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

180
src/py2048/objects/tile.py Normal file
View File

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

View File

@ -0,0 +1,4 @@
from .header import Header
from .menu import Menu
__all__ = ["Menu", "Header"]

View File

@ -1,7 +1,6 @@
import pygame import pygame
from py2048.config import Config from py2048 import Config
from py2048.objects import Label
from .elements.label import Label
class Header: class Header:

View File

@ -1,37 +1,37 @@
import pygame import pygame
from loguru import logger 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: class Menu:
def __init__(self): 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 = [ self.buttons = [
Button( Button(
"Play", text=text,
Config.FONT_FAMILY, font_family=Config.FONT_FAMILY,
Config.FONT_SIZE, font_size=Config.FONT_SIZE,
Config.COLORSCHEME.LIGHT_TEXT, font_color=Config.COLORSCHEME.LIGHT_TEXT,
(Config.SCREEN_WIDTH / 2 - 50, Config.SCREEN_HEIGHT / 2 - 100), position=(
100, Config.SCREEN_WIDTH / 2 - 50,
50, Config.SCREEN_HEIGHT / (len(buttons_data) + 1) * index
self.play, - button_height,
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,
), ),
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: def _handle_events(self, event: pygame.event.Event) -> None:
@ -49,5 +49,11 @@ class Menu:
def play(self) -> None: def play(self) -> None:
logger.debug("Play") logger.debug("Play")
def ai(self) -> None:
logger.debug("AI")
def settings(self) -> None:
logger.debug("Settings")
def exit(self) -> None: def exit(self) -> None:
logger.debug("Exit") logger.debug("Exit")

16
src/py2048/utils.py Executable file → Normal file
View File

@ -1,11 +1,21 @@
from enum import Enum from enum import Enum
from pathlib import Path
from loguru import logger
from .config import Config from .config import Config
BASE_PATH = Path(__file__).resolve().parent.parent.parent
def grid_pos(pos: int) -> int:
"""Return the position in the grid.""" def _setup_logger() -> None:
return pos // Config.BLOCK_SIZE + 1 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): class Direction(Enum):