Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb022ca29 | ||
|
|
2d7fd87a86 | ||
|
|
7ee1c08789 | ||
|
|
86cb2e5841 | ||
|
|
0c8dff53e1 | ||
|
|
bc8721e077 | ||
|
|
f52f218a5f | ||
|
|
f8af193c66 | ||
|
|
f535f131b4 | ||
|
|
d9437bd28d | ||
|
|
742a6599d8 | ||
|
|
527c280261 | ||
|
|
092930b202 | ||
|
|
1af624ec11 | ||
|
|
4ea80c921b | ||
|
|
328f149bd8 |
@@ -6,7 +6,7 @@ The game was created using the pygame python library and builds on Github are pa
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
126
Tetris.py
126
Tetris.py
@@ -1,126 +0,0 @@
|
|||||||
import sys
|
|
||||||
import pygame
|
|
||||||
from pygame import mixer
|
|
||||||
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
from util.PieceGenerator import PieceGenerator
|
|
||||||
from entity.Well import Well
|
|
||||||
from entity.Stack import Stack
|
|
||||||
|
|
||||||
# TODO should be a singleton and refactor the whole file?
|
|
||||||
class Tetris:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.fps = -1
|
|
||||||
self.tile_size = -1
|
|
||||||
self.screen = None
|
|
||||||
self.clock = None
|
|
||||||
self.main_music = None
|
|
||||||
self.current_piece = None
|
|
||||||
self.well = None
|
|
||||||
self.stack = None
|
|
||||||
|
|
||||||
def initialize(self) -> None:
|
|
||||||
pygame.init()
|
|
||||||
mixer.init()
|
|
||||||
|
|
||||||
win_width = ConfigurationManager.configuration["window"]["width"]
|
|
||||||
win_height = ConfigurationManager.configuration["window"]["height"]
|
|
||||||
win_icon = ConfigurationManager.configuration["window"]["icon"]
|
|
||||||
win_title = ConfigurationManager.configuration["window"]["title"]
|
|
||||||
|
|
||||||
self.fps = ConfigurationManager.configuration["engine"]["fps"]
|
|
||||||
self.tile_size = ConfigurationManager.configuration["engine"]["tile-size"]
|
|
||||||
self.screen = pygame.display.set_mode((win_width, win_height))
|
|
||||||
self.clock = pygame.time.Clock()
|
|
||||||
self.main_music = mixer.Channel(0)
|
|
||||||
self.well = Well((280, 80), ConfigurationManager.configuration["color"]["well-1"], ConfigurationManager.configuration["color"]["well-border-1"]) # TODO calculate position later and redo color config for well
|
|
||||||
self.stack = Stack(ConfigurationManager.configuration["color"]["stack-1"], ConfigurationManager.configuration["color"]["stack-border-1"])
|
|
||||||
|
|
||||||
loaded_icon = pygame.image.load(win_icon)
|
|
||||||
pygame.display.set_caption(win_title)
|
|
||||||
pygame.display.set_icon(loaded_icon)
|
|
||||||
|
|
||||||
self.is_pressing_down = False # TODO move into control util later
|
|
||||||
|
|
||||||
main_music_file = ConfigurationManager.configuration["sound"]["main-music"]
|
|
||||||
self.main_music.set_volume(0.7) # TODO add volume to the config
|
|
||||||
self.main_music.play(mixer.Sound(main_music_file), -1)
|
|
||||||
|
|
||||||
# gets called from the games main loop
|
|
||||||
def update(self) -> None:
|
|
||||||
# TODO write not initialized exception
|
|
||||||
elapsed_time = self.clock.tick(self.fps)
|
|
||||||
|
|
||||||
if self.current_piece:
|
|
||||||
self.current_piece.update(elapsed_time, self)
|
|
||||||
else:
|
|
||||||
self.current_piece = PieceGenerator.get_piece((360, 100)) # TODO calculate spawn position
|
|
||||||
if self.stack and self.current_piece.collide(self.stack):
|
|
||||||
pygame.quit()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
if self.stack:
|
|
||||||
self.stack.update(elapsed_time)
|
|
||||||
|
|
||||||
# TODO create control utility class
|
|
||||||
for event in pygame.event.get():
|
|
||||||
if event.type == pygame.QUIT:
|
|
||||||
pygame.quit()
|
|
||||||
sys.exit()
|
|
||||||
if event.type == pygame.KEYDOWN:
|
|
||||||
if self.current_piece:
|
|
||||||
if event.key == pygame.K_SPACE:
|
|
||||||
self.current_piece.rotate()
|
|
||||||
if self.well and self.current_piece.collide(self.well):
|
|
||||||
self.current_piece.revert()
|
|
||||||
if self.stack and self.current_piece.collide(self.stack):
|
|
||||||
self.current_piece.revert()
|
|
||||||
if event.key == pygame.K_LEFT:
|
|
||||||
self.current_piece.move((-self.tile_size, 0))
|
|
||||||
if self.well and self.current_piece.collide(self.well):
|
|
||||||
self.current_piece.revert()
|
|
||||||
if self.stack and self.current_piece.collide(self.stack):
|
|
||||||
self.current_piece.revert()
|
|
||||||
if event.key == pygame.K_RIGHT:
|
|
||||||
self.current_piece.move((self.tile_size, 0))
|
|
||||||
if self.well and self.current_piece.collide(self.well):
|
|
||||||
self.current_piece.revert()
|
|
||||||
if self.stack and self.current_piece.collide(self.stack):
|
|
||||||
self.current_piece.revert()
|
|
||||||
if event.key == pygame.K_DOWN:
|
|
||||||
self.is_pressing_down = True
|
|
||||||
if self.current_piece:
|
|
||||||
self.current_piece.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] / 8
|
|
||||||
self.current_piece.set_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] / 8
|
|
||||||
if event.type == pygame.KEYUP:
|
|
||||||
if event.key == pygame.K_DOWN:
|
|
||||||
self.is_pressing_down = False
|
|
||||||
if self.current_piece:
|
|
||||||
self.current_piece.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"]
|
|
||||||
self.current_piece.set_time = ConfigurationManager.configuration["engine"]["piece-set-time"]
|
|
||||||
|
|
||||||
if self.is_pressing_down:
|
|
||||||
if self.current_piece:
|
|
||||||
self.current_piece.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"] / 8
|
|
||||||
self.current_piece.set_time = ConfigurationManager.configuration["engine"]["piece-set-time"] / 8
|
|
||||||
|
|
||||||
|
|
||||||
def draw(self) -> None:
|
|
||||||
# TODO write not initialized exception
|
|
||||||
|
|
||||||
# draw window bg
|
|
||||||
bg_color = pygame.Color(ConfigurationManager.configuration["color"]["window-bg"])
|
|
||||||
self.screen.fill(bg_color)
|
|
||||||
|
|
||||||
# draw all game objects
|
|
||||||
if self.well:
|
|
||||||
self.well.draw(self.screen)
|
|
||||||
if self.stack:
|
|
||||||
self.stack.draw(self.screen)
|
|
||||||
if self.current_piece:
|
|
||||||
self.current_piece.draw(self.screen)
|
|
||||||
|
|
||||||
# update display
|
|
||||||
pygame.display.update()
|
|
||||||
|
|
||||||
23
build.py
23
build.py
@@ -9,7 +9,7 @@ if sys.platform == 'win32':
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="Tetris",
|
name="Tetris",
|
||||||
version="0.4.0",
|
version="0.7.0",
|
||||||
description="Tetris Python Clone",
|
description="Tetris Python Clone",
|
||||||
options={
|
options={
|
||||||
"build_exe": {
|
"build_exe": {
|
||||||
@@ -20,20 +20,17 @@ setup(
|
|||||||
"yaml",
|
"yaml",
|
||||||
"random",
|
"random",
|
||||||
"sys",
|
"sys",
|
||||||
"entity.Entity",
|
"tetris.entity",
|
||||||
"entity.Piece",
|
"tetris.game",
|
||||||
"entity.Stack",
|
"tetris.util"
|
||||||
"entity.Well",
|
|
||||||
"util.ConfigurationManager",
|
|
||||||
"util.PieceGenerator",
|
|
||||||
"Tetris"
|
|
||||||
],
|
],
|
||||||
"include_files": [
|
"include_files": [
|
||||||
"config.yaml",
|
("config.yaml", "config.yaml"),
|
||||||
"tetris_icon.png",
|
("resource/image/tetris_icon.png", "resource/image/tetris_icon.png"),
|
||||||
"main_music.ogg",
|
("resource/image/press-start-2p-font.bmp", "resource/image/press-start-2p-font.bmp"),
|
||||||
"piece_set_3.wav",
|
("resource/sound/main_music.ogg", "resource/sound/main_music.ogg"),
|
||||||
"row_completion.wav"
|
("resource/sound/piece_set_3.wav", "resource/sound/piece_set_3.wav"),
|
||||||
|
("resource/sound/row_completion.wav", "resource/sound/row_completion.wav"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
24
config.yaml
24
config.yaml
@@ -1,19 +1,31 @@
|
|||||||
window:
|
window:
|
||||||
width: 800
|
width: 800
|
||||||
height: 600
|
height: 600
|
||||||
icon: "tetris_icon.png"
|
|
||||||
title: "Tetris"
|
title: "Tetris"
|
||||||
|
|
||||||
sound:
|
sound:
|
||||||
main-music: "main_music.ogg"
|
main-music: "resource/sound/main_music.ogg"
|
||||||
row-completion: "row_completion.wav"
|
row-completion: "resource/sound/row_completion.wav"
|
||||||
piece-set: "piece_set_3.wav"
|
piece-set: "resource/sound/piece_set_3.wav"
|
||||||
|
|
||||||
|
image:
|
||||||
|
window-icon: "resource/image/tetris_icon.png"
|
||||||
|
font: "resource/image/press-start-2p-font.bmp"
|
||||||
|
|
||||||
engine:
|
engine:
|
||||||
fps: 60
|
fps: 60
|
||||||
tile-size: 20
|
tile-size: 20
|
||||||
piece-gravity-time: 400
|
piece-gravity-time: 600
|
||||||
piece-set-time: 600
|
piece-set-time: 600
|
||||||
|
piece-gravity-increase: 56
|
||||||
|
lines-per-level: 5
|
||||||
|
points-per-lines-completed:
|
||||||
|
- 0 # 0 line
|
||||||
|
- 40 # 1 line
|
||||||
|
- 100 # 2 lines
|
||||||
|
- 300 # 3 lines
|
||||||
|
- 1200 # 4 lines
|
||||||
|
|
||||||
color:
|
color:
|
||||||
window-bg: "#000000"
|
window-bg: "#000000"
|
||||||
@@ -21,7 +33,7 @@ color:
|
|||||||
piece-2: "#3DBBFC"
|
piece-2: "#3DBBFC"
|
||||||
piece-3: "#FFFFFF"
|
piece-3: "#FFFFFF"
|
||||||
piece-inner-border-1: "#1F37EC"
|
piece-inner-border-1: "#1F37EC"
|
||||||
piece-border-1: "#000000"
|
piece-outer-border-1: "#000000"
|
||||||
well-1: "#9BFCF0"
|
well-1: "#9BFCF0"
|
||||||
well-border-1: "#000000"
|
well-border-1: "#000000"
|
||||||
stack-1: "#747474"
|
stack-1: "#747474"
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
from typing import Tuple
|
|
||||||
import pygame
|
|
||||||
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
|
|
||||||
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) -> None:
|
|
||||||
self.elapsed_time += elapsed_time
|
|
||||||
|
|
||||||
def draw(self, surface: pygame.Surface) -> None:
|
|
||||||
tile_size = ConfigurationManager.configuration["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) -> 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
|
|
||||||
138
entity/Piece.py
138
entity/Piece.py
@@ -1,138 +0,0 @@
|
|||||||
from typing import List, Tuple
|
|
||||||
from pygame import mixer
|
|
||||||
import pygame
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from entity.Entity import Entity
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
|
|
||||||
'''
|
|
||||||
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.gravity_time = ConfigurationManager.configuration["engine"]["piece-gravity-time"]
|
|
||||||
self.current_gravity_time = 0
|
|
||||||
self.set_time = ConfigurationManager.configuration["engine"]["piece-set-time"]
|
|
||||||
self.current_set_time = 0
|
|
||||||
self.piece_set_sound = mixer.Channel(2)
|
|
||||||
|
|
||||||
self.previous_points = None
|
|
||||||
self.previous_center = None
|
|
||||||
|
|
||||||
def update(self, elapsed_time: int, tetris) -> None:
|
|
||||||
super().update(elapsed_time)
|
|
||||||
|
|
||||||
well = tetris.well
|
|
||||||
stack = tetris.stack
|
|
||||||
|
|
||||||
tile_size = ConfigurationManager.configuration["engine"]["tile-size"]
|
|
||||||
if self.elapsed_time >= self.gravity_time and self.current_set_time == 0:
|
|
||||||
self.elapsed_time = 0
|
|
||||||
|
|
||||||
self.move((0, tile_size))
|
|
||||||
if well and self.collide(well) or stack and self.collide(stack):
|
|
||||||
self.revert()
|
|
||||||
self.current_set_time += elapsed_time
|
|
||||||
|
|
||||||
if self.current_set_time > 0:
|
|
||||||
self.current_set_time += elapsed_time
|
|
||||||
|
|
||||||
if self.current_set_time >= self.set_time:
|
|
||||||
self.__play_piece_set_sound()
|
|
||||||
self.current_set_time = 0
|
|
||||||
|
|
||||||
stack.add_piece(self)
|
|
||||||
tetris.current_piece = None
|
|
||||||
|
|
||||||
def draw(self, surface: pygame.Surface) -> None:
|
|
||||||
super().draw(surface)
|
|
||||||
|
|
||||||
tile_size = ConfigurationManager.configuration["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:
|
|
||||||
# reset elapsed time if user moves down to stall gravity
|
|
||||||
if vector[1] > 0:
|
|
||||||
self.elapsed_time = 0
|
|
||||||
|
|
||||||
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 sub_points in self.points:
|
|
||||||
for point in sub_points:
|
|
||||||
point[0] += vector[0]
|
|
||||||
point[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 __play_piece_set_sound(self) -> None:
|
|
||||||
piece_set_sound_file = ConfigurationManager.configuration["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.configuration["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.configuration["engine"]["tile-size"]
|
|
||||||
center = shape[-1]
|
|
||||||
|
|
||||||
# cast to int and avoid exception from pygame
|
|
||||||
return [int(center[0] * tile_size + position[0]), int(center[1] * tile_size + position[1])]
|
|
||||||
|
|
||||||
# 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))
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
from pygame import mixer
|
|
||||||
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
from entity.Piece import Piece
|
|
||||||
from entity.Well import Well
|
|
||||||
from entity.Entity import Entity
|
|
||||||
|
|
||||||
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) -> None:
|
|
||||||
super().update(elapsed_time)
|
|
||||||
self.rows_completed_count += self.__complete_rows()
|
|
||||||
|
|
||||||
def add_piece(self, piece: Piece) -> None:
|
|
||||||
self.points += piece.points
|
|
||||||
|
|
||||||
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.configuration["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.configuration["sound"]["row-completion"]
|
|
||||||
self.row_completion_sound.play(mixer.Sound(row_completion_sound_file))
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
from entity.Entity import Entity
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
|
|
||||||
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.configuration["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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
18
main.py
18
main.py
@@ -4,20 +4,18 @@
|
|||||||
https://tetris.com/play-tetris
|
https://tetris.com/play-tetris
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# TODO review imports to make sure it is being done correctly
|
from tetris.game import Game
|
||||||
|
from tetris.util import ConfigurationManager
|
||||||
from Tetris import Tetris
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
ConfigurationManager.load()
|
ConfigurationManager.load()
|
||||||
|
|
||||||
tetris = Tetris()
|
game = Game()
|
||||||
tetris.initialize()
|
game.initialize()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
tetris.update()
|
game.update()
|
||||||
tetris.draw()
|
game.draw()
|
||||||
|
|
||||||
# start main function
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
BIN
resource/image/press-start-2p-font.bmp
Normal file
BIN
resource/image/press-start-2p-font.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
406
tetris/entity.py
Normal file
406
tetris/entity.py
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import copy
|
||||||
|
import random
|
||||||
|
import pygame
|
||||||
|
from typing import List, Tuple
|
||||||
|
from pygame import mixer
|
||||||
|
from tetris.util import ConfigurationManager
|
||||||
|
from tetris.util import Controller
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
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.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.revert()
|
||||||
|
|
||||||
|
# handle soft drop movement and gravity based on level
|
||||||
|
gravity_time = ConfigurationManager.get("engine", "piece-gravity-time")
|
||||||
|
set_time = ConfigurationManager.get("engine", "piece-set-time")
|
||||||
|
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)
|
||||||
|
if not Controller.key_pressed(pygame.K_DOWN):
|
||||||
|
self.gravity_time = gravity_time - (game.get_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
|
||||||
|
|
||||||
|
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))
|
||||||
|
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 _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 _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 _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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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, color: str, border_color: str):
|
||||||
|
super().__init__([], color, border_color)
|
||||||
|
self.lines_completed_count = 0
|
||||||
|
self.line_completed_sound = mixer.Channel(1)
|
||||||
|
|
||||||
|
def update(self, elapsed_time: int, game: "Game") -> None:
|
||||||
|
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
|
||||||
|
|
||||||
|
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_line_completed_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_line_completed_sound(self) -> None:
|
||||||
|
line_completed_sound_file = ConfigurationManager.get("sound", "row-completion")
|
||||||
|
self.line_completed_sound.play(mixer.Sound(line_completed_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, outer_border_color = cls._get_piece_color()
|
||||||
|
return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_border_color, outer_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")
|
||||||
|
outer_border_color = ConfigurationManager.get("color", "piece-outer-border-1")
|
||||||
|
|
||||||
|
return (base_color, inner_border_color, outer_border_color)
|
||||||
112
tetris/game.py
Normal file
112
tetris/game.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import sys
|
||||||
|
import pygame
|
||||||
|
from pygame import mixer
|
||||||
|
from tetris.util import ConfigurationManager
|
||||||
|
from tetris.util import TextGenerator
|
||||||
|
from tetris.entity import PieceGenerator
|
||||||
|
from tetris.entity import Well
|
||||||
|
from tetris.entity import Stack
|
||||||
|
|
||||||
|
# TODO should be a singleton and refactor the whole file?
|
||||||
|
class Game:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.fps = -1
|
||||||
|
self.tile_size = -1
|
||||||
|
self.screen = None
|
||||||
|
self.clock = None
|
||||||
|
self.main_music = None
|
||||||
|
self.current_piece = None
|
||||||
|
self.next_piece = None
|
||||||
|
self.well = None
|
||||||
|
self.stack = None
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
pygame.init()
|
||||||
|
TextGenerator.load(ConfigurationManager.get("image", "font"), (20, 20))
|
||||||
|
|
||||||
|
win_width = ConfigurationManager.get("window", "width")
|
||||||
|
win_height = ConfigurationManager.get("window", "height")
|
||||||
|
win_title = ConfigurationManager.get("window", "title")
|
||||||
|
win_icon = ConfigurationManager.get("image", "window-icon")
|
||||||
|
|
||||||
|
self.fps = ConfigurationManager.get("engine", "fps")
|
||||||
|
self.tile_size = ConfigurationManager.get("engine", "tile-size")
|
||||||
|
self.screen = pygame.display.set_mode((win_width, win_height))
|
||||||
|
self.clock = pygame.time.Clock()
|
||||||
|
self.main_music = mixer.Channel(0)
|
||||||
|
self.well = Well((280, 80), ConfigurationManager.get("color", "well-1"), ConfigurationManager.get("color", "well-border-1")) # TODO calculate position later and redo color config for well
|
||||||
|
self.stack = Stack(ConfigurationManager.get("color", "stack-1"), ConfigurationManager.get("color", "stack-border-1"))
|
||||||
|
self.score = 0
|
||||||
|
|
||||||
|
loaded_icon = pygame.image.load(win_icon)
|
||||||
|
pygame.display.set_caption(win_title)
|
||||||
|
pygame.display.set_icon(loaded_icon)
|
||||||
|
|
||||||
|
main_music_file = ConfigurationManager.get("sound", "main-music")
|
||||||
|
self.main_music.set_volume(0.7) # TODO add volume to the config
|
||||||
|
self.main_music.play(mixer.Sound(main_music_file), -1)
|
||||||
|
|
||||||
|
# gets called from the games main loop
|
||||||
|
def update(self) -> None:
|
||||||
|
# TODO write not initialized exception
|
||||||
|
elapsed_time = self.clock.tick(self.fps)
|
||||||
|
|
||||||
|
if not self.next_piece:
|
||||||
|
self.next_piece = PieceGenerator.get_piece((620, 160))
|
||||||
|
|
||||||
|
if self.current_piece:
|
||||||
|
self.current_piece.update(elapsed_time, self)
|
||||||
|
else:
|
||||||
|
self.current_piece = self.next_piece
|
||||||
|
self.current_piece.move((360 - 620, 100 - 160)) # TODO calculate spawn position correctly
|
||||||
|
self.next_piece = PieceGenerator.get_piece((620, 160)) # (360, 100)
|
||||||
|
if self.stack and self.current_piece.collide(self.stack): # TODO game over redo
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if self.stack:
|
||||||
|
self.stack.update(elapsed_time, self)
|
||||||
|
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
def draw(self) -> None:
|
||||||
|
# TODO write not initialized exception
|
||||||
|
|
||||||
|
# draw window bg
|
||||||
|
bg_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
|
||||||
|
self.screen.fill(bg_color)
|
||||||
|
|
||||||
|
# draw all game objects
|
||||||
|
if self.next_piece:
|
||||||
|
self.next_piece.draw(self.screen)
|
||||||
|
if self.well:
|
||||||
|
self.well.draw(self.screen)
|
||||||
|
if self.stack:
|
||||||
|
self.stack.draw(self.screen)
|
||||||
|
if self.current_piece:
|
||||||
|
self.current_piece.draw(self.screen, self.well, self.stack)
|
||||||
|
|
||||||
|
score = str(self.score).zfill(6)
|
||||||
|
lines = str(self.stack.lines_completed_count).zfill(4)
|
||||||
|
level = str(self.get_level()).zfill(2)
|
||||||
|
|
||||||
|
TextGenerator.draw("Top", (80, 120), self.screen)
|
||||||
|
TextGenerator.draw("000000", (80, 140), self.screen)
|
||||||
|
TextGenerator.draw("Score", (80, 180), self.screen)
|
||||||
|
TextGenerator.draw(score, (80, 200), self.screen)
|
||||||
|
TextGenerator.draw("Lines " + lines, (300, 40), self.screen)
|
||||||
|
TextGenerator.draw("Next", (600, 120), self.screen)
|
||||||
|
TextGenerator.draw("LVL " + level, (600, 220), self.screen)
|
||||||
|
|
||||||
|
# update display
|
||||||
|
pygame.display.update()
|
||||||
|
|
||||||
|
def get_level(self) -> int:
|
||||||
|
lines_per_level = ConfigurationManager.get("engine", "lines-per-level")
|
||||||
|
return 0 if not self.stack else self.stack.lines_completed_count // lines_per_level
|
||||||
|
|
||||||
|
|
||||||
104
tetris/util.py
Normal file
104
tetris/util.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import yaml
|
||||||
|
import pygame
|
||||||
|
from typing import KeysView, Tuple
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO description
|
||||||
|
"""
|
||||||
|
class ConfigurationManager:
|
||||||
|
|
||||||
|
CONFIG_FILE_LOCATION = "config.yaml"
|
||||||
|
configuration = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls) -> None:
|
||||||
|
with open(cls.CONFIG_FILE_LOCATION, "r") as yaml_file:
|
||||||
|
cls.configuration = yaml.safe_load(yaml_file)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, key: str, sub_key: str = None):
|
||||||
|
if sub_key:
|
||||||
|
return cls.configuration[key][sub_key]
|
||||||
|
else:
|
||||||
|
return cls.configuration[key]
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO description
|
||||||
|
"""
|
||||||
|
class TextGenerator:
|
||||||
|
|
||||||
|
sheet = None
|
||||||
|
glyph_size = (-1, -1)
|
||||||
|
characters = { }
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, file: str, glyph_size: Tuple) -> None:
|
||||||
|
cls.sheet = pygame.image.load(file)
|
||||||
|
cls.glyph_size = glyph_size
|
||||||
|
|
||||||
|
# load character positions in bitmap into the characters dictionary
|
||||||
|
# letters
|
||||||
|
cls.characters["A"] = (9 * glyph_size[0], 2 * glyph_size[1])
|
||||||
|
cls.characters["B"] = (10 * glyph_size[0], 2 * glyph_size[1])
|
||||||
|
cls.characters["C"] = (11 * glyph_size[0], 2 * glyph_size[1])
|
||||||
|
cls.characters["D"] = (0 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["E"] = (1 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["F"] = (2 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["G"] = (3 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["H"] = (4 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["I"] = (5 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["J"] = (6 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["K"] = (7 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["L"] = (8 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["M"] = (9 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["N"] = (10 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["O"] = (11 * glyph_size[0], 3 * glyph_size[1])
|
||||||
|
cls.characters["P"] = (0 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["Q"] = (1 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["R"] = (2 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["S"] = (3 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["T"] = (4 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["U"] = (5 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["V"] = (6 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["W"] = (7 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["X"] = (8 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["Y"] = (9 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
cls.characters["Z"] = (10 * glyph_size[0], 4 * glyph_size[1])
|
||||||
|
|
||||||
|
# numbers
|
||||||
|
cls.characters["0"] = (4 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["1"] = (5 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["2"] = (6 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["3"] = (7 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["4"] = (8 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["5"] = (9 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["6"] = (10 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["7"] = (11 * glyph_size[0], 1 * glyph_size[1])
|
||||||
|
cls.characters["8"] = (0 * glyph_size[0], 2 * glyph_size[1])
|
||||||
|
cls.characters["9"] = (1 * glyph_size[0], 2 * glyph_size[1])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def draw(cls, text: str, position: Tuple, surface: pygame.Surface) -> None:
|
||||||
|
x_position = 0
|
||||||
|
for char_ in text:
|
||||||
|
if not char_.isspace():
|
||||||
|
surface.blit(cls.sheet, (position[0] + x_position, position[1]), pygame.Rect(cls.characters[char_.upper()], (cls.glyph_size[0], cls.glyph_size[1])))
|
||||||
|
x_position += cls.glyph_size[0]
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO description
|
||||||
|
"""
|
||||||
|
class Controller:
|
||||||
|
|
||||||
|
keys_pressed = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def key_down(cls, key: int) -> bool:
|
||||||
|
prior_pressed_state = False if key not in cls.keys_pressed else cls.keys_pressed[key]
|
||||||
|
cls.keys_pressed[key] = pygame.key.get_pressed()[key]
|
||||||
|
|
||||||
|
return cls.keys_pressed[key] and not prior_pressed_state
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def key_pressed(cls, key: int) -> bool:
|
||||||
|
return pygame.key.get_pressed()[key]
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import yaml
|
|
||||||
|
|
||||||
CONFIG_FILE_LOCATION = "config.yaml"
|
|
||||||
|
|
||||||
# TODO add getter for configuration?
|
|
||||||
class ConfigurationManager:
|
|
||||||
|
|
||||||
configuration = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load(cls) -> None:
|
|
||||||
with open(CONFIG_FILE_LOCATION, "r") as yaml_file:
|
|
||||||
cls.configuration = yaml.safe_load(yaml_file)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
from typing import Tuple
|
|
||||||
import random
|
|
||||||
|
|
||||||
from entity.Piece import Piece
|
|
||||||
from util.ConfigurationManager import ConfigurationManager
|
|
||||||
|
|
||||||
"""
|
|
||||||
TODO Add link that goes through random piece generation
|
|
||||||
"""
|
|
||||||
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.configuration["color"]["piece-" + str(random_number)]
|
|
||||||
inner_border_color = None if random_number != 3 else ConfigurationManager.configuration["color"]["piece-inner-border-1"]
|
|
||||||
border_color = ConfigurationManager.configuration["color"]["piece-border-1"]
|
|
||||||
|
|
||||||
return (base_color, inner_border_color, border_color)
|
|
||||||
Reference in New Issue
Block a user