Files
tetri5/tetris/entity.py

348 lines
13 KiB
Python

import copy
import random
import pygame
from typing import List, Tuple
from pygame import mixer
from tetris.util import ConfigurationManager
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tetris.game import Game
"""
TODO description
"""
class Entity:
def __init__(self, points: Tuple, color: str, border_color: str = None):
self.points = points
self.color = color
self.border_color = border_color
self.elapsed_time = 0
def update(self, elapsed_time: int) -> None:
self.elapsed_time += elapsed_time
def draw(self, surface: pygame.Surface) -> None:
tile_size = ConfigurationManager.get("engine", "tile-size")
for square in self.points:
pygame.draw.polygon(surface, pygame.Color(self.color), square, 0)
if self.border_color:
pygame.draw.polygon(surface, pygame.Color(self.border_color), square, max(tile_size // 6, 1))
def collide(self, entity: "Entity") -> bool: # TODO figure out how to do type hint for entity param of type Entity
for square_one in self.points:
for square_two in entity.points:
for i in range(4): # 4 vertices
if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]:
return True
return False
def _collide(self, points: List) -> bool:
for square_one in self.points:
for square_two in points:
for i in range(4): # 4 vertices
if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]:
return True
return False
"""
TODO description
"""
class Well(Entity):
WIDTH = 10 # standard tetris well width, should not be changed
HEIGHT = 20 # standard tetris well height, should not be changed
def __init__(self, position: Tuple, color: str, border_color: str):
super().__init__(self._get_points(position), color, border_color)
def _get_points(self, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
shape = []
for i in range(self.WIDTH + 2):
for j in range(self.HEIGHT + 2):
if i == 0 or i == self.WIDTH + 1:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
elif j == 0 or j == self.HEIGHT + 1:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
points = []
for square in shape:
sub_points = []
for vertex in square:
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
'''
For information on the Tetris piece Tetromino go here:
https://tetris.fandom.com/wiki/Tetromino
'''
class Piece(Entity):
def __init__(self, shape: Tuple, position: Tuple, color: str, inner_border_color: str, border_color: str):
super().__init__(self._get_points(shape, position), color, border_color)
self.inner_border_color = inner_border_color
self.center = self._get_center(shape, position)
self.piece_set_sound = mixer.Channel(2)
self.previous_points = None
self.previous_center = None
# Gravity
self.gravity_time = ConfigurationManager.get("engine", "piece-gravity-time")
self.current_gravity_time = 0
self.applying_gravity = True
# Set
self.set_time = ConfigurationManager.get("engine", "piece-set-time")
self.current_set_time = 0
self.applying_set = False
def update(self, elapsed_time: int, game: "Game") -> None:
super().update(elapsed_time)
if self.applying_gravity:
self.applying_gravity = self._apply_gravity(elapsed_time, game.well, game.stack)
self.applying_set = not self.applying_gravity
if self.applying_set:
self.applying_set = self._apply_set(elapsed_time, game)
self.applying_gravity = not self.applying_set
def draw(self, surface: pygame.Surface) -> None:
super().draw(surface)
tile_size = ConfigurationManager.get("engine", "tile-size")
for square in self.points:
if self.inner_border_color:
vertex_one = (square[0][0] + (tile_size // 10), square[0][1] + (tile_size // 10))
vertex_two = (square[1][0] - (tile_size // 10), square[1][1] + (tile_size // 10))
vertex_three = (square[2][0] - (tile_size // 10), square[2][1] - (tile_size // 10))
vertex_four = (square[3][0] + (tile_size // 10), square[3][1] - (tile_size // 10))
new_square = (vertex_one, vertex_two, vertex_three, vertex_four)
pygame.draw.polygon(surface, pygame.Color(self.inner_border_color), new_square, max(tile_size // 6, 1))
def move(self, vector: Tuple) -> None:
self.previous_points = copy.deepcopy(self.points)
self.previous_center = copy.deepcopy(self.center)
self.center[0] += vector[0]
self.center[1] += vector[1]
for square in self.points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
'''
For more information on a rotation of a piece go here:
https://gamedev.stackexchange.com/questions/17974/how-to-rotate-blocks-in-tetris
'''
def rotate(self) -> None:
self.previous_points = copy.deepcopy(self.points)
self.previous_center = copy.deepcopy(self.center)
new_points = []
for square in self.points:
for vertex in square:
h = vertex[0] - self.center[0]
k = vertex[1] - self.center[1]
vertex[0] = (k * -1) + self.center[0]
vertex[1] = h + self.center[1]
new_points.append([square[-1]] + square[0:-1])
self.points = new_points
def revert(self) -> None:
if self.previous_points and self.previous_center:
self.points = self.previous_points
self.center = self.previous_center
def _mimic_move(self, vector: Tuple) -> List:
mimic_points = copy.deepcopy(self.points)
for square in mimic_points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
return mimic_points
def _play_piece_set_sound(self) -> None:
piece_set_sound_file = ConfigurationManager.get("sound", "piece-set")
self.piece_set_sound.play(mixer.Sound(piece_set_sound_file))
def _get_points(self, shape: Tuple, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
points = []
for square in shape[:-1]:
sub_points = []
for vertex in square:
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
def _get_center(self, shape: Tuple, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
center = shape[-1]
# cast to int and avoid exception from pygame (center can be a floating point)
return [int(center[0] * tile_size + position[0]), int(center[1] * tile_size + position[1])]
def _apply_gravity(self, elapsed_time: int, well: Well, stack: "Stack") -> bool:
tile_size = ConfigurationManager.get("engine", "tile-size")
self.current_gravity_time += elapsed_time
if self.current_gravity_time >= self.gravity_time:
self.current_gravity_time = 0
if not self._entity_is_below(well) and not self._entity_is_below(stack):
self.move((0, tile_size))
else:
return False
return True
def _apply_set(self, elapsed_time: int, game: "Game") -> bool:
self.current_set_time += elapsed_time
if self.current_set_time >= self.set_time:
self.current_set_time = 0
if self._entity_is_below(game.well) or self._entity_is_below(game.stack):
self._play_piece_set_sound()
game.stack.add_piece(self) # TODO do on tetris object level?
game.current_piece = None # TODO turn into a method
else:
return False
return True
def _entity_is_below(self, entity: Entity) -> bool:
tile_size = ConfigurationManager.get("engine", "tile-size")
mimic_points = self._mimic_move((0, tile_size))
return entity and entity._collide(mimic_points)
# shape attributes
I_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((3, 0), (4, 0), (4, 1), (3, 1)), (2, 0))
J_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5))
L_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1.5, 0.5))
O_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1, 1))
S_SHAPE = (((0, 1), (1, 1), (1, 2), (0, 2)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5))
T_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5))
Z_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5))
"""
TODO description
"""
class Stack(Entity):
def __init__(self, color: str, border_color: str):
super().__init__([], color, border_color)
self.rows_completed_count = 0
self.row_completion_sound = mixer.Channel(1)
def update(self, elapsed_time: int) -> None:
super().update(elapsed_time)
self.rows_completed_count += self._complete_rows()
def add_piece(self, piece: Piece) -> None:
self.points += piece.points
# TODO refactor into multiple functions
def _complete_rows(self) -> int:
squares_by_row = {}
for square in self.points:
top_left_vertex = square[0]
if top_left_vertex[1] not in squares_by_row:
squares_by_row[top_left_vertex[1]] = []
if square not in squares_by_row[top_left_vertex[1]]:
squares_by_row[top_left_vertex[1]].append(square)
squares_to_exclude = []
rows_completed = []
for key in squares_by_row:
if len(squares_by_row[key]) == Well.WIDTH:
squares_to_exclude += squares_by_row[key]
rows_completed.append(key)
if len(squares_to_exclude) == 0:
return 0
self._play_row_completion_sound()
tile_size = ConfigurationManager.get("engine", "tile-size")
new_points = []
for square in self.points:
if square not in squares_to_exclude:
for vertex in square:
distance_to_move = 0
for row_completed in rows_completed:
if vertex[1] <= row_completed:
distance_to_move += 1
vertex[1] += tile_size * distance_to_move
new_points.append(square)
self.points = new_points
return len(rows_completed)
def _play_row_completion_sound(self) -> None:
row_completion_sound_file = ConfigurationManager.get("sound", "row-completion")
self.row_completion_sound.play(mixer.Sound(row_completion_sound_file))
"""
TODO description
"""
class PieceGenerator:
_bucket = []
@classmethod
def get_piece(cls, position: Tuple) -> Piece:
if len(cls._bucket) == 0:
cls._generate_bucket()
base_color, inner_border_color, border_color = cls._get_piece_color()
return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_border_color, border_color)
@classmethod
def _generate_bucket(cls) -> None:
piece_types = list(range(7))
while len(cls._bucket) != 7:
random_number = random.randint(0, 6 - len(cls._bucket))
cls._bucket.append(piece_types.pop(random_number))
def _get_piece_shape(piece_number: int) -> Tuple:
if piece_number == 0:
return Piece.I_SHAPE
if piece_number == 1:
return Piece.J_SHAPE
if piece_number == 2:
return Piece.L_SHAPE
if piece_number == 3:
return Piece.O_SHAPE
if piece_number == 4:
return Piece.S_SHAPE
if piece_number == 5:
return Piece.T_SHAPE
if piece_number == 6:
return Piece.Z_SHAPE
return None
def _get_piece_color() -> Tuple:
random_number = random.randint(1, 3)
base_color = ConfigurationManager.get("color", "piece-" + str(random_number))
inner_border_color = None if random_number != 3 else ConfigurationManager.get("color", "piece-inner-border-1")
border_color = ConfigurationManager.get("color", "piece-border-1")
return (base_color, inner_border_color, border_color)