mirror of
https://github.com/kristoferssolo/Tetris.git
synced 2025-10-21 20:00:35 +00:00
docs(game): add docstrings
`Preview` `Game` `Score` `Tetromino` `Timer` style(docs): adhere to Google docstring style
This commit is contained in:
parent
918e832862
commit
ac46665dca
@ -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)
|
||||
|
||||
336
src/game/game.py
336
src/game/game.py
@ -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,
|
||||
)
|
||||
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_vertical_grid_line(x)
|
||||
for row in range(1, CONFIG.game.rows):
|
||||
y = row * CONFIG.game.cell.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 += 1
|
||||
self.initial_block_speed *= 0.75
|
||||
self.increased_block_speed *= 0.75
|
||||
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,
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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):
|
||||
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)
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
self._update_block_positions(new_positions)
|
||||
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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user