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

#!/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