diff --git a/src/game/__init__.py b/src/game/__init__.py index 4fab904..de7f5b2 100644 --- a/src/game/__init__.py +++ b/src/game/__init__.py @@ -1,9 +1,4 @@ from .log import log -from .screens import Game, Main, Tetris +from .screens import Game, Main, Preview, Score, Tetris -__all__ = [ - "log", - "Main", - "Game", - "Preview", -] +__all__ = ["Main", "Game", "Preview", "Score", "Tetris", "log"] diff --git a/src/game/screens/game.py b/src/game/screens/game.py index 6836d6a..f015946 100644 --- a/src/game/screens/game.py +++ b/src/game/screens/game.py @@ -4,7 +4,7 @@ import pygame from utils import CONFIG, Figure, GameMode from game.log import log -from game.sprites.tetromino import Tetromino +from game.sprites import Tetromino from .base import BaseScreen from .preview import Preview @@ -24,7 +24,7 @@ class Game(BaseScreen): score: Score object. preview: Preview object. next_figures: List of upcoming figures. - music: Pygame music that plays in the background. + music: Music that plays in the background. """ def __init__(self, game_mode: GameMode) -> None: diff --git a/src/game/screens/tetris.py b/src/game/screens/tetris.py index 817503d..f029f80 100644 --- a/src/game/screens/tetris.py +++ b/src/game/screens/tetris.py @@ -5,8 +5,7 @@ import pygame from utils import CONFIG, Direction, Figure, GameMode, Rotation from game.log import log -from game.sprites.block import Block -from game.sprites.tetromino import Tetromino +from game.sprites import Block, Tetromino from game.timer import Timer, Timers from .base import BaseScreen, SceenElement @@ -91,31 +90,61 @@ class Tetris(BaseScreen): self._handle_down_key(keys) self._handle_drop_key(keys) - def move_down(self) -> None: - """Move the current tetromino down.""" - self.tetromino.move_down() + def move_down(self) -> bool: + """ + Move the current tetromino down. - def move_left(self) -> None: - """Move the current tetromino to the left.""" - self.tetromino.move_horizontal(Direction.LEFT) + Returns: + True if the movement was successful, False otherwise. + """ + return self.tetromino.move_down() - def move_right(self) -> None: - """Move the current tetromino to the right.""" - self.tetromino.move_horizontal(Direction.RIGHT) + def move_left(self) -> bool: + """ + Move the current tetromino to the left. - def rotate(self) -> None: - """Rotate the current tetromino clockwise.""" - self.tetromino.rotate() + Returns: + True if the movement was successful, False otherwise. + """ + return self.tetromino.move_horizontal(Direction.LEFT) - def rotate_reverse(self) -> None: - """Rotate the current tetromino counter-clockwise.""" - self.tetromino.rotate(Rotation.COUNTER_CLOCKWISE) + def move_right(self) -> bool: + """ + Move the current tetromino to the right. - def drop(self) -> None: - """Drop the current tetromino.""" - self.tetromino.drop() + Returns: + True if the movement was successful, False otherwise. + """ + return self.tetromino.move_horizontal(Direction.RIGHT) - def create_new_tetromino(self) -> None: + def rotate(self) -> bool: + """ + Rotate the current tetromino clockwise. + + Returns: + True if the rotation was successful, False otherwise. + """ + return self.tetromino.rotate() + + def rotate_reverse(self) -> bool: + """ + Rotate the current tetromino counter-clockwise. + + Returns: + True if the rotation was successful, False otherwise. + """ + return self.tetromino.rotate(Rotation.COUNTER_CLOCKWISE) + + def drop(self) -> bool: + """ + Drop the current tetromino. + + Returns: + True if the movement was successful, False otherwise. + """ + return self.tetromino.drop() + + def create_new_tetromino(self, shape: Optional[Figure] = None) -> Tetromino: """Create a new tetromino and perform necessary actions.""" self._play_landing_sound() self._check_finished_rows() @@ -128,9 +157,11 @@ class Tetris(BaseScreen): self.sprites, self.create_new_tetromino, self.field, - self.get_next_figure(), + shape or self.get_next_figure(), ) + return self.tetromino + def _check_game_over(self) -> bool: """ Check if the game is over. diff --git a/src/game/sprites/tetromino.py b/src/game/sprites/tetromino.py index d280aa2..5da521e 100644 --- a/src/game/sprites/tetromino.py +++ b/src/game/sprites/tetromino.py @@ -31,7 +31,7 @@ class Tetromino: def __init__( self, group: pygame.sprite.Group, - create_new: Callable[[], None], + create_new: Callable[[Optional[Figure]], "Tetromino"], field: np.ndarray[Optional[Block], Any], shape: Optional[Figure] = None, ) -> None: @@ -42,32 +42,44 @@ class Tetromino: self.field = field self.blocks = self._initialize_blocks(group) - def move_down(self) -> None: + def move_down(self) -> bool: """ Moves the Tetromino down. If there is a collision, the Tetromino is placed on the field, and a new one is created. + + Returns: + True if the movement was successful, False otherwise. """ if not self._check_horizontal_collision(self.blocks, Direction.DOWN): for block in self.blocks: block.pos.y += 1 - else: - for block in self.blocks: - self.field[int(block.pos.y), int(block.pos.x)] = block - self.create_new() + return True - def move_horizontal(self, direction: Direction) -> None: + for block in self.blocks: + self.field[int(block.pos.y), int(block.pos.x)] = block + + self.create_new(None) + + return False + + def move_horizontal(self, direction: Direction) -> bool: """ Moves the Tetromino horizontally. Args: direction: Direction to move (LEFT or RIGHT). + + Returns: + True if the movement was successful, False otherwise. """ if not self._check_vertical_collision(self.blocks, direction): for block in self.blocks: block.pos.x += direction.value + return True + return False - def rotate(self, rotation: Rotation = Rotation.CLOCKWISE) -> None: + def rotate(self, rotation: Rotation = Rotation.CLOCKWISE) -> bool: """ Rotates the Tetromino. @@ -75,9 +87,12 @@ class Tetromino: Args: rotation: Rotation to perform (CLOCKWISE or COUNTER_CLOCKWISE). + + Returns: + True if the rotation was successful, False otherwise. """ if self.figure == Figure.O: - return + return False pivot: pygame.Vector2 = self.blocks[0].pos @@ -88,19 +103,48 @@ class Tetromino: if self._are_new_positions_valid(new_positions): self._update_block_positions(new_positions) - return + return True if any(pos.x < 0 for pos in new_positions): self.move_horizontal(Direction.RIGHT) else: self.move_horizontal(Direction.LEFT) - def drop(self) -> None: - """Drops the Tetromino to the bottom of the game field.""" + return False + + def next_rotation(self) -> "Tetromino": + self.rotate() + return self + + def drop(self) -> bool: + """ + Drops the Tetromino to the bottom of the game field. + + Returns: + True if the drop was successful, False otherwise. + """ + while not self._check_horizontal_collision(self.blocks, Direction.DOWN): for block in self.blocks: block.pos.y += 1 + return True + + def check_collision(self, direction: Direction) -> bool: + """ + Checks if there is a collision in the given direction. + + Args: + direction: Direction to check (UP, DOWN, LEFT, or RIGHT). + + Returns: + True if there is a collision, False otherwise. + """ + + return self._check_horizontal_collision( + self.blocks, direction + ) or self._check_vertical_collision(self.blocks, direction) + def _check_vertical_collision( self, blocks: list[Block], direction: Direction ) -> bool: diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 7257e72..c99d702 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -3,7 +3,8 @@ from .enum import Direction, GameMode, Rotation from .figure import Figure, FigureConfig from .log import log from .path import BASE_PATH -from .size import Size +from .tuples import BestMove, Size +from .weights import Weights __all__ = [ "BASE_PATH", @@ -15,4 +16,5 @@ __all__ = [ "Direction", "Rotation", "GameMode", + "Weights", ] diff --git a/src/utils/config.py b/src/utils/config.py index 72e9727..48e99f6 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -5,7 +5,7 @@ from pygame import Vector2 as Vec2 from .colors import TokyoNightNight from .path import BASE_PATH -from .size import Size +from .tuples import Size PADDING = 20 diff --git a/src/utils/figure.py b/src/utils/figure.py index 29e6d2c..34de17a 100644 --- a/src/utils/figure.py +++ b/src/utils/figure.py @@ -99,4 +99,4 @@ class Figure(Enum): @classmethod def random(cls) -> "Figure": - return random.choice(list(Figure)) + return random.choice(list(cls)) diff --git a/src/utils/size.py b/src/utils/tuples.py similarity index 77% rename from src/utils/size.py rename to src/utils/tuples.py index 9cdacf0..837d6a3 100644 --- a/src/utils/size.py +++ b/src/utils/tuples.py @@ -1,5 +1,7 @@ from typing import NamedTuple, Union +from .enum import Direction + class Size(NamedTuple): width: int | float @@ -9,3 +11,8 @@ class Size(NamedTuple): if isinstance(other, Size): return Size(self.width - other.width, self.height - other.height) return Size(self.width - other, self.height - other) + + +class BestMove(NamedTuple): + rotation: int + direction: Direction diff --git a/src/utils/weights.py b/src/utils/weights.py new file mode 100644 index 0000000..6328075 --- /dev/null +++ b/src/utils/weights.py @@ -0,0 +1,9 @@ +from attrs import define + + +@define +class Weights: + height: float + lines: float + holes: float + bumpiness: float diff --git a/tests/ai/test_fitness.py b/tests/ai/test_fitness.py index 91ad4eb..1f7ef40 100644 --- a/tests/ai/test_fitness.py +++ b/tests/ai/test_fitness.py @@ -2,7 +2,7 @@ import unittest import numpy as np from ai.fitness.bumpiness import get_bumpiness -from ai.fitness.holes import get_holes +from ai.fitness.holes import holes from ai.fitness.peaks import get_peaks_sum from ai.fitness.transitions import ( get_col_transition, @@ -58,7 +58,7 @@ class TestFitness(unittest.TestCase): np.array([0, 1, 0, 0, 0]), ) for field, answer in zip(self.fields, answers): - self.assertTrue(np.array_equal(get_holes(field), answer)) + self.assertTrue(np.array_equal(holes(field), answer)) def test_get_wells(self) -> None: answers = (