492 lines
21 KiB
Python
492 lines
21 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, base_color: str, outer_color: str = None):
|
|
self._tile_size = ConfigurationManager.get("engine", "tile-size")
|
|
self._points = points
|
|
self._base_color = base_color
|
|
self._outer_color = outer_color
|
|
self._elapsed_time = 0 # TODO does nothing right now
|
|
|
|
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._base_color is not None:
|
|
square_color = pygame.Color(self._base_color)
|
|
pygame.draw.polygon(surface, square_color, square, 0)
|
|
if self._outer_color is not None:
|
|
pygame.draw.polygon(surface, pygame.Color(self._outer_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, base_color: str, outer_color: str):
|
|
super().__init__(Well._get_points(position), base_color, outer_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, base_color: str, inner_color: str, outer_color: str):
|
|
super().__init__(Piece._get_points(shape, position), base_color, outer_color)
|
|
self._inner_color = inner_color
|
|
self._ghost_piece_base_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, ghost_piece_off: bool = False) -> None:
|
|
# ghost piece
|
|
if well is not None and stack is not None and not ghost_piece_off:
|
|
ghost_piece_points = self._get_ghost_piece_points(well, stack)
|
|
if ghost_piece_points is not None:
|
|
for square in ghost_piece_points:
|
|
pygame.draw.polygon(surface, pygame.Color(self._ghost_piece_base_color), square, max(self._tile_size // 6, 1)) # TODO add white to the yaml
|
|
|
|
for square in self._points:
|
|
if self._base_color is not None:
|
|
square_color = pygame.Color(ConfigurationManager.get("color", self._base_color))
|
|
pygame.draw.polygon(surface, square_color, square, 0)
|
|
if self._outer_color is not None:
|
|
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", self._outer_color)), square, max(self._tile_size // 6, 1))
|
|
|
|
for square in self._points:
|
|
# inner color border piece
|
|
if self._inner_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(ConfigurationManager.get("color", self._inner_color)), new_square, max(self._tile_size // 6, 1))
|
|
|
|
# draw glimmer
|
|
surface.set_at((square[0][0]+3, square[0][1]+3), "white")
|
|
surface.set_at((square[0][0]+4, square[0][1]+4), "white")
|
|
surface.set_at((square[0][0]+5, square[0][1]+5), "white")
|
|
surface.set_at((square[0][0]+4, square[0][1]+5), "white")
|
|
surface.set_at((square[0][0]+5, square[0][1]+4), "white")
|
|
|
|
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)
|
|
prior_points = None
|
|
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_colors = []
|
|
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:
|
|
# only draw if squares and colors are aligned
|
|
if len(self._square_colors) != len(self._points):
|
|
return
|
|
|
|
for i in range(len(self._points)):
|
|
square = self._points[i]
|
|
square_design = self._square_colors[i]
|
|
if square_design.base_color is not None:
|
|
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", square_design.base_color)), square, 0)
|
|
if square_design.outer_color is not None:
|
|
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("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(ConfigurationManager.get("color", square_design.inner_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]+5, square[0][1]+5), 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_colors += [SquareColor(piece._base_color, piece._inner_color, piece._outer_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 = []
|
|
self.new_square_colors = []
|
|
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)
|
|
self.new_square_colors.append(self._square_colors[i])
|
|
self._points = new_points
|
|
self._square_colors = self.new_square_colors
|
|
|
|
return len(rows_completed)
|
|
|
|
"""
|
|
TODO description
|
|
"""
|
|
class SquareColor:
|
|
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 = []
|
|
|
|
@classmethod
|
|
def get_piece(cls, position: Tuple) -> Piece:
|
|
if len(cls._bucket) == 0:
|
|
cls._generate_bucket(cls._bucket)
|
|
|
|
base_color, inner_color, outer_color = cls._get_piece_color()
|
|
return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_color, outer_color)
|
|
|
|
@classmethod
|
|
def get_opponent_piece(cls, base_color: str, inner_color: str, outer_color: str) -> Piece:
|
|
return Piece(Piece.Z_SHAPE, (-250, -250), base_color, inner_color, outer_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) -> Tuple:
|
|
random_number = random.randint(1, 3)
|
|
|
|
base_color = "piece-" + str(random_number) + "-player-1"
|
|
inner_color = None if random_number != 3 else "piece-inner-border-1-player-1"
|
|
outer_color = "piece-outer-border-1"
|
|
|
|
return (base_color, inner_color, outer_color) |