Files
tetri5/tetri5/entity.py
2021-07-15 10:37:09 -04:00

478 lines
20 KiB
Python

import copy
import random
import pygame
from typing import List, Tuple
from types import FunctionType
from tetri5.util import ConfigurationManager
from tetri5.util import Controller
from tetri5.util import SoundManager
"""
TODO description
"""
class Entity:
def __init__(self, points: Tuple, color: str, border_color: str = None):
self._tile_size = ConfigurationManager.get("engine", "tile-size")
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:
for square in self._points:
if self._color is not None:
square_color = pygame.Color(self._color)
square_color.a = 255
pygame.draw.polygon(surface, square_color, square, 0)
if self._border_color is not None:
pygame.draw.polygon(surface, pygame.Color(self._border_color), square, max(self._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__(Well._get_points(position), color, border_color)
@classmethod
def _get_points(cls, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
shape = []
for i in range(cls.WIDTH + 2):
for j in range(cls.HEIGHT + 2):
if i in [0, cls.WIDTH + 1]:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
elif j in [0, cls.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__(Piece._get_points(shape, position), color, border_color)
self._inner_border_color = inner_border_color
self._ghost_piece_color = ConfigurationManager.get("color", "piece-ghost")
self._center = self._get_center(shape, position)
self._previous_points = None
self._previous_center = None
# Drop delay
self._drop_delay = ConfigurationManager.get("engine", "piece-drop-delay") # in ms
self._drop_delay_acc = 0 # acumulated drop delay, in ms
self._applying_drop_delay = True
# Lock delay
self._lock_delay = ConfigurationManager.get("engine", "piece-lock-delay") # in ms
self._lock_delay_acc = 0 # acumulated lock delay, in ms
self._applying_lock_delay = False
def update(self, elapsed_time: int, well: Well, stack: "Stack", level: int, clear_current_piece: FunctionType) -> None:
super().update(elapsed_time)
# handle rotation, left and right movement
if Controller.key_down(pygame.K_UP):
if self.rotate_with_wall_kick(well, stack):
SoundManager.play_piece_rotate_sfx()
if Controller.key_down(pygame.K_LEFT):
self.move((-self._tile_size, 0))
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
if Controller.key_down(pygame.K_RIGHT):
self.move((self._tile_size, 0))
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
# handle soft drop movement and gravity based on level
start_drop_delay = ConfigurationManager.get("engine", "piece-drop-delay")
start_lock_delay = ConfigurationManager.get("engine", "piece-lock-delay")
drop_delay_decrease = ConfigurationManager.get("engine", "piece-drop-delay-decrease")
if Controller.key_pressed(pygame.K_DOWN):
self._drop_delay = max(10, (start_drop_delay - (level * drop_delay_decrease)) // 10)
self._set_time = max(10, start_lock_delay // 10)
if not Controller.key_pressed(pygame.K_DOWN):
self._drop_delay = start_drop_delay - (level * drop_delay_decrease)
self._set_time = start_lock_delay
# handle hard drop
if Controller.key_down(pygame.K_SPACE):
self._points = self._get_ghost_piece_points(well, stack)
self._add_to_stack(stack, clear_current_piece)
if self._applying_drop_delay:
self._applying_drop_delay = self._apply_drop(elapsed_time, well, stack)
self._applying_lock_delay = not self._applying_drop_delay
"""
For more information on the piece set logic go here:
https://strategywiki.org/wiki/Tetris/Features#Lock_delay
"""
if self._applying_lock_delay:
self._applying_lock_delay = self._apply_lock(elapsed_time, well, stack, clear_current_piece)
self._applying_drop_delay = not self._applying_lock_delay
def draw(self, surface: pygame.Surface, well: Well = None, stack: "Stack" = None) -> None:
# ghost piece
if well and stack:
for square in self._get_ghost_piece_points(well, stack):
pygame.draw.polygon(surface, pygame.Color(self._ghost_piece_color), square, max(self._tile_size // 6, 1)) # TODO add white to the yaml
super().draw(surface)
# inner border piece
for square in self._points:
if self._inner_border_color:
vertex_one = (square[0][0] + (self._tile_size // 10), square[0][1] + (self._tile_size // 10))
vertex_two = (square[1][0] - (self._tile_size // 10), square[1][1] + (self._tile_size // 10))
vertex_three = (square[2][0] - (self._tile_size // 10), square[2][1] - (self._tile_size // 10))
vertex_four = (square[3][0] + (self._tile_size // 10), square[3][1] - (self._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(self._tile_size // 6, 1))
# draw glimmer
surface.set_at((square[0][0]+3, square[0][1]+3), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+4, square[0][1]+4), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+4, square[0][1]+5), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+5, square[0][1]+4), pygame.Color(255, 255, 255))
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]
def rotate_with_wall_kick(self, well: Well, stack: "Stack") -> bool:
self._rotate()
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
# trying kick to the left
self.move((-self._tile_size, 0))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick to the left
return True
# trying kick to the right
self.move((self._tile_size, 0))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick to the right
return True
# trying kick from top
self.move((0, self._tile_size))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick from top
return True
# trying kick 2 tiles from top
self.move((0, self._tile_size * 2))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick 2 tiles from top
return True
else:
return True
# failed rotation
return False
def revert(self) -> None:
if self._previous_points and self._previous_center:
self._points = self._previous_points
self._center = self._previous_center
'''
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, no_revert: bool = False) -> None:
if not no_revert:
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
@classmethod
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:
center = shape[-1]
return [int(center[0] * self._tile_size + position[0]), int(center[1] * self._tile_size + position[1])]
def _apply_drop(self, elapsed_time: int, well: Well, stack: "Stack") -> bool:
self._drop_delay_acc += elapsed_time
if self._drop_delay_acc >= self._drop_delay:
self._drop_delay_acc = 0
if not self._entity_is_below(well) and not self._entity_is_below(stack):
self.move((0, self._tile_size))
else:
return False
return True
def _apply_lock(self, elapsed_time: int, well: Well, stack: "Stack", clear_current_piece: FunctionType) -> bool:
self._lock_delay_acc += elapsed_time
if self._lock_delay_acc >= self._lock_delay:
self._add_to_stack(stack, clear_current_piece)
return False
return self._entity_is_below(well) or self._entity_is_below(stack)
def _add_to_stack(self, stack: "Stack", clear_current_piece: FunctionType) -> None:
SoundManager.play_piece_set_sfx()
stack.add_piece(self)
clear_current_piece()
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 _entity_is_below(self, entity: Entity) -> bool:
mimic_points = self._mimic_move((0, self._tile_size))
return entity and entity._collide(mimic_points)
def _get_ghost_piece_points(self, well: Well, stack: "Stack") -> List:
current_points = copy.deepcopy(self._points)
while not well._collide(current_points) and not stack._collide(current_points):
prior_points = copy.deepcopy(current_points)
for square in current_points:
for vertex in square:
vertex[1] += 1 * self._tile_size
return prior_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):
super().__init__([], None, None)
self._square_designs = []
self.total_lines = 0
self.lines_completed_last = 0
def update(self, elapsed_time: int) -> None:
super().update(elapsed_time)
self.lines_completed_last = self._complete_rows()
self.total_lines += self.lines_completed_last
def draw(self, surface: pygame.Surface) -> None:
for i in range(len(self._points)):
square = self._points[i]
square_design = self._square_designs[i]
if square_design.base_color is not None:
pygame.draw.polygon(surface, pygame.Color(square_design.base_color), square, 0)
if square_design.outer_color is not None:
pygame.draw.polygon(surface, pygame.Color(square_design.outer_color), square, max(self._tile_size // 6, 1))
if square_design.inner_color is not None:
vertex_one = (square[0][0] + (self._tile_size // 10), square[0][1] + (self._tile_size // 10))
vertex_two = (square[1][0] - (self._tile_size // 10), square[1][1] + (self._tile_size // 10))
vertex_three = (square[2][0] - (self._tile_size // 10), square[2][1] - (self._tile_size // 10))
vertex_four = (square[3][0] + (self._tile_size // 10), square[3][1] - (self._tile_size // 10))
new_square = (vertex_one, vertex_two, vertex_three, vertex_four)
pygame.draw.polygon(surface, pygame.Color(square_design.inner_color), new_square, max(self._tile_size // 6, 1))
# draw square glimmer
surface.set_at((square[0][0]+3, square[0][1]+3), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+4, square[0][1]+4), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+4, square[0][1]+5), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+5, square[0][1]+4), pygame.Color(255, 255, 255))
def add_piece(self, piece: Piece) -> None:
self._points += piece._points
self._square_designs += [_SquareDesign(piece._color, piece._inner_border_color, piece._border_color) for _ in range(len(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, value in squares_by_row.items():
if len(squares_by_row[key]) == Well.WIDTH:
squares_to_exclude += value
rows_completed.append(key)
if not squares_to_exclude:
return 0
if len(rows_completed) == 4:
SoundManager.play_four_lines_complete_sfx()
else:
SoundManager.play_line_complete_sfx()
new_points = []
new_square_designs = []
for i in range(len(self._points)):
square = self._points[i]
if square not in squares_to_exclude:
for vertex in square:
distance_to_move = sum(
vertex[1] <= row_completed
for row_completed in rows_completed
)
vertex[1] += self._tile_size * distance_to_move
new_points.append(square)
new_square_designs.append(self._square_designs[i])
self._points = new_points
self._square_designs = new_square_designs
return len(rows_completed)
"""
TODO description
"""
class _SquareDesign:
def __init__(self, base_color: str, inner_color: str, outer_color: str):
self.base_color = base_color
self.inner_color = inner_color
self.outer_color = outer_color
"""
TODO description
"""
class PieceGenerator:
_bucket_one = []
_bucket_two = []
@classmethod
def get_piece(cls, position: Tuple, is_player_two: bool = False) -> Piece:
bucket = cls._bucket_one if not is_player_two else cls._bucket_two
if len(bucket) == 0:
cls._generate_bucket(bucket)
base_color, inner_border_color, outer_border_color = cls._get_piece_color(is_player_two)
return Piece(cls._get_piece_shape(bucket.pop()), position, base_color, inner_border_color, outer_border_color)
@classmethod
def _generate_bucket(cls, bucket: List) -> None:
piece_types = list(range(7))
while len(bucket) != 7:
random_number = random.randint(0, 6 - len(bucket))
bucket.append(piece_types.pop(random_number))
@classmethod
def _get_piece_shape(cls, 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
@classmethod
def _get_piece_color(cls, is_player_two: bool) -> Tuple:
random_number = random.randint(1, 3)
player_mod = "player-1" if not is_player_two else "player-2"
base_color = ConfigurationManager.get("color", "piece-" + str(random_number) + "-" + player_mod)
inner_border_color = None if random_number != 3 else ConfigurationManager.get("color", "piece-inner-border-1" + "-" + player_mod)
outer_border_color = ConfigurationManager.get("color", "piece-outer-border-1")
return (base_color, inner_border_color, outer_border_color)