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

View File

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

View File

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

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

View File

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

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
from py2048.config import Config
from .elements.label import Label
from py2048 import Config
from py2048.objects import Label
class Header:

View File

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

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

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