27 Commits

Author SHA1 Message Date
Giovani Rodriguez
0fb022ca29 chore: update gif in readme 2021-06-14 15:03:36 -04:00
Giovani Rodriguez
2d7fd87a86 chore: fix build script 2021-06-14 14:48:01 -04:00
Giovani Rodriguez
7ee1c08789 feat: add next piece logic 2021-06-14 14:38:25 -04:00
Giovani Rodriguez
86cb2e5841 fix: adjust points per line to fix calc 2021-06-14 14:17:05 -04:00
Giovani Rodriguez
0c8dff53e1 feat: add score, lines and level calc 2021-06-14 14:00:33 -04:00
Giovani Rodriguez
bc8721e077 refactor: improve keyboard input logic 2021-06-14 13:17:18 -04:00
Giovani Rodriguez
f52f218a5f refactor: improve ghost piece logic 2021-06-14 10:14:04 -04:00
Giovani Rodriguez
f8af193c66 feat: add ghost piece 2021-06-14 00:33:13 -04:00
Giovani Rodriguez
f535f131b4 feat: add labels 2021-06-13 23:41:48 -04:00
Giovani Rodriguez
d9437bd28d chore: change bmp font and test text gen 2021-06-13 12:17:02 -04:00
Giovani Rodriguez
742a6599d8 feat: create sheet splice part of text generator 2021-06-11 23:48:55 -04:00
Giovani Rodriguez
527c280261 chore: add new modules to build script 2021-06-11 22:54:18 -04:00
Giovani Rodriguez
092930b202 refactor!: restructure project files and modules 2021-06-11 22:48:10 -04:00
Giovani Rodriguez
1af624ec11 chore: move resources into folders 2021-06-11 15:31:37 -04:00
Giovani Rodriguez
4ea80c921b refactor: improve piece class readability 2021-06-11 15:15:15 -04:00
Giovani Rodriguez
328f149bd8 chore: update gif in readme 2021-06-10 18:48:58 -04:00
Giovani Rodriguez
8b72e841e9 chore: fix build script to include music files 2021-06-10 18:32:23 -04:00
Giovani Rodriguez
8952753fc3 feat: add soft drop and exit on full well 2021-06-10 18:25:54 -04:00
Giovani Rodriguez
15851cb269 fix: move piece to correct spawn position 2021-06-10 17:49:39 -04:00
Giovani Rodriguez
5c9b172882 feat: add set piece logic 2021-06-10 17:45:12 -04:00
Giovani Rodriguez
fc3d2fc04c feat: revamp colors 2021-06-10 17:19:27 -04:00
Giovani Rodriguez
231c19c7fd fix: address issue with nonplaying sound and delay 2021-06-10 20:09:05 +00:00
Giovani Rodriguez
5cf73ceb7d feat: add the rest of the sounds with channels 2021-06-10 15:53:14 -04:00
Giovani Rodriguez
479553424f feat: add main music to game 2021-06-10 15:14:01 -04:00
Giovani Rodriguez
f003cb2d2e feat: implement line completion logic 2021-06-10 14:01:26 -04:00
Giovani Rodriguez
8c8a1e6de0 chore: add demo gif to readme 2021-06-09 15:18:07 -04:00
Giovani Rodriguez
27e7ad8dc3 chore: add project readme 2021-06-09 14:45:07 -04:00
19 changed files with 708 additions and 383 deletions

37
README.md Normal file
View File

@@ -0,0 +1,37 @@
# Python Tetris Clone
The python-tetris-clone project was created for the purpose of becoming familiar with Python 3. The goal is to replicate the most current version of Tetris (https://tetris.com/play-tetris) down to every mechanic. This includes the way pieces rotate and even newer features such as the ghost piece.
The game was created using the pygame python library and builds on Github are packaged into one executable using cx_Freeze.
## Demo
![gif](https://i.imgur.com/pWAs0qX.gif)
## Requirements
Make sure you have installed Python 3. If you have not, download and install Python 3 at [python.org](https://www.python.org/downloads/).
Then use the package manager [pip](https://pip.pypa.io/en/stable/) to install the required packages.
```bash
pip install pygame
pip install pyyaml
```
## Run the Game
Clone the Github repository. From your terminal move to the root of the project and run the following line:
```bash
python main.py
```
In some systems you may need to run this instead:
```bash
python3 main.py
```
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

112
Tetris.py
View File

@@ -1,112 +0,0 @@
import sys
import pygame
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?
class Tetris:
def __init__(self):
self.fps = -1
self.tile_size = -1
self.screen = None
self.clock = None
self.current_piece = None
self.well = None
self.stack = None
def initialize(self) -> None:
pygame.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.well = Well((280, 80), ConfigurationManager.configuration["color"]["border"]) # TODO calculate position later and redo color config for well
self.stack = Stack(ConfigurationManager.configuration["color"]["base-4"], ConfigurationManager.configuration["color"]["border"])
loaded_icon = pygame.image.load(win_icon)
pygame.display.set_caption(win_title)
pygame.display.set_icon(loaded_icon)
# 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.well, self.stack)
# 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_UP:
self.current_piece.move((0, -self.tile_size))
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.current_piece.move((0, self.tile_size))
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_z:
if self.current_piece:
self.stack.add_piece(self.current_piece)
self.__generate_piece((300, 100))
def draw(self) -> None:
# TODO write not initialized exception
# draw window bg
bg_color = pygame.Color(ConfigurationManager.configuration["window"]["bg-color"])
self.screen.fill(bg_color)
# draw all game objects
if self.current_piece:
self.current_piece.draw(self.screen)
if self.well:
self.well.draw(self.screen)
if self.stack:
self.stack.draw(self.screen)
# update display
pygame.display.update()
# TODO one line method is questionable
def __generate_piece(self, position):
self.current_piece = PieceGenerator.get_piece(position)

View File

@@ -9,7 +9,7 @@ if sys.platform == 'win32':
setup(
name="Tetris",
version="0.1.0",
version="0.7.0",
description="Tetris Python Clone",
options={
"build_exe": {
@@ -20,17 +20,17 @@ setup(
"yaml",
"random",
"sys",
"entity.Entity",
"entity.Piece",
"entity.Stack",
"entity.Well",
"util.ConfigurationManager",
"util.PieceGenerator",
"Tetris"
"tetris.entity",
"tetris.game",
"tetris.util"
],
"include_files": [
"config.yaml",
"tetris_icon.png"
("config.yaml", "config.yaml"),
("resource/image/tetris_icon.png", "resource/image/tetris_icon.png"),
("resource/image/press-start-2p-font.bmp", "resource/image/press-start-2p-font.bmp"),
("resource/sound/main_music.ogg", "resource/sound/main_music.ogg"),
("resource/sound/piece_set_3.wav", "resource/sound/piece_set_3.wav"),
("resource/sound/row_completion.wav", "resource/sound/row_completion.wav"),
]
}
},

View File

@@ -1,18 +1,40 @@
window:
width: 800
height: 600
icon: "tetris_icon.png"
title: "Tetris"
bg-color: "#6495ED" # cornflower-blue
sound:
main-music: "resource/sound/main_music.ogg"
row-completion: "resource/sound/row_completion.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:
fps: 60
tile-size: 20
piece-gravity-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
# https://coolors.co/6495ed-ee6352-59cd90-fac05e-dfd9e2
color:
base-1: "#EE6352"
base-2: "#59CD90"
base-3: "#FAC05E"
base-4: "#4C5B5C"
border: "#DFD9E2"
window-bg: "#000000"
piece-1: "#1F37EC"
piece-2: "#3DBBFC"
piece-3: "#FFFFFF"
piece-inner-border-1: "#1F37EC"
piece-outer-border-1: "#000000"
well-1: "#9BFCF0"
well-border-1: "#000000"
stack-1: "#747474"
stack-border-1: "#000000"

View File

@@ -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

View File

@@ -1,102 +0,0 @@
from typing import List, Tuple
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):
GRAVITY = 800 # A move down every 800 ms
def __init__(self, shape: Tuple, position: Tuple, color: str, border_color: str):
super().__init__(self.__get_points(shape, position), color, border_color)
self.center = self.__get_center(shape, position)
self.previous_points = None
self.previous_center = None
def update(self, elapsed_time: int, well: Entity, stack: Entity):
super().update(elapsed_time)
tile_size = ConfigurationManager.configuration["engine"]["tile-size"]
if self.elapsed_time >= Piece.GRAVITY:
self.elapsed_time -= Piece.GRAVITY
self.move((0, tile_size))
if well and self.collide(well):
self.revert()
if stack and self.collide(stack):
self.revert()
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 __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))

View File

@@ -1,10 +0,0 @@
from entity.Piece import Piece
from entity.Entity import Entity
class Stack(Entity):
def __init__(self, color: str, border_color: str):
super().__init__([], color, border_color)
def add_piece(self, piece: Piece):
self.points += piece.points

View 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):
super().__init__(self.__get_points(position), 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

16
main.py
View File

@@ -4,20 +4,18 @@
https://tetris.com/play-tetris
'''
# TODO review imports to make sure it is being done correctly
from Tetris import Tetris
from util.ConfigurationManager import ConfigurationManager
from tetris.game import Game
from tetris.util import ConfigurationManager
def main() -> None:
ConfigurationManager.load()
tetris = Tetris()
tetris.initialize()
game = Game()
game.initialize()
while True:
tetris.update()
tetris.draw()
game.update()
game.draw()
# start main function
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

406
tetris/entity.py Normal file
View 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
View 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
View 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]

View File

@@ -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)

View File

@@ -1,50 +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()
border_color = ConfigurationManager.configuration["color"]["border"] # TODO abstract color call to config out?
return Piece(cls.__get_piece_shape(cls.__bucket.pop()), position, cls.__get_piece_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() -> str:
random_number = random.randint(1, 3)
return ConfigurationManager.configuration["color"]["base-" + str(random_number)]