[major] refactor(game): add abstract classes

I don't know anymore where and what changed
This commit is contained in:
Kristofers Solo 2024-01-02 22:59:30 +02:00
parent c03be8f3cf
commit 624401d27b
22 changed files with 404 additions and 256 deletions

View File

@ -2,7 +2,7 @@
from loguru import logger
from py2048.game import Game
from py2048 import Game
@logger.catch

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

18
src/py2048/utils/enums.py Normal file
View File

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