11 Commits

Author SHA1 Message Date
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
12 changed files with 207 additions and 53 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/2cRKEi6.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.

View File

@@ -1,12 +1,13 @@
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?
# TODO should be a singleton and refactor the whole file?
class Tetris:
def __init__(self):
@@ -14,12 +15,14 @@ class Tetris:
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"]
@@ -30,20 +33,35 @@ class Tetris:
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"])
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.well, self.stack)
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():
@@ -70,43 +88,39 @@ class Tetris:
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:
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.stack.add_piece(self.current_piece)
self.__generate_piece((300, 100))
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["window"]["bg-color"])
bg_color = pygame.Color(ConfigurationManager.configuration["color"]["window-bg"])
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)
if self.current_piece:
self.current_piece.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.4.0",
description="Tetris Python Clone",
options={
"build_exe": {
@@ -30,7 +30,10 @@ setup(
],
"include_files": [
"config.yaml",
"tetris_icon.png"
"tetris_icon.png",
"main_music.ogg",
"piece_set_3.wav",
"row_completion.wav"
]
}
},

View File

@@ -3,16 +3,26 @@ window:
height: 600
icon: "tetris_icon.png"
title: "Tetris"
bg-color: "#6495ED" # cornflower-blue
sound:
main-music: "main_music.ogg"
row-completion: "row_completion.wav"
piece-set: "piece_set_3.wav"
engine:
fps: 60
tile-size: 20
piece-gravity-time: 400
piece-set-time: 600
# 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-border-1: "#000000"
well-1: "#9BFCF0"
well-border-1: "#000000"
stack-1: "#747474"
stack-border-1: "#000000"

View File

@@ -1,4 +1,6 @@
from typing import List, Tuple
from pygame import mixer
import pygame
import copy
from entity.Entity import Entity
@@ -9,28 +11,58 @@ from util.ConfigurationManager import ConfigurationManager
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):
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, well: Entity, stack: Entity):
def update(self, elapsed_time: int, tetris) -> None:
super().update(elapsed_time)
tile_size = ConfigurationManager.configuration["engine"]["tile-size"]
if self.elapsed_time >= Piece.GRAVITY:
self.elapsed_time -= Piece.GRAVITY
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):
self.revert()
if stack and self.collide(stack):
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
@@ -72,6 +104,10 @@ class Piece(Entity):
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"]

View File

@@ -1,10 +1,60 @@
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 add_piece(self, piece: Piece):
self.points += piece.points
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))

View File

@@ -8,8 +8,8 @@ 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 __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"]

BIN
main_music.ogg Normal file

Binary file not shown.

BIN
piece_set_3.wav Normal file

Binary file not shown.

BIN
row_completion.wav Normal file

Binary file not shown.

0
util/ControlsManger.py Normal file
View File

View File

@@ -16,9 +16,8 @@ class PieceGenerator:
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)
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:
@@ -45,6 +44,11 @@ class PieceGenerator:
return None
def __get_piece_color() -> str:
def __get_piece_color() -> Tuple:
random_number = random.randint(1, 3)
return ConfigurationManager.configuration["color"]["base-" + str(random_number)]
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)