mirror of
https://github.com/kristoferssolo/2048.git
synced 2025-10-21 15:20:35 +00:00
refactor(game): rename Block to Tile and organize files
This commit is contained in:
parent
61976e40aa
commit
c03be8f3cf
@ -0,0 +1,5 @@
|
|||||||
|
from .color import ColorScheme
|
||||||
|
from .config import Config
|
||||||
|
from .utils import BASE_PATH, Direction
|
||||||
|
|
||||||
|
__all__ = ["Direction", "ColorScheme", "Config", "BASE_PATH"]
|
||||||
@ -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)
|
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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",
|
|
||||||
)
|
|
||||||
7
src/py2048/objects/__init__.py
Normal file
7
src/py2048/objects/__init__.py
Normal 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"]
|
||||||
@ -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
|
||||||
|
|
||||||
@ -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,
|
||||||
|
)
|
||||||
39
src/py2048/objects/sprite.py
Normal file
39
src/py2048/objects/sprite.py
Normal 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
180
src/py2048/objects/tile.py
Normal 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)
|
||||||
4
src/py2048/screens/__init__.py
Normal file
4
src/py2048/screens/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .header import Header
|
||||||
|
from .menu import Menu
|
||||||
|
|
||||||
|
__all__ = ["Menu", "Header"]
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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,
|
width=buttons_width,
|
||||||
),
|
height=button_height,
|
||||||
Button(
|
action=action,
|
||||||
"Exit",
|
bg_color=Config.COLORSCHEME.BOARD_BG,
|
||||||
Config.FONT_FAMILY,
|
hover_color=Config.COLORSCHEME.TILE_0,
|
||||||
Config.FONT_SIZE,
|
)
|
||||||
Config.COLORSCHEME.LIGHT_TEXT,
|
for index, (text, action) in enumerate(buttons_data.items(), start=1)
|
||||||
(Config.SCREEN_WIDTH / 2 - 50, Config.SCREEN_HEIGHT / 2),
|
|
||||||
100,
|
|
||||||
50,
|
|
||||||
self.exit,
|
|
||||||
Config.COLORSCHEME.BOARD_BG,
|
|
||||||
Config.COLORSCHEME.BLOCK_0,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
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
16
src/py2048/utils.py
Executable file → Normal 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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user