You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
15 KiB
Python
352 lines
15 KiB
Python
2 years ago
|
#!/usr/bin/env python3
|
||
|
|
||
|
# File: tetris.py
|
||
|
# Description: Main file with tetris game.
|
||
|
# Author: Pavel Benáček <pavel.benacek@gmail.com>
|
||
|
|
||
|
# This program is free software: you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published by
|
||
|
# the Free Software Foundation, either version 3 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
import pygame
|
||
|
import pdb
|
||
|
|
||
|
import random
|
||
|
import math
|
||
|
import block
|
||
|
import constants
|
||
|
|
||
|
class Tetris(object):
|
||
|
"""
|
||
|
The class with implementation of tetris game logic.
|
||
|
"""
|
||
|
|
||
|
def __init__(self,bx,by):
|
||
|
"""
|
||
|
Initialize the tetris object.
|
||
|
|
||
|
Parameters:
|
||
|
- bx - number of blocks in x
|
||
|
- by - number of blocks in y
|
||
|
"""
|
||
|
# Compute the resolution of the play board based on the required number of blocks.
|
||
|
self.resx = bx*constants.BWIDTH+2*constants.BOARD_HEIGHT+constants.BOARD_MARGIN
|
||
|
self.resy = by*constants.BHEIGHT+2*constants.BOARD_HEIGHT+constants.BOARD_MARGIN
|
||
|
# Prepare the pygame board objects (white lines)
|
||
|
self.board_up = pygame.Rect(0,constants.BOARD_UP_MARGIN,self.resx,constants.BOARD_HEIGHT)
|
||
|
self.board_down = pygame.Rect(0,self.resy-constants.BOARD_HEIGHT,self.resx,constants.BOARD_HEIGHT)
|
||
|
self.board_left = pygame.Rect(0,constants.BOARD_UP_MARGIN,constants.BOARD_HEIGHT,self.resy)
|
||
|
self.board_right = pygame.Rect(self.resx-constants.BOARD_HEIGHT,constants.BOARD_UP_MARGIN,constants.BOARD_HEIGHT,self.resy)
|
||
|
# List of used blocks
|
||
|
self.blk_list = []
|
||
|
# Compute start indexes for tetris blocks
|
||
|
self.start_x = math.ceil(self.resx/2.0)
|
||
|
self.start_y = constants.BOARD_UP_MARGIN + constants.BOARD_HEIGHT + constants.BOARD_MARGIN
|
||
|
# Blocka data (shapes and colors). The shape is encoded in the list of [X,Y] points. Each point
|
||
|
# represents the relative position. The true/false value is used for the configuration of rotation where
|
||
|
# False means no rotate and True allows the rotation.
|
||
|
self.block_data = (
|
||
|
([[-8,0],[-7,0],[-6,0],[-5,0],[-4,0],[-3,0],[-2,0],[-1,0],[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],
|
||
|
[-8,1],[-7,1],[-6,1],[-5,1],[-4,1],[-3,1],[-2,1],[-1,1],[0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1],
|
||
|
[-8,2],[-7,2],[-6,2],[-5,2],[-4,2],[-3,2],[-2,2],[-1,2],[0,2],[1,2],[2,2],[3,2],[4,2],[5,2],[6,2],[7,2],
|
||
|
[-8,3],[-7,3],[-6,3],[-5,3],[-4,3],[-3,3],[-2,3],[-1,3],[0,3],[1,3],[2,3],[3,3],[4,3],[5,3],[6,3],[7,3],
|
||
|
[-8,4],[-7,4],[-6,4],[-5,4],[-4,4],[-3,4],[-2,4],[-1,4],[0,4],[1,4],[2,4],[3,4],[4,4],[5,4],[6,4],[7,4],
|
||
|
[-8,5],[-7,5],[-6,5],[-5,5],[-4,5],[-3,5],[-2,5],[-1,5],[0,5],[1,5],[2,5],[3,5],[4,5],[5,5],[6,5],[7,5]
|
||
|
],constants.RED,True), # I block
|
||
|
([[0,0]],constants.GREEN,True), # S block
|
||
|
# ([[0,0],[1,0],[2,0],[2,1]],constants.BLUE,True), # J block
|
||
|
# ([[0,0],[0,1],[1,0],[1,1]],constants.ORANGE,False), # O block
|
||
|
# ([[-1,0],[0,0],[0,1],[1,1]],constants.GOLD,True), # Z block
|
||
|
# ([[0,0],[1,0],[2,0],[1,1]],constants.PURPLE,True), # T block
|
||
|
# ([[0,0],[1,0],[2,0],[0,1]],constants.CYAN,True), # J block
|
||
|
)
|
||
|
# Compute the number of blocks. When the number of blocks is even, we can use it directly but
|
||
|
# we have to decrese the number of blocks in line by one when the number is odd (because of the used margin).
|
||
|
self.blocks_in_line = bx if bx%2 == 0 else bx-1
|
||
|
self.blocks_in_pile = by
|
||
|
# Score settings
|
||
|
self.score = 0
|
||
|
# Remember the current speed
|
||
|
self.speed = 1
|
||
|
# The score level threshold
|
||
|
self.score_level = constants.SCORE_LEVEL
|
||
|
|
||
|
def apply_action(self):
|
||
|
"""
|
||
|
Get the event from the event queue and run the appropriate
|
||
|
action.
|
||
|
"""
|
||
|
# Take the event from the event queue.
|
||
|
for ev in pygame.event.get():
|
||
|
# Check if the close button was fired.
|
||
|
if ev.type == pygame.QUIT or (ev.type == pygame.KEYDOWN and ev.unicode == 'q'):
|
||
|
self.done = True
|
||
|
# Detect the key evevents for game control.
|
||
|
if ev.type == pygame.KEYDOWN:
|
||
|
if ev.key == pygame.K_DOWN:
|
||
|
self.active_block.move(0,constants.BHEIGHT)
|
||
|
if ev.key == pygame.K_LEFT:
|
||
|
self.active_block.move(-constants.BWIDTH,0)
|
||
|
if ev.key == pygame.K_RIGHT:
|
||
|
self.active_block.move(constants.BWIDTH,0)
|
||
|
if ev.key == pygame.K_SPACE:
|
||
|
self.active_block.rotate()
|
||
|
if ev.key == pygame.K_p:
|
||
|
self.pause()
|
||
|
|
||
|
# Detect if the movement event was fired by the timer.
|
||
|
if ev.type == constants.TIMER_MOVE_EVENT:
|
||
|
self.active_block.move(0,constants.BHEIGHT)
|
||
|
|
||
|
def pause(self):
|
||
|
"""
|
||
|
Pause the game and draw the string. This function
|
||
|
also calls the flip function which draws the string on the screen.
|
||
|
"""
|
||
|
# Draw the string to the center of the screen.
|
||
|
self.print_center(["PAUSE","Press \"p\" to continue"])
|
||
|
pygame.display.flip()
|
||
|
while True:
|
||
|
for ev in pygame.event.get():
|
||
|
if ev.type == pygame.KEYDOWN and ev.key == pygame.K_p:
|
||
|
return
|
||
|
|
||
|
def set_move_timer(self):
|
||
|
"""
|
||
|
Setup the move timer to the
|
||
|
"""
|
||
|
# Setup the time to fire the move event. Minimal allowed value is 1
|
||
|
speed = math.floor(constants.MOVE_TICK / self.speed)
|
||
|
speed = max(1,speed)
|
||
|
pygame.time.set_timer(constants.TIMER_MOVE_EVENT,speed)
|
||
|
|
||
|
def run(self):
|
||
|
# Initialize the game (pygame, fonts)
|
||
|
pygame.init()
|
||
|
pygame.font.init()
|
||
|
self.myfont = pygame.font.SysFont(pygame.font.get_default_font(),constants.FONT_SIZE)
|
||
|
self.screen = pygame.display.set_mode((self.resx,self.resy))
|
||
|
pygame.display.set_caption("Tetris")
|
||
|
# Setup the time to fire the move event every given time
|
||
|
self.set_move_timer()
|
||
|
# Control variables for the game. The done signal is used
|
||
|
# to control the main loop (it is set by the quit action), the game_over signal
|
||
|
# is set by the game logic and it is also used for the detection of "game over" drawing.
|
||
|
# Finally the new_block variable is used for the requesting of new tetris block.
|
||
|
self.done = False
|
||
|
self.game_over = False
|
||
|
self.new_block = True
|
||
|
# Print the initial score
|
||
|
self.print_status_line()
|
||
|
while not(self.done) and not(self.game_over):
|
||
|
# Get the block and run the game logic
|
||
|
self.get_block()
|
||
|
self.game_logic()
|
||
|
self.draw_game()
|
||
|
# Display the game_over and wait for a keypress
|
||
|
if self.game_over:
|
||
|
self.print_game_over()
|
||
|
# Disable the pygame stuff
|
||
|
pygame.font.quit()
|
||
|
pygame.display.quit()
|
||
|
|
||
|
def print_status_line(self):
|
||
|
"""
|
||
|
Print the current state line
|
||
|
"""
|
||
|
string = ["SCORE: {0} SPEED: {1}x".format(self.score,self.speed)]
|
||
|
self.print_text(string,constants.POINT_MARGIN,constants.POINT_MARGIN)
|
||
|
|
||
|
def print_game_over(self):
|
||
|
"""
|
||
|
Print the game over string.
|
||
|
"""
|
||
|
# Print the game over text
|
||
|
self.print_center(["Game Over","Press \"q\" to exit"])
|
||
|
# Draw the string
|
||
|
pygame.display.flip()
|
||
|
# Wait untill the space is pressed
|
||
|
while True:
|
||
|
for ev in pygame.event.get():
|
||
|
if ev.type == pygame.QUIT or (ev.type == pygame.KEYDOWN and ev.unicode == 'q'):
|
||
|
return
|
||
|
|
||
|
def print_text(self,str_lst,x,y):
|
||
|
"""
|
||
|
Print the text on the X,Y coordinates.
|
||
|
|
||
|
Parameters:
|
||
|
- str_lst - list of strings to print. Each string is printed on new line.
|
||
|
- x - X coordinate of the first string
|
||
|
- y - Y coordinate of the first string
|
||
|
"""
|
||
|
prev_y = 0
|
||
|
for string in str_lst:
|
||
|
size_x,size_y = self.myfont.size(string)
|
||
|
txt_surf = self.myfont.render(string,False,(255,255,255))
|
||
|
self.screen.blit(txt_surf,(x,y+prev_y))
|
||
|
prev_y += size_y
|
||
|
|
||
|
def print_center(self,str_list):
|
||
|
"""
|
||
|
Print the string in the center of the screen.
|
||
|
|
||
|
Parameters:
|
||
|
- str_lst - list of strings to print. Each string is printed on new line.
|
||
|
"""
|
||
|
max_xsize = max([tmp[0] for tmp in map(self.myfont.size,str_list)])
|
||
|
self.print_text(str_list,self.resx/2-max_xsize/2,self.resy/2)
|
||
|
|
||
|
def block_colides(self):
|
||
|
"""
|
||
|
Check if the block colides with any other block.
|
||
|
|
||
|
The function returns True if the collision is detected.
|
||
|
"""
|
||
|
for blk in self.blk_list:
|
||
|
# Check if the block is not the same
|
||
|
if blk == self.active_block:
|
||
|
continue
|
||
|
# Detect situations
|
||
|
if(blk.check_collision(self.active_block.shape)):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def game_logic(self):
|
||
|
"""
|
||
|
Implementation of the main game logic. This function detects colisions
|
||
|
and insertion of new tetris blocks.
|
||
|
"""
|
||
|
# Remember the current configuration and try to
|
||
|
# apply the action
|
||
|
self.active_block.backup()
|
||
|
self.apply_action()
|
||
|
# Border logic, check if we colide with down border or any
|
||
|
# other border. This check also includes the detection with other tetris blocks.
|
||
|
down_board = self.active_block.check_collision([self.board_down])
|
||
|
any_border = self.active_block.check_collision([self.board_left,self.board_up,self.board_right])
|
||
|
block_any = self.block_colides()
|
||
|
# Restore the configuration if any collision was detected
|
||
|
if down_board or any_border or block_any:
|
||
|
self.active_block.restore()
|
||
|
# So far so good, sample the previous state and try to move down (to detect the colision with other block).
|
||
|
# After that, detect the the insertion of new block. The block new block is inserted if we reached the boarder
|
||
|
# or we cannot move down.
|
||
|
self.active_block.backup()
|
||
|
self.active_block.move(0,constants.BHEIGHT)
|
||
|
can_move_down = not self.block_colides()
|
||
|
self.active_block.restore()
|
||
|
# We end the game if we are on the respawn and we cannot move --> bang!
|
||
|
if not can_move_down and (self.start_x == self.active_block.x and self.start_y == self.active_block.y):
|
||
|
self.game_over = True
|
||
|
# The new block is inserted if we reached down board or we cannot move down.
|
||
|
if down_board or not can_move_down:
|
||
|
# Request new block
|
||
|
self.new_block = True
|
||
|
# Detect the filled line and possibly remove the line from the
|
||
|
# screen.
|
||
|
self.detect_line()
|
||
|
|
||
|
def detect_line(self):
|
||
|
"""
|
||
|
Detect if the line is filled. If yes, remove the line and
|
||
|
move with remaining bulding blocks to new positions.
|
||
|
"""
|
||
|
# Get each shape block of the non-moving tetris block and try
|
||
|
# to detect the filled line. The number of bulding blocks is passed to the class
|
||
|
# in the init function.
|
||
|
for shape_block in self.active_block.shape:
|
||
|
tmp_y = shape_block.y
|
||
|
tmp_cnt = self.get_blocks_in_line(tmp_y)
|
||
|
# Detect if the line contains the given number of blocks
|
||
|
if tmp_cnt != self.blocks_in_line:
|
||
|
continue
|
||
|
# Ok, the full line is detected!
|
||
|
self.remove_line(tmp_y)
|
||
|
# Update the score.
|
||
|
self.score += self.blocks_in_line * constants.POINT_VALUE
|
||
|
# Check if we need to speed up the game. If yes, change control variables
|
||
|
if self.score > self.score_level:
|
||
|
self.score_level *= constants.SCORE_LEVEL_RATIO
|
||
|
self.speed *= constants.GAME_SPEEDUP_RATIO
|
||
|
# Change the game speed
|
||
|
self.set_move_timer()
|
||
|
|
||
|
def remove_line(self,y):
|
||
|
"""
|
||
|
Remove the line with given Y coordinates. Blocks below the filled
|
||
|
line are untouched. The rest of blocks (yi > y) are moved one level done.
|
||
|
|
||
|
Parameters:
|
||
|
- y - Y coordinate to remove.
|
||
|
"""
|
||
|
# Iterate over all blocks in the list and remove blocks with the Y coordinate.
|
||
|
for block in self.blk_list:
|
||
|
block.remove_blocks(y)
|
||
|
# Setup new block list (not needed blocks are removed)
|
||
|
self.blk_list = [blk for blk in self.blk_list if blk.has_blocks()]
|
||
|
|
||
|
def get_blocks_in_line(self,y):
|
||
|
"""
|
||
|
Get the number of shape blocks on the Y coordinate.
|
||
|
|
||
|
Parameters:
|
||
|
- y - Y coordinate to scan.
|
||
|
"""
|
||
|
# Iteraveovel all block's shape list and increment the counter
|
||
|
# if the shape block equals to the Y coordinate.
|
||
|
tmp_cnt = 0
|
||
|
for block in self.blk_list:
|
||
|
for shape_block in block.shape:
|
||
|
tmp_cnt += (1 if y == shape_block.y else 0)
|
||
|
return tmp_cnt
|
||
|
|
||
|
def draw_board(self):
|
||
|
"""
|
||
|
Draw the white board.
|
||
|
"""
|
||
|
pygame.draw.rect(self.screen,constants.WHITE,self.board_up)
|
||
|
pygame.draw.rect(self.screen,constants.WHITE,self.board_down)
|
||
|
pygame.draw.rect(self.screen,constants.WHITE,self.board_left)
|
||
|
pygame.draw.rect(self.screen,constants.WHITE,self.board_right)
|
||
|
# Update the score
|
||
|
self.print_status_line()
|
||
|
|
||
|
def get_block(self):
|
||
|
"""
|
||
|
Generate new block into the game if is required.
|
||
|
"""
|
||
|
if self.new_block:
|
||
|
# Get the block and add it into the block list(static for now)
|
||
|
tmp = random.randint(0,len(self.block_data)-1)
|
||
|
data = self.block_data[tmp]
|
||
|
self.active_block = block.Block(data[0],self.start_x,self.start_y,self.screen,data[1],data[2])
|
||
|
self.blk_list.append(self.active_block)
|
||
|
self.new_block = False
|
||
|
|
||
|
def draw_game(self):
|
||
|
"""
|
||
|
Draw the game screen.
|
||
|
"""
|
||
|
# Clean the screen, draw the board and draw
|
||
|
# all tetris blocks
|
||
|
self.screen.fill(constants.BLACK)
|
||
|
self.draw_board()
|
||
|
for blk in self.blk_list:
|
||
|
blk.draw()
|
||
|
# Draw the screen buffer
|
||
|
pygame.display.flip()
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
Tetris(60,45).run()
|
||
|
|
||
|
#Special add to try pull requests
|