2048/src/py2048/objects/tile.py
2024-01-03 00:49:09 +02:00

206 lines
7.0 KiB
Python

import random
from typing import Union
import pygame
from loguru import logger
from py2048 import Config
from py2048.utils import ColorScheme, Direction, Position, Size
from .abc import MovableUIElement, UIElement
def _grid_pos(pos: int) -> int:
"""Return the position in the grid."""
return pos // Config.TILE.size + 1
class Tile(MovableUIElement, pygame.sprite.Sprite):
def __init__(
self,
position: Position,
group: pygame.sprite.Group,
):
pygame.sprite.Sprite.__init__(self)
self.value = 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 = self.position
self.update()
def draw(self, surface: pygame.Surface) -> None:
"""Draw the value of the tile."""
self._draw_background(surface)
self._draw_text()
self.image.blit(self.image, (0, 0))
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 on the given surface."""
if self.size:
pygame.draw.rect(
surface,
self._get_color(),
(0, 0, *self.size),
border_radius=Config.TILE.border.radius,
)
self._draw_border(surface)
def _draw_border(self, surface: pygame.Surface) -> None:
"""Draw a rounded border on the given surface."""
if self.size:
pygame.draw.rect(
surface,
(0, 0, 0, 0),
(0, 0, *self.size),
border_radius=Config.TILE.border.radius,
width=Config.TILE.border.width,
)
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."""
surface = pygame.Surface(self.size, pygame.SRCALPHA)
self._draw_background(surface)
return surface
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
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):
self._merge(collided_tile)
else:
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
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.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:
"""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") -> 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)
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) -> str:
"""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) -> Position:
"""Return the position of the tile."""
return Position(_grid_pos(self.rect.x), _grid_pos(self.rect.y))