docs(game): add docstrings

`Preview`

`Game`

`Score`

`Tetromino`

`Timer`

style(docs): adhere to Google docstring style
This commit is contained in:
Kristofers Solo 2024-01-04 06:57:53 +02:00
parent 918e832862
commit ac46665dca
9 changed files with 626 additions and 173 deletions

View File

@ -4,6 +4,20 @@ from utils import CONFIG, Size
class Block(pygame.sprite.Sprite):
"""
Initializes a Block object.
Args:
group: Sprite group to which the block belongs.
pos: Initial position of the block.
color: Color of the block.
Attributes:
image: Image representing the block.
pos: Position of the block.
rect: Rectangle representing the block.
"""
def __init__(
self,
/,
@ -13,20 +27,67 @@ class Block(pygame.sprite.Sprite):
color: str,
) -> None:
super().__init__(group)
self.image = pygame.Surface(CONFIG.game.cell)
self.image.fill(color)
self.pos = pygame.Vector2(pos) + CONFIG.game.offset
self.rect = self.image.get_rect(topleft=self.pos * CONFIG.game.cell.width)
self._initialize_image(color)
self._initialize_positions(pos)
def update(self) -> None:
"""Updates the block's position on the screen."""
self.rect.topleft = self.pos * CONFIG.game.cell.width
def vertical_collision(self, x: int, field: np.ndarray) -> bool:
"""
Checks for vertical collision with the game field.
Args:
x: The x-coordinate to check for collision.
field: 2D array representing the game field.
Returns:
True if there is a vertical collision, False otherwise.
"""
return not 0 <= x < CONFIG.game.columns or field[int(self.pos.y), x]
def horizontal_collision(self, y: int, field: np.ndarray) -> bool:
"""
Checks for horizontal collision with the game field.
Args:
y: The y-coordinate to check for collision.
field: 2D array representing the game field.
Returns:
True if there is a horizontal collision, False otherwise.
"""
return y >= CONFIG.game.rows or (y >= 0 and field[y, int(self.pos.x)])
def rotate(self, pivot: pygame.Vector2) -> pygame.Vector2:
"""
Rotates the block around a given pivot point.
Args:
pivot: The pivot point for rotation.
Returns:
The new position of the block after rotation.
"""
return pivot + (self.pos - pivot).rotate(90)
def _initialize_image(self, color: str) -> None:
"""
Initializes the image of the block with a specified color.
Args:
color: Color of the block.
"""
self.image = pygame.Surface(CONFIG.game.cell)
self.image.fill(color)
def _initialize_positions(self, pos: pygame.Vector2) -> None:
"""
Initializes the position of the block.
Args:
pos: Initial position of the block.
"""
self.pos = pygame.Vector2(pos) + CONFIG.game.offset
self.rect = self.image.get_rect(topleft=self.pos * CONFIG.game.cell.width)

View File

@ -11,55 +11,62 @@ from .timer import Timer, Timers
class Game:
"""
Game class for managing the game state.
Args:
get_next_figure: A function to get the next figure.
update_score: A function to update the score.
Attributes:
surface: Surface representing the game.
dispaly_surface: Surface representing the display.
rect: Rect representing the game surface.
sprites: Sprite group for managing blocks.
get_next_figure: A function to get the next figure.
update_score: A function to update the score.
grid_surface: Surface representing the grid.
field: 2D array representing the game field.
tetromino: The current tetromino.
timers: Game timers.
initial_block_speed: Initial block speed.
increased_block_speed: Increased block speed.
down_pressed: True if the down key is pressed, False otherwise.
level: Current game level.
score: Current game score.
lines: Number of lines cleared.
landing_sound: Sound effect for landing blocks.
"""
def __init__(
self,
get_next_figure: Callable[[], Figure],
update_score: Callable[[int, int, int], None],
) -> None:
self.surface = pygame.Surface(CONFIG.game.size)
self.dispaly_surface = pygame.display.get_surface()
self.rect = self.surface.get_rect(topleft=CONFIG.game.pos)
self._initialize_game_surface()
self._initialize_sprites()
self.sprites: pygame.sprite.Group[Block] = pygame.sprite.Group()
self.get_next_shape = get_next_figure
self.get_next_figure = get_next_figure
self.update_score = update_score
self._create_grid_surface()
self._initialize_grid_surface()
self._initialize_field_and_tetromino()
self.tetromino: Tetromino
self.field = self._generate_empty_field()
self.tetromino = Tetromino(
self.sprites,
self.create_new_tetromino,
self.field,
)
self.initial_block_speed = CONFIG.game.initial_speed
self.increased_block_speed = self.initial_block_speed * 0.3
self.down_pressed = False
self.timers = Timers(
Timer(self.initial_block_speed, True, self.move_down),
Timer(CONFIG.game.movment_delay),
Timer(CONFIG.game.rotation_delay),
)
self.timers.vertical.activate()
self.level = 1
self.score = 0
self.lines = 0
self.landing_sound = pygame.mixer.Sound(CONFIG.music.landing)
self.landing_sound.set_volume(CONFIG.music.volume)
self._initialize_game_state()
self._initialize_timers()
self.timers: Timers
def run(self) -> None:
self.dispaly_surface.blit(self.surface, CONFIG.game.pos)
"""Run a single iteration of the game loop."""
self._update_display_surface()
self.draw()
self._timer_update()
self.handle_event()
def draw(self) -> None:
self.surface.fill(CONFIG.colors.bg_float)
"""Draw the game surface and its components."""
self._fill_game_surface()
self.update()
self.sprites.draw(self.surface)
self._draw_border()
@ -69,51 +76,28 @@ class Game:
self.sprites.update()
def handle_event(self) -> None:
keys = pygame.key.get_pressed()
"""Handle player input events."""
keys: list[bool] = pygame.key.get_pressed()
left_keys = keys[pygame.K_LEFT] or keys[pygame.K_a] or keys[pygame.K_h]
right_keys = keys[pygame.K_RIGHT] or keys[pygame.K_d] or keys[pygame.K_l]
down_keys = keys[pygame.K_DOWN] or keys[pygame.K_s] or keys[pygame.K_j]
rotate_keys = (
keys[pygame.K_SPACE]
or keys[pygame.K_r]
or keys[pygame.K_UP]
or keys[pygame.K_w]
or keys[pygame.K_k]
)
if not self.timers.horizontal.active:
if left_keys:
self.move_left()
self.timers.horizontal.activate()
elif right_keys:
self.move_right()
self.timers.horizontal.activate()
if not self.timers.rotation.active:
if rotate_keys:
self.tetromino.rotate()
self.timers.rotation.activate()
if not self.down_pressed and down_keys:
self.down_pressed = True
self.timers.vertical.duration = self.increased_block_speed
if self.down_pressed and not down_keys:
self.down_pressed = False
self.timers.vertical.duration = self.initial_block_speed
self._handle_movement_keys(keys)
self._handle_rotation_keys(keys)
self._handle_down_key(keys)
def move_down(self) -> None:
"""Move the current tetromino down."""
self.tetromino.move_down()
def move_left(self) -> None:
"""Move the current tetromino to the left."""
self.tetromino.move_horizontal(Direction.LEFT)
def move_right(self) -> None:
"""Move the current tetromino to the right."""
self.tetromino.move_horizontal(Direction.RIGHT)
def create_new_tetromino(self) -> None:
self.landing_sound.play()
"""Create a new tetromino and perform necessary actions."""
self._play_landing_sound()
if self.game_over():
self.restart()
@ -122,58 +106,40 @@ class Game:
self.sprites,
self.create_new_tetromino,
self.field,
self.get_next_shape(),
self.get_next_figure(),
)
def game_over(self) -> bool:
"""
Check if the game is over.
Returns:
True if the game is over, False otherwise.
"""
for block in self.sprites:
if block.pos.y < 0:
log.info("Game over!")
return True
return False
def restart(self) -> None:
self.sprites.empty()
self.field = self._generate_empty_field()
self.tetromino = Tetromino(
self.sprites,
self.create_new_tetromino,
self.field,
self.get_next_shape(),
)
self.level = 1
self.score = 0
self.lines = 0
self.update_score(self.lines, self.score, self.level)
def _create_grid_surface(self) -> None:
self.grid_surface = self.surface.copy()
self.grid_surface.fill("#00ff00")
self.grid_surface.set_colorkey("#00ff00")
self.grid_surface.set_alpha(100)
"""Restart the game."""
self._reset_game_state()
self._initialize_field_and_tetromino()
def _draw_grid(self) -> None:
"""Draw the grid on the game surface."""
for col in range(1, CONFIG.game.columns):
x = col * CONFIG.game.cell.width
pygame.draw.line(
self.grid_surface,
CONFIG.colors.border_highlight,
(x, 0),
(x, self.grid_surface.get_height()),
CONFIG.game.line_width,
)
self._draw_vertical_grid_line(x)
for row in range(1, CONFIG.game.rows):
y = row * CONFIG.game.cell.width
pygame.draw.line(
self.grid_surface,
CONFIG.colors.border_highlight,
(0, y),
(self.grid_surface.get_width(), y),
CONFIG.game.line_width,
)
self._draw_horizontal_grid_line(y)
self.surface.blit(self.grid_surface, (0, 0))
def _draw_border(self) -> None:
"""Draw the border of the game surface."""
pygame.draw.rect(
self.dispaly_surface,
CONFIG.colors.border_highlight,
@ -183,10 +149,12 @@ class Game:
)
def _timer_update(self) -> None:
"""Update the timers."""
for timer in self.timers:
timer.update()
def _check_finished_rows(self) -> None:
"""Check and handle finished rows."""
delete_rows: list[int] = []
for idx, row in enumerate(self.field):
if all(row):
@ -195,41 +163,193 @@ class Game:
self._delete_rows(delete_rows)
def _delete_rows(self, delete_rows: list[int]) -> None:
"""Delete the specified rows."""
if not delete_rows:
return
self._calculate_score(len(delete_rows))
for row in delete_rows:
for block in self.field[row]:
block.kill()
self._remove_blocks_in_row(row)
self._move_rows_down(row)
self._rebuild_field()
def _remove_blocks_in_row(self, row: int) -> None:
"""Remove blocks in the specified row."""
for block in self.field[row]:
block.kill()
def _move_rows_down(self, deleted_row: int) -> None:
"""Move rows down after deleting a row."""
for row in self.field:
for block in row:
if block and block.pos.y < deleted_row:
block.pos.y += 1
def _rebuild_field(self) -> None:
"""Rebuild the game field after deleting rows."""
self.field = self._generate_empty_field()
for block in self.sprites:
self.field[int(block.pos.y), int(block.pos.x)] = block
def _generate_empty_field(self) -> np.ndarray:
"""Generate an empty game field."""
return np.full((CONFIG.game.rows, CONFIG.game.columns), None, dtype=Field)
def _calculate_score(self, rows_deleted: int) -> None:
"""Calculate and update the game score."""
self.lines += rows_deleted
self.score += CONFIG.game.score.get(rows_deleted, 0) * self.level
self._check_level_up()
self.update_score(self.lines, self.score, self.level)
# every 10 lines increase level
def _check_level_up(self) -> None:
"""Check if the player should level up."""
# incerement level every 10 lines
if self.lines // 10 + 1 > self.level:
self._level_up()
def _level_up(self) -> None:
"""Level up."""
self.level += 1
self.initial_block_speed *= 0.75
self.increased_block_speed *= 0.75
self.timers.vertical.duration = self.initial_block_speed
def _draw_components(self) -> None:
"""Draw additional components like borders and grid."""
self.sprites.draw(self.surface)
self._draw_border()
self._draw_grid()
def _initialize_game_surface(self) -> None:
"""Initialize the game surface."""
self.surface = pygame.Surface(CONFIG.game.size)
self.dispaly_surface = pygame.display.get_surface()
self.rect = self.surface.get_rect(topleft=CONFIG.game.pos)
def _initialize_sprites(self) -> None:
"""Initialize the game sprites."""
self.sprites: pygame.sprite.Group[Block] = pygame.sprite.Group()
def _initialize_grid_surface(self) -> None:
"""Initialize the grid surface."""
self.grid_surface = self.surface.copy()
self.grid_surface.fill("#00ff00")
self.grid_surface.set_colorkey("#00ff00")
self.grid_surface.set_alpha(100)
def _initialize_field_and_tetromino(self) -> None:
"""Initialize the game field and tetromino."""
self.field = self._generate_empty_field()
self.tetromino = Tetromino(
self.sprites,
self.create_new_tetromino,
self.field,
)
def _initialize_timers(self) -> None:
"""Initialize game timers."""
self.timers = Timers(
Timer(self.initial_block_speed, True, self.move_down),
Timer(CONFIG.game.movment_delay),
Timer(CONFIG.game.rotation_delay),
)
self.timers.vertical.activate()
def _initialize_game_state(self) -> None:
"""Initialize the game state."""
self.initial_block_speed = CONFIG.game.initial_speed
self.increased_block_speed = self.initial_block_speed * 0.4
self.down_pressed = False
self.level = 1
self.score = 0
self.lines = 0
self._initialize_sound()
def _initialize_sound(self) -> None:
"""Initialize game sounds."""
self.landing_sound = pygame.mixer.Sound(CONFIG.music.landing)
self.landing_sound.set_volume(CONFIG.music.volume * 2)
def _play_landing_sound(self) -> None:
"""Play the landing sound effect."""
self.landing_sound.play()
def _update_display_surface(self) -> None:
"""Update the display surface."""
self.dispaly_surface.blit(self.surface, CONFIG.game.pos)
def _fill_game_surface(self) -> None:
"""Fill the game surface with background color."""
self.surface.fill(CONFIG.colors.bg_float)
def _handle_movement_keys(self, keys: list[bool]) -> None:
"""Handle movement keys [K_LEFT, K_RIGHT, K_a, K_d, K_h, K_l]."""
left_keys = keys[pygame.K_LEFT] or keys[pygame.K_a] or keys[pygame.K_h]
right_keys = keys[pygame.K_RIGHT] or keys[pygame.K_d] or keys[pygame.K_l]
if not self.timers.horizontal.active:
if left_keys:
self.move_left()
self.timers.horizontal.activate()
elif right_keys:
self.move_right()
self.timers.horizontal.activate()
def _handle_rotation_keys(self, keys: list[bool]) -> None:
"""Handle rotation keys [K_SPACE, K_r, K_UP, K_w, K_k]."""
rotate_keys = (
keys[pygame.K_SPACE]
or keys[pygame.K_r]
or keys[pygame.K_UP]
or keys[pygame.K_w]
or keys[pygame.K_k]
)
if not self.timers.rotation.active:
if rotate_keys:
self.tetromino.rotate()
self.timers.rotation.activate()
def _handle_down_key(self, keys: list[bool]) -> None:
"""Handle the down key [K_DOWN, K_s, K_j]."""
down_keys = keys[pygame.K_DOWN] or keys[pygame.K_s] or keys[pygame.K_j]
if not self.down_pressed and down_keys:
self.down_pressed = True
self.timers.vertical.duration = self.increased_block_speed
if self.down_pressed and not down_keys:
self.down_pressed = False
self.timers.vertical.duration = self.initial_block_speed
def _reset_game_state(self) -> None:
"""Reset the game state."""
self.sprites.empty()
self.field = self._generate_empty_field()
self.level = 1
self.score = 0
self.lines = 0
self.update_score(self.lines, self.score, self.level)
def _draw_vertical_grid_line(self, x: int | float) -> None:
"""Draw a vertical grid line."""
pygame.draw.line(
self.grid_surface,
CONFIG.colors.border_highlight,
(x, 0),
(x, self.grid_surface.get_height()),
CONFIG.game.line_width,
)
def _draw_horizontal_grid_line(self, y: int | float) -> None:
"""Draw a horizontal grid line."""
pygame.draw.line(
self.grid_surface,
CONFIG.colors.border_highlight,
(0, y),
(self.grid_surface.get_width(), y),
CONFIG.game.line_width,
)

View File

@ -11,22 +11,37 @@ from .tetromino import Tetromino
class Main:
"""
Main class for the game.
Attributes:
display_surface: Pygame display surface.
clock: Pygame clock.
music: Pygame music.
game: Game object.
score: Score object.
preview: Preview object.
next_figures: List of upcoming figures.
music: Pygame music that plays in the background.
"""
def __init__(self) -> None:
log.info("Initializing the game")
self._initialize_pygeme()
self._initialize_game_components()
self._start_background_music()
def draw(self) -> None:
"""Update the display."""
pygame.display.update()
def run(self) -> None:
"""Run the main game loop."""
while True:
self._run_game_loop()
def _update_score(self, lines: int, score: int, level: int) -> None:
self.score.update(lines, score, level)
def handle_events(self) -> None:
"""Handle Pygame events."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.exit()
@ -35,18 +50,46 @@ class Main:
self.exit()
def exit(self) -> None:
"""Exit the game."""
pygame.quit()
sys.exit()
def _update_score(self, lines: int, score: int, level: int) -> None:
"""
Update the game score.
Args:
lines: Number of lines cleared.
score: Current score.
level: Current game level.
"""
self.score.update(lines, score, level)
def _generate_next_figures(self, amount: int = 3) -> list[Figure]:
"""
Generate the next set of random figures.
Args:
amount: Number of figures to generate (default is 3).
Returns:
List of randomly generated figures.
"""
return [Figure.random() for _ in range(amount)]
def _get_next_figure(self) -> Figure:
"""
Get the next figure in the sequence.
Returns:
The next figure in the sequence.
"""
next_shape = self.next_figures.pop(0)
self.next_figures.append(Figure.random())
return next_shape
def _initialize_pygeme(self) -> None:
"""Initialize Pygame and set up the display."""
pygame.init()
pygame.display.set_caption(CONFIG.window.title)
self.display_surface = pygame.display.set_mode(CONFIG.window.size)
@ -54,6 +97,7 @@ class Main:
self.clock = pygame.time.Clock()
def _initialize_game_components(self) -> None:
"""Initialize game-related components."""
self.next_figures = self._generate_next_figures()
self.game = Game(self._get_next_figure, self._update_score)
@ -61,11 +105,13 @@ class Main:
self.preview = Preview()
def _start_background_music(self) -> None:
"""Start playing background music."""
self.music = pygame.mixer.Sound(CONFIG.music.background)
self.music.set_volume(CONFIG.music.volume)
self.music.play(-1)
def _run_game_loop(self) -> None:
"""Run a single iteration of the game loop."""
self.draw()
self.handle_events()

View File

@ -3,28 +3,34 @@ from utils import CONFIG, Figure, Size
class Preview:
def __init__(self) -> None:
self.surface = pygame.Surface(CONFIG.sidebar.preview)
self.rect = self.surface.get_rect(
topright=(
CONFIG.window.size.width - CONFIG.window.padding,
CONFIG.window.padding,
)
)
self.dispaly_surface = pygame.display.get_surface()
"""
Class representing the preview of upcoming figures on the sidebar.
self.increment_height = self.surface.get_height() / 3
Attributes:
surface: Pygame surface representing the preview.
rect: Pygame rectangle representing the preview.
dispaly_surface: Pygame display surface.
increment_height: Height of each figure in the preview.
"""
def __init__(self) -> None:
self._initialize_surface()
self._initialize_rect()
self._initialize_increment_height()
def run(self, next_figures: list[Figure]) -> None:
self.dispaly_surface.blit(self.surface, self.rect)
self.draw(next_figures)
"""
Run the preview by updating the display and drawing next figures.
def draw(self, next_figures: list[Figure]) -> None:
self.surface.fill(CONFIG.colors.bg_sidebar)
self._draw_border()
self._draw_figures(next_figures)
Args:
next_figures (list[Figure]): List of upcoming figures.
"""
self.dispaly_surface.blit(self.surface, self.rect)
self._draw_preview(next_figures)
def _draw_border(self) -> None:
"""Draw the border around the preview surface."""
pygame.draw.rect(
self.dispaly_surface,
CONFIG.colors.border_highlight,
@ -34,9 +40,58 @@ class Preview:
)
def _draw_figures(self, figures: list[Figure]) -> None:
"""
Draw the upcoming figures on the preview surface.
Args:
figures (list[Figure]): List of upcoming figures.
"""
for idx, figure in enumerate(figures):
self._draw_figure(figure, idx)
def _draw_figure(self, figure: Figure, idx: int) -> None:
"""
Draw a single upcoming figure on the preview surface.
Args:
figure (Figure): The upcoming figure to draw.
idx (int): Index of the figure in the list.
"""
figure_surface = figure.value.image
x = self.surface.get_width() / 2
y = self.increment_height / 2 + idx * self.increment_height
rect = figure_surface.get_rect(center=(x, y))
self.surface.blit(figure_surface, rect)
def _draw_preview(self, next_figures: list[Figure]) -> None:
"""
Draw the preview with the background, border, and next figures.
Args:
next_figures (list[Figure]): List of upcoming figures.
"""
self._draw_background()
self._draw_border()
self._draw_figures(next_figures)
def _draw_background(self) -> None:
"""Draw the background of the preview."""
self.surface.fill(CONFIG.colors.bg_sidebar)
def _initialize_surface(self) -> None:
"""Initialize the preview surface."""
self.surface = pygame.Surface(CONFIG.sidebar.preview)
self.dispaly_surface = pygame.display.get_surface()
def _initialize_rect(self) -> None:
"""Initialize the preview rectangle."""
self.rect = self.surface.get_rect(
topright=(
CONFIG.window.size.width - CONFIG.window.padding,
CONFIG.window.padding,
)
)
def _initialize_increment_height(self) -> None:
"""Initialize the increment height for positioning text elements."""
self.increment_height = self.surface.get_height() / 3

View File

@ -3,42 +3,66 @@ from utils import CONFIG, Size
class Score:
"""
Class representing the score on the sidebar.
Attributes:
surface: Pygame surface representing the score.
dispaly_surface: Pygame display surface.
rect: Pygame rectangle representing the score.
font: Pygame font used to display the score.
text: Text to be displayed on the score.
increment_height: Height of each text element in the score.
"""
def __init__(self) -> None:
self.surface = pygame.Surface(CONFIG.sidebar.score)
self.dispaly_surface = pygame.display.get_surface()
self.rect = self.surface.get_rect(
bottomright=CONFIG.window.size.sub(CONFIG.window.padding)
)
self.font = pygame.font.Font(CONFIG.font.family, CONFIG.font.size)
self._initialize_surface()
self._initialize_rect()
self._initialize_font()
self.update(1, 0, 0)
self.increment_height = self.surface.get_height() / 3
self._initialize_increment_height()
def run(self) -> None:
"""Display the score on the game surface."""
self.dispaly_surface.blit(self.surface, self.rect)
self.draw()
def update(self, lines: int, score: int, level: int) -> None:
self.text = (
"""
Update the score information.
Args:
lines (int): Number of lines cleared.
score (int): Current game score.
level (int): Current game level.
"""
self.text: tuple[tuple[str, int], tuple[str, int], tuple[str, int]] = (
("Score", score),
("Level", level),
("Lines", lines),
)
def draw(self) -> None:
self.surface.fill(CONFIG.colors.bg_sidebar)
"""Draw the score on the score surface."""
self._draw_background()
self._draw_text()
self._draw_border()
def _draw_text(self) -> None:
"""Draw the text on the score surface."""
for idx, text in enumerate(self.text):
x = self.surface.get_width() / 2
y = self.increment_height / 2 + idx * self.increment_height
self._display_text(text, (x, y))
def _display_text(self, text: tuple[str, int], pos: tuple[int, int]) -> None:
"""
Display a single text element on the score surface.
Args:
text (tuple[str, int]): A tuple containing the label and value of the text element.
pos (tuple[int, int]): The position (x, y) where the text should be displayed.
"""
text_surface = self.font.render(
f"{text[0]}: {text[1]}", True, CONFIG.colors.fg_sidebar
)
@ -46,6 +70,7 @@ class Score:
self.surface.blit(text_surface, text_rect)
def _draw_border(self) -> None:
"""Draw the border of the score surface."""
pygame.draw.rect(
self.dispaly_surface,
CONFIG.colors.border_highlight,
@ -53,3 +78,26 @@ class Score:
CONFIG.game.line_width * 2,
CONFIG.game.border_radius,
)
def _draw_background(self) -> None:
"""Fill the background of the score display."""
self.surface.fill(CONFIG.colors.bg_sidebar)
def _initialize_surface(self) -> None:
"""Initialize the score surface."""
self.surface = pygame.Surface(CONFIG.sidebar.score)
self.dispaly_surface = pygame.display.get_surface()
def _initialize_rect(self) -> None:
"""Initialize the score rectangle."""
self.rect = self.surface.get_rect(
bottomright=CONFIG.window.size.sub(CONFIG.window.padding)
)
def _initialize_font(self) -> None:
"""Initialize the font used to display the score."""
self.font = pygame.font.Font(CONFIG.font.family, CONFIG.font.size)
def _initialize_increment_height(self) -> None:
"""Initialize the increment height for positioning text elements."""
self.increment_height = self.surface.get_height() / 3

View File

@ -9,25 +9,44 @@ from .log import log
class Tetromino:
"""
Class representing a Tetromino.
Args:
group: Sprite group for managing blocks.
create_new: Callback function to create a new Tetromino.
field: 2D array representing the game field.
shape: Initial shape of the Tetromino (default is None).
Attributes:
figure: Tetromino figure.
block_positions: List of block positions.
color: Color of the Tetromino.
create_new: Callback function to create a new Tetromino.
field: 2D array representing the game field.
blocks: List of Tetromino blocks.
"""
def __init__(
self,
group: pygame.sprite.Group,
func: Callable[[], None],
create_new: Callable[[], None],
field: np.ndarray,
shape: Optional[Figure] = None,
) -> None:
self.figure: Figure = shape if shape else Figure.random()
self.figure: Figure = self._generate_figure(shape)
self.block_positions: list[pygame.Vector2] = self.figure.value.shape
self.color: str = self.figure.value.color
self.create_new = func
self.create_new = create_new
self.field = field
self.blocks = [
Block(group=group, pos=pos, color=self.color)
for pos in self.block_positions
]
self.blocks = self._initialize_blocks(group)
def move_down(self) -> None:
"""
Moves the Tetromino down.
If there is a collision, the Tetromino is placed on the field, and a new one is created.
"""
if not self._check_horizontal_collision(self.blocks, Direction.DOWN):
for block in self.blocks:
block.pos.y += 1
@ -37,11 +56,22 @@ class Tetromino:
self.create_new()
def move_horizontal(self, direction: Direction) -> None:
"""
Moves the Tetromino horizontally.
Args:
direction: Direction to move (LEFT or RIGHT).
"""
if not self._check_vertical_collision(self.blocks, direction):
for block in self.blocks:
block.pos.x += direction.value
def rotate(self) -> None:
"""
Rotates the Tetromino clockwise.
Does not rotate if the Tetromino is an O-shaped (square) figure.
"""
if self.figure == Figure.O:
return
@ -51,14 +81,22 @@ class Tetromino:
block.rotate(pivot) for block in self.blocks
]
if not self._are_new_positions_valid(new_positions):
return
if self._are_new_positions_valid(new_positions):
self._update_block_positions(new_positions)
def _check_vertical_collision(
self, blocks: list[Block], direction: Direction
) -> bool:
"""
Checks for vertical collision.
Args:
blocks: List of blocks to check for collision.
direction: Direction of movement.
Returns:
True if there is a vertical collision, False otherwise.
"""
return any(
block.vertical_collision(int(block.pos.x + direction.value), self.field)
for block in self.blocks
@ -67,21 +105,71 @@ class Tetromino:
def _check_horizontal_collision(
self, blocks: list[Block], direction: Direction
) -> bool:
"""
Checks for horizontal collision.
Args:
blocks: List of blocks to check for collision.
direction: Direction of movement.
Returns:
True if there is a horizontal collision, False otherwise.
"""
return any(
block.horizontal_collision(int(block.pos.y + direction.value), self.field)
for block in self.blocks
)
def _are_new_positions_valid(self, new_positions: list[pygame.Vector2]) -> bool:
for pos in new_positions:
if not (
0 <= pos.x < CONFIG.game.columns and 0 <= pos.y <= CONFIG.game.rows
):
return False
if self.field[int(pos.y), int(pos.x)]:
return False
return True
def _update_block_positions(self, new_positions: list[pygame.Vector2]) -> None:
"""
Updates the positions of Tetromino blocks.
Args:
new_positions: New positions for the blocks.
"""
for block, new_pos in zip(self.blocks, new_positions):
block.pos = new_pos
def _are_new_positions_valid(self, new_positions: list[pygame.Vector2]) -> bool:
"""
Checks if the new positions are valid within the game field.
Args:
new_positions: New positions to check.
Returns:
True if all positions are valid, False otherwise.
"""
return all(
0 <= pos.x < CONFIG.game.columns
and 0 <= pos.y <= CONFIG.game.rows
and not self.field[int(pos.y), int(pos.x)]
for pos in new_positions
)
def _initialize_blocks(self, group: pygame.sprite.Group) -> list[Block]:
"""
Initializes Tetromino blocks.
Args:
group: Sprite group for managing blocks.
Returns:
List of initialized blocks.
"""
return [
Block(group=group, pos=pos, color=self.color)
for pos in self.block_positions
]
def _generate_figure(self, shape: Optional[Figure]) -> Figure:
"""
Generates a Tetromino figure.
Args:
shape: Initial shape of the Tetromino (default is None).
Returns:
Generated Tetromino figure.
"""
return shape if shape else Figure.random()

View File

@ -6,6 +6,22 @@ from attrs import define, field
@define
class Timer:
"""
Timer class for managing timed events.
Args:
duration: Duration of the timer in milliseconds.
repeated: Whether the timer should repeat after each completion.
func: Callback function to execute when the timer completes.
Attributes:
duration: Duration of the timer in milliseconds.
repeated: Whether the timer should repeat after each completion.
func: Callback function to execute when the timer completes.
start_time: Time when the timer was last activated.
active: Indicates whether the timer is currently active.
"""
duration: float = field(converter=float)
repeated: bool = field(default=False)
func: Optional[Callable[[], None]] = field(default=None)
@ -13,14 +29,21 @@ class Timer:
active: bool = False
def activate(self) -> None:
"""Activates the timer, setting the start time to the current time."""
self.active = True
self.start_time = pygame.time.get_ticks()
def deactivate(self) -> None:
"""Deactivates the timer, resetting the start time to 0."""
self.active = False
self.start_time = 0
def update(self) -> None:
"""
Updates the timer, checking if it has completed its duration.
If completed, executes the callback function (if provided) and either deactivates or reactivates the timer.
"""
current_time = pygame.time.get_ticks()
if current_time - self.start_time >= self.duration and self.active:
if self.func and self.start_time:
@ -33,6 +56,15 @@ class Timer:
class Timers(NamedTuple):
"""
NamedTuple for grouping different timers.
Args and Attributes:
vertical: Timer for vertical movement.
horizontal: Timer for horizontal movement.
rotation: Timer for rotation.
"""
vertical: Timer
horizontal: Timer
rotation: Timer

View File

@ -21,7 +21,7 @@ class Game:
size: Size = Size(columns * cell.width, rows * cell.width)
pos: Vec2 = Vec2(padding, padding)
offset: Vec2 = Vec2(columns // 2, -1)
initial_speed: int = 400
initial_speed: float | int = 400
movment_delay: int = 200
rotation_delay: int = 200
score: dict[int, int] = {1: 40, 2: 100, 3: 300, 4: 1200}

View File

@ -18,7 +18,10 @@ class FigureConfig(NamedTuple):
def _load_image(filename: str) -> pygame.Surface:
return pygame.image.load(BASE_PATH / "assets" / "figures" / filename)
return pygame.image.load(
BASE_PATH / "assets" / "figures" / filename
) # TODO: add `.convert_alpha()``
# TODO: change colors of images
class Figure(Enum):