How to create a Tetris clone with python and pygame (2020 tutorial)

 This is a simple python game, use left and right arrows to move left and right.



# tetris
import pygame, sys, random, time
from pygame.locals import *

SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480

BOARD_WIDTH = 10
BOARD_HEIGHT = 20
SQUARE_SIZE = 20

# coordinates of the board's top-left corner
START_POS = (218, 78)

# fps
FPS = 30

# color set up
WHITE = (255,255,255)
BLACK = (0,0,0)
RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
LIGHT_GREEN = (144,238,144)


# define shapes
# T shape data structure
t_shape = [[
      '     ',
      '  *  ',
      ' *** ',
      '     ',
      '     ',
      ],
           [
      '     ',
      ' *** ',
      '  *  ',
      '     ',
      '     ',
      ],
           [
      '     ',
      '  *  ',
      ' **  ',
      '  *  ',
      '     ',
      ],
           [
      '     ',
      '  *  ',
      '  ** ',
      '  *  ',
      '     ',
      ],
           ]

s_shape = [[
      '     ',
      '     ',
      '  ** ',
      ' **  ',
      '     ',
      ],
           [
      '     ',
      '  *  ',
      '  ** ',
      '   * ',
      '     ',
      ],
           ]

z_shape = [[
      '     ',
      '     ',
      ' **  ',
      '  ** ',
      '     ',
      ],
           [
      '     ',
      '  *  ',
      ' **  ',
      ' *   ',
      '     ',
      ],
           ]

i_shape = [[
      '  *  ',
      '  *  ',
      '  *  ',
      '  *  ',
      '     ',
      ],
           [
      '     ',
      '     ',
      '**** ',
      '     ',
      '     ',
      ],
           ]

o_shape = [[
      '     ',
      '     ',
      ' **  ',
      ' **  ',
      '     ',
      ],
           ]

l_shape = [[
      '     ',
      '   * ',
      ' *** ',
      '     ',
      '     ',
      ],
           [
      '     ',
      '  *  ',
      '  *  ',
      '  ** ',
      '     ',
      ],
           [
      '     ',
      '     ',
      ' *** ',
      ' *   ',
      '     ',
      ],
           [
      '     ',
      ' **  ',
      '  *  ',
      '  *  ',
      '     ',
      ],
           ]

j_shape = [[
      '     ',
      ' *   ',
      ' *** ',
      '     ',
      '     ',
      ],
           [
      '     ',
      '  *  ',
      '  *  ',
      ' **  ',
      '     ',
      ],
           [
      '     ',
      ' *   ',
      ' *** ',
      '     ',
      '     ',
      ],
           [
      '     ',
      '  ** ',
      '  *  ',
      '  *  ',
      '     ',
      ],
           ]

shapes = {
      'T': t_shape,
      'S': s_shape,
      'Z': z_shape,
      'J': j_shape,
      'L': l_shape,
      'I': i_shape,
      'O': o_shape,
      }
# generate the data structure of the pattern
# it is a dictionary, contains all the information about the pattern
# shape: the shape of the pattern, 'T', 'S', 'Z','J','L','I', 'O'
# state: the state of the shape, 'T' has four states...
# x: the starting x of the pattern, default 3
# y: the starting y of the pattern, default 0
def get_random_pattern():
      # select a shape randomly
      shape_name = random.choice('TSZJLIO')
      shape = shapes[shape_name]
      pattern = {
                  'shape': shape_name,
                  'state': random.randint(0, len(shape)-1),
                  'x': 3,
                  'y': 0,
            }
      return pattern


def draw_shape(pattern, next_pattern):
      global screen
      shape_pattern = shapes[pattern['shape']][pattern['state']]
      next_shape_pattern = shapes[next_pattern['shape']][next_pattern['state']]
      start_x, start_y = pattern['x'], pattern['y']
      next_start_x, next_start_y = next_pattern['x'], next_pattern['y']
      for i in range(5):
            for j in range(5):
                  if shape_pattern[j][i] == '*':
                        rect = pygame.Rect((START_POS[0]+1)+(start_x+i)*SQUARE_SIZE,
                                           (START_POS[1]+1)+(start_y+j)*SQUARE_SIZE,
                                           SQUARE_SIZE, SQUARE_SIZE)
                        light_rect = pygame.Rect((START_POS[0]+4)+(start_x+i)*SQUARE_SIZE,
                                           (START_POS[1]+4)+(start_y+j)*SQUARE_SIZE,
                                           SQUARE_SIZE-3, SQUARE_SIZE-3)
                        pygame.draw.rect(screen, GREEN, rect)
                        pygame.draw.rect(screen, LIGHT_GREEN, light_rect)
                        
                  if next_shape_pattern[j][i] == '*':
                        next_rect = pygame.Rect((420)+(next_start_x+i)*SQUARE_SIZE,
                                                (100)+(next_start_y+j)*SQUARE_SIZE,
                                                SQUARE_SIZE, SQUARE_SIZE)
                        light_next_rect = pygame.Rect((420+3)+(next_start_x+i)*SQUARE_SIZE,
                                                (100+3)+(next_start_y+j)*SQUARE_SIZE,
                                                SQUARE_SIZE-3, SQUARE_SIZE-3)
                        pygame.draw.rect(screen, GREEN, next_rect)
                        pygame.draw.rect(screen, LIGHT_GREEN, light_next_rect)
# generate the board data structure, namely a list of list or 2D list
def get_board():
      board = []
      for i in range(BOARD_WIDTH):
            board.append([])
            for j in range(BOARD_HEIGHT):
                  board[i].append('')
      return board

def draw_board(board):
      global screen
      screen.fill(BLACK)
      # draw the board boarder
      boarder_rect = pygame.Rect(START_POS[0]-2,START_POS[1]-2,SQUARE_SIZE*BOARD_WIDTH+2, SQUARE_SIZE*BOARD_HEIGHT+2)
      pygame.draw.rect(screen, BLUE, boarder_rect, 2)
      # draw the board on the screen
      for i in range(BOARD_HEIGHT):
            for j in range(BOARD_WIDTH):
                  if board[j][i]: # if board cell is not empty string ''
                        rect = pygame.Rect(START_POS[0]+j*SQUARE_SIZE,START_POS[1]+i*SQUARE_SIZE,SQUARE_SIZE, SQUARE_SIZE)
                        light_rect = pygame.Rect(START_POS[0]+3+j*SQUARE_SIZE,
                                                 START_POS[1]+3+i*SQUARE_SIZE,
                                                 SQUARE_SIZE-3, SQUARE_SIZE-3)
                        pygame.draw.rect(screen, GREEN, rect)
                        pygame.draw.rect(screen, LIGHT_GREEN, light_rect)

def touch_bottom(pattern):
      shape_pattern = shapes[pattern['shape']][pattern['state']]
      y = pattern['y']
      for i in range(5):
            for j in range(5):
                  if shape_pattern[j][i] == '*':
                        if j + y == (BOARD_HEIGHT-1):
                              return True
      return False

def touch_others(pattern):
      shape_pattern = shapes[pattern['shape']][pattern['state']]
      x, y = pattern['x'], pattern['y']
      touch_pos = []
      for i in range(BOARD_WIDTH):
            for j in range(BOARD_HEIGHT):
                  if board[i][j] == '*' and board[i][j-1] == '':
                        touch_pos.append((i,j-1))
      if touch_pos:
            for i in range(5):
                  for j in range(5):
                        if shape_pattern[j][i] == '*':
                              if (x+i, y+j) in touch_pos:
                                    return True
      return False

def add_pattern_to_board(pattern):
      shape_pattern = shapes[pattern['shape']][pattern['state']]
      x, y = pattern['x'], pattern['y']
      for i in range(5):
            for j in range(5):
                  if shape_pattern[j][i] == '*':
                        board[x+i][y+j] = '*'

def is_valid_pos(pattern):
      shape_pattern = shapes[pattern['shape']][pattern['state']]
      x, y = pattern['x'], pattern['y']
      for i in range(5):
            for j in range(5):
                  if shape_pattern[j][i] == '*':
                        if not (0 <= x+i <= 9):
                              return False
                        elif board[x+i][y+j] == '*':
                              return False
      return True

# covert between row-based data structure and column-based data structure
def convert_board(board, colomn_based=True):
      b = []
      if colomn_based:
            for i in range(len(board[0])):
                  b.append([])
                  for j in range(len(board)):
                        b[i].append(board[j][i])
            return b
      else:
            for i in range(len(board[0])):
                  b.append([])
                  for j in range(len(board)):
                        b[i].append(board[j][i])
            return b
                  
def check_board_update():
##      print('check_board_update')
##      print(board)
      # convert column-based board to row-based board
      global board, score
      b = convert_board(board.copy())
      for row in range(len(b)):
            if b[row] == ['*'] * BOARD_WIDTH:
                  score += 1
                  while row > 0:
                        b[row] = b[row-1]
                        row -= 1
      # convert row-based board to column-based board
      b = convert_board(b, False)
      # update the board with b
      board = b
      
##      print(board)             
def display_game_info():
      global info_font, score
      score_surf = info_font.render(f'Score: {score}', True, WHITE)
      next_surf = info_font.render('Next: ', True, WHITE)
      screen.blit(score_surf,(450, 20))
      screen.blit(next_surf,(450, 60))

def game_over(pattern):
      global board
      shape_pattern = shapes[pattern['shape']][pattern['state']]
      x, y = pattern['x'], pattern['y']
      for i in range(5):
            for j in range(5):
                  if shape_pattern[j][i] == '*':
                        if board[x+i][y+j] == '*':
                              return True
      return False

def show_game_over():
      global info_font, screen
      game_over_surf = info_font.render('GAME OVER', True, WHITE)
      screen.blit(game_over_surf, (240, 200))
pygame.init()

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Tetris')


game_clock = pygame.time.Clock()

board = get_board()

pattern = get_random_pattern()
next_pattern = get_random_pattern()

# time before and after drop
# we use the difference between begin_drop_time and end_drop_time to control the drop frequency
begin_drop_time = time.time()
end_drop_time = time.time()
drop_interval = 1 # each drop will take roughly 1 second

# score
score = 0
info_font = pygame.font.SysFont('arial', 38)

key_down_pressed = False
while True:
      
      for event in pygame.event.get():
            if event.type == QUIT:
                  pygame.quit()
                  sys.exit()
            elif event.type == KEYDOWN:
                  # handle left and right movement
                  if event.key == K_LEFT:
                        pattern['x'] -= 1
                        if not is_valid_pos(pattern):
                              pattern['x'] += 1
                  elif event.key == K_RIGHT:
                        pattern['x'] += 1
                        if not is_valid_pos(pattern):
                              pattern['x'] -= 1
                  elif event.key == K_DOWN:
                        # move the pattern to the bottom quickly
                        key_down_pressed = True
                  # handle state change
                  elif event.key == K_UP:
                        # print(pattern['shape'])
                        original_state = pattern['state']
                        # print(original_state)
                        pattern['state'] = (pattern['state'] + 1) % len(shapes[pattern['shape']])
                        if not is_valid_pos(pattern): # if there is no enough space to rotate
                              pattern['state'] = original_state # go back to the original state
           
      #print(end_drop_time - begin_drop_time)
      if end_drop_time - begin_drop_time > drop_interval or key_down_pressed:
            pattern['y'] += 1
            begin_drop_time = time.time()
            
            
      if touch_bottom(pattern) or touch_others(pattern):
            if key_down_pressed:
                  key_down_pressed = False
            # if pattern touches bottom, add the pattern to the board 
            add_pattern_to_board(pattern)
            # check_board for eliminate the full rows
            check_board_update()
            # get a new random pattern for next drop
            pattern = next_pattern
            next_pattern = get_random_pattern()

      if game_over(pattern):
            draw_board(board)
            draw_shape(pattern, next_pattern)
            show_game_over()
            pygame.display.update()
            break
                  
            
      end_drop_time = time.time()
      # draw everything on the screen
      draw_board(board)
      draw_shape(pattern, next_pattern)
      display_game_info()
      
      pygame.display.update()    
      game_clock.tick(FPS)

Comments

Popular posts from this blog

How to write a slide puzzle game with Python and Pygame (2020 tutorial)

How to create a memory puzzle game with Python and Pygame (#005)

Introduction to multitasking with Python #001 multithreading (2020 tutorial)