feat: add single player scene

This commit is contained in:
Giovani
2021-07-08 01:20:33 -04:00
parent 2b6b755318
commit 68a24a5f4d
5 changed files with 231 additions and 253 deletions

View File

@@ -1,47 +1,43 @@
import copy
import random
import pygame
from typing import List, Tuple
from pygame import mixer
from typing import List, Tuple
from types import FunctionType
from tetri5.util import ConfigurationManager
from tetri5.util import Controller
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tetri5.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
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
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))
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(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 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_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]:
@@ -57,17 +53,17 @@ class Well(Entity):
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)
super().__init__(Well._get_points(position), color, border_color)
def _get_points(self, position: Tuple) -> List:
@classmethod
def _get_points(cls, 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 in [0, self.WIDTH + 1]:
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, self.HEIGHT + 1]:
elif j in [0, cls.HEIGHT + 1]:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
points = []
@@ -77,7 +73,6 @@ class Well(Entity):
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
'''
@@ -87,52 +82,49 @@ class Well(Entity):
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
super().__init__(Piece._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
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
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:
def update(self, elapsed_time: int, well: Well, stack: "Stack", level: int, clear_current_piece: FunctionType) -> None:
super().update(elapsed_time)
tile_size = ConfigurationManager.get("engine", "tile-size")
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_gravity:
self._applying_gravity = self._apply_gravity(elapsed_time, well, stack)
self._applying_set = not self._applying_gravity
"""
For more information on the piece set logic go here:
https://strategywiki.org/wiki/Tetris/Features#Lock_delay
"""
if self.applying_set:
self.applying_set = self._apply_set(elapsed_time, game)
self.applying_gravity = not self.applying_set
if self._applying_set:
self._applying_set = self._apply_set(elapsed_time, well, stack, clear_current_piece)
self._applying_gravity = not self._applying_set
# handle rotation, left and right movement
if Controller.key_down(pygame.K_SPACE):
self.rotate()
if game.well and self.collide(game.well) or game.stack and self.collide(game.stack):
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
if Controller.key_down(pygame.K_LEFT):
self.move((-tile_size, 0))
if game.well and self.collide(game.well) or game.stack and self.collide(game.stack):
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((tile_size, 0))
if game.well and self.collide(game.well) or game.stack and self.collide(game.stack):
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
@@ -141,41 +133,37 @@ class Piece(Entity):
gravity_increase = ConfigurationManager.get("engine", "piece-gravity-increase")
if Controller.key_pressed(pygame.K_DOWN):
self.gravity_time = max(10, (gravity_time - (game.get_level() * gravity_increase)) // 10)
self.set_time = max(10, set_time // 10)
self._gravity_time = max(10, (gravity_time - (level * gravity_increase)) // 10)
self._set_time = max(10, set_time // 10)
if not Controller.key_pressed(pygame.K_DOWN):
self.gravity_time = gravity_time - (game.get_level() * gravity_increase)
self.set_time = set_time
self._gravity_time = gravity_time - (level * gravity_increase)
self._set_time = set_time
def draw(self, surface: pygame.Surface, well: Well = None, stack: "Stack" = None) -> None:
tile_size = ConfigurationManager.get("engine", "tile-size")
# ghost piece
if well and stack:
for square in self._get_ghost_piece_points(well, stack):
pygame.draw.polygon(surface, pygame.Color("#FFFFFF"), square, max(tile_size // 6, 1)) # TODO add white to the yaml
pygame.draw.polygon(surface, pygame.Color("#FFFFFF"), 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] + (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))
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(tile_size // 6, 1))
pygame.draw.polygon(surface, pygame.Color(self._inner_border_color), new_square, max(self._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._previous_points = copy.deepcopy(self._points)
self._previous_center = copy.deepcopy(self._center)
self.center[0] += vector[0]
self.center[1] += vector[1]
self._center[0] += vector[0]
self._center[1] += vector[1]
for square in self.points:
for square in self._points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
@@ -185,28 +173,28 @@ class Piece(Entity):
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)
self._previous_points = copy.deepcopy(self._points)
self._previous_center = copy.deepcopy(self._center)
new_points = []
for square in self.points:
for square in self._points:
for vertex in square:
h = vertex[0] - self.center[0]
k = vertex[1] - self.center[1]
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]
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
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
if self._previous_points and self._previous_center:
self._points = self._previous_points
self._center = self._previous_center
@classmethod
def _get_points(self, shape: Tuple, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
tile_size = ConfigurationManager.get("engine", "tile-size")
points = []
for square in shape[:-1]:
sub_points = []
@@ -214,45 +202,36 @@ class Piece(Entity):
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])]
return [int(center[0] * self._tile_size + position[0]), int(center[1] * self._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
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))
self.move((0, self._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):
def _apply_set(self, elapsed_time: int, well: Well, stack: "Stack", clear_current_piece: FunctionType) -> 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(well) or self._entity_is_below(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
stack.add_piece(self)
clear_current_piece()
else:
return False
return True
def _mimic_move(self, vector: Tuple) -> List:
mimic_points = copy.deepcopy(self.points)
mimic_points = copy.deepcopy(self._points)
for square in mimic_points:
for vertex in square:
@@ -263,24 +242,19 @@ class Piece(Entity):
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))
self._piece_set_sound.play(mixer.Sound(piece_set_sound_file))
def _entity_is_below(self, entity: Entity) -> bool:
tile_size = ConfigurationManager.get("engine", "tile-size")
mimic_points = self._mimic_move((0, tile_size))
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:
tile_size = ConfigurationManager.get("engine", "tile-size")
prior_points = []
current_points = copy.deepcopy(self.points)
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 * tile_size
vertex[1] += 1 * self._tile_size
return prior_points
# shape attributes
@@ -299,26 +273,22 @@ class Stack(Entity):
def __init__(self, color: str, border_color: str):
super().__init__([], color, border_color)
self.lines_completed_count = 0
self.total_lines = 0
self.lines_completed_last = 0
self.line_completed_sound = mixer.Channel(1)
def update(self, elapsed_time: int, game: "Game") -> None:
def update(self, elapsed_time: int) -> None: # TODO remove scene argument
super().update(elapsed_time)
lines_completed = self._complete_rows()
current_level = game.get_level()
points_per_lines_completed = ConfigurationManager.get("engine", "points-per-lines-completed")
game.score += points_per_lines_completed[lines_completed] * (current_level + 1)
self.lines_completed_count += lines_completed
self.lines_completed_last = self._complete_rows()
self.total_lines += self.lines_completed_last
def add_piece(self, piece: Piece) -> None:
self.points += piece.points
self._points += piece._points
# TODO refactor into multiple functions
def _complete_rows(self) -> int:
squares_by_row = {}
for square in self.points:
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]] = []
@@ -327,28 +297,27 @@ class Stack(Entity):
squares_to_exclude = []
rows_completed = []
for key in squares_by_row:
for key, value in squares_by_row.items():
if len(squares_by_row[key]) == Well.WIDTH:
squares_to_exclude += squares_by_row[key]
squares_to_exclude += value
rows_completed.append(key)
if len(squares_to_exclude) == 0:
if not squares_to_exclude:
return 0
self._play_line_completed_sound()
tile_size = ConfigurationManager.get("engine", "tile-size")
new_points = []
for square in self.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
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.points = new_points
self._points = new_points
return len(rows_completed)
@@ -398,9 +367,7 @@ class PieceGenerator:
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")
outer_border_color = ConfigurationManager.get("color", "piece-outer-border-1")
return (base_color, inner_border_color, outer_border_color)