Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Tetris Clone with Pygame

Tech 1

Blocks Module

This module defines all the tetromino shapes used in the game. Each piece is represented as a template with rotatoin states.

import random
from collections import namedtuple

Point = namedtuple('Point', 'X Y')
ShapeDef = namedtuple('ShapeDef', 'template start_pos end_pos name rotation')
Tetromino = namedtuple('Tetromino', 'template start_pos end_pos name next')

# Tetromino definitions using 4x4 grid templates
# '.' represents empty space, 'O' represents a filled block

# S-piece: two horizontal blocks stacked with offset
S_SHAPE = [
    Tetromino(['.OO',
               'OO.',
               '...'], Point(0, 0), Point(2, 1), 'S', 1),
    Tetromino(['O..',
               'OO.',
               '.O.'], Point(0, 0), Point(1, 2), 'S', 0)]

# Z-piece: mirror of S-piece
Z_SHAPE = [
    Tetromino(['OO.',
               '.OO',
               '...'], Point(0, 0), Point(2, 1), 'Z', 1),
    Tetromino(['.O.',
               'OO.',
               'O..'], Point(0, 0), Point(1, 2), 'Z', 0)]

# I-piece: vertical or horizontal line
I_SHAPE = [
    Tetromino(['.O..',
               '.O..',
               '.O..',
               '.O..'], Point(1, 0), Point(1, 3), 'I', 1),
    Tetromino(['....',
               '....',
               'OOOO',
               '....'], Point(0, 2), Point(3, 2), 'I', 0)]

# O-piece: 2x2 square
O_SHAPE = [
    Tetromino(['OO',
               'OO'], Point(0, 0), Point(1, 1), 'O', 0)]

# J-piece: L-shaped with variant rotations
J_SHAPE = [
    Tetromino(['O..',
               'OOO',
               '...'], Point(0, 0), Point(2, 1), 'J', 1),
    Tetromino(['.OO',
               '.O.',
               '.O.'], Point(1, 0), Point(2, 2), 'J', 2),
    Tetromino(['...',
               'OOO',
               '..O'], Point(0, 1), Point(2, 2), 'J', 3),
    Tetromino(['.O.',
               '.O.',
               'OO.'], Point(0, 0), Point(1, 2), 'J', 0)]

# L-piece: mirror of J-piece
L_SHAPE = [
    Tetromino(['..O',
               'OOO',
               '...'], Point(0, 0), Point(2, 1), 'L', 1),
    Tetromino(['.O.',
               '.O.',
               '.OO'], Point(1, 0), Point(2, 2), 'L', 2),
    Tetromino(['...',
               'OOO',
               'O..'], Point(0, 1), Point(2, 2), 'L', 3),
    Tetromino(['OO.',
               '.O.',
               '.O.'], Point(0, 0), Point(1, 2), 'L', 0)]

# T-piece: T-shaped
T_SHAPE = [
    Tetromino(['.O.',
               'OOO',
               '...'], Point(0, 0), Point(2, 1), 'T', 1),
    Tetromino(['.O.',
               '.OO',
               '.O.'], Point(1, 0), Point(2, 2), 'T', 2),
    Tetromino(['...',
               'OOO',
               '.O.'], Point(0, 1), Point(2, 2), 'T', 3),
    Tetromino(['.O.',
               'OO.',
               '.O.'], Point(0, 0), Point(1, 2), 'T', 0)]

# Collection of all tetrominoes
TETROMINOES = {
    'O': O_SHAPE,
    'I': I_SHAPE,
    'Z': Z_SHAPE,
    'T': T_SHAPE,
    'L': L_SHAPE,
    'S': S_SHAPE,
    'J': J_SHAPE
}


def spawn_tetromino():
    """Generate a random tetromino piece."""
    piece_type = random.choice('OIJLSTZ')
    variants = TETROMINOES[piece_type]
    rotation_index = random.randint(0, len(variants) - 1)
    return variants[rotation_index]


def get_next_rotation(current_piece):
    """Get the next rotation state of a piece."""
    variants = TETROMINOES[current_piece.name]
    return variants[current_piece.next]

Main Game Module

This module handles the game loop, rendering, input processign, and game mechanics.

# -*- coding: utf-8 -*-
import sys
import time
import pygame
from pygame.locals import *
import blocks

CELL_SIZE = 30
GRID_HEIGHT = 20
GRID_WIDTH = 10
BORDER_SIZE = 4
BORDER_COLOR = (40, 40, 200)
WINDOW_WIDTH = CELL_SIZE * (GRID_WIDTH + 5)
WINDOW_HEIGHT = CELL_SIZE * GRID_HEIGHT
BACKGROUND_COLOR = (40, 40, 60)
GRID_COLOR = (0, 0, 0)
PIECE_COLOR = (20, 128, 200)
GAMEOVER_COLOR = (200, 30, 30)


def draw_text(surface, font_obj, x, y, content, fcolor=(255, 255, 255)):
    """Render and display text on screen."""
    text_surface = font_obj.render(content, True, fcolor)
    surface.blit(text_surface, (x, y))


def main():
    pygame.init()
    screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
    pygame.display.set_caption('Tetris')

    score_font = pygame.font.SysFont('SimHei', 24)
    gameover_font = pygame.font.Font(None, 72)
    
    info_panel_x = GRID_WIDTH * CELL_SIZE + BORDER_SIZE + 10
    gameover_dims = gameover_font.size('GAME OVER')
    font_height = int(score_font.size('Score')[1])

    current_piece = None
    preview_piece = None
    current_color = PIECE_COLOR
    piece_x, piece_y = 0, 0

    grid = None
    game_over = True
    game_started = False
    total_score = 0
    base_drop_interval = 0.5
    current_drop_interval = base_drop_interval
    is_paused = False
    last_drop_time = None
    last_keypress_time = None

    def lock_piece():
        """Lock current piece into the grid and check for line clears."""
        nonlocal current_piece, preview_piece, grid, piece_x, piece_y, game_over, total_score, current_drop_interval
        
        for row in range(current_piece.start_pos.Y, current_piece.end_pos.Y + 1):
            for col in range(current_piece.start_pos.X, current_piece.end_pos.X + 1):
                if current_piece.template[row][col] != '.':
                    grid[piece_y + row][piece_x + col] = 'X'
        
        if piece_y + current_piece.start_pos.Y <= 0:
            game_over = True
        else:
            # Check for completed lines
            completed_lines = []
            for row in range(current_piece.start_pos.Y, current_piece.end_pos.Y + 1):
                if all(cell == 'X' for cell in grid[piece_y + row]):
                    completed_lines.append(piece_y + row)
            
            if completed_lines:
                # Calculate score based on lines cleared
                line_count = len(completed_lines)
                if line_count == 1:
                    total_score += 100
                elif line_count == 2:
                    total_score += 300
                elif line_count == 3:
                    total_score += 700
                elif line_count == 4:
                    total_score += 1500
                
                current_drop_interval = base_drop_interval - 0.03 * (total_score // 10000)
                
                # Remove completed lines and shift above rows down
                dest_row = src_row = completed_lines[-1]
                while src_row >= 0:
                    while src_row in completed_lines:
                        src_row -= 1
                    if src_row < 0:
                        grid[dest_row] = ['.'] * GRID_WIDTH
                    else:
                        grid[dest_row] = grid[src_row]
                    dest_row -= 1
                    src_row -= 1
            
            current_piece = preview_piece
            preview_piece = blocks.spawn_tetromino()
            piece_x, piece_y = (GRID_WIDTH - current_piece.end_pos.X - 1) // 2, -1 - current_piece.end_pos.Y

    def can_place(pos_x, pos_y, piece):
        """Check if piece can be placed at given position."""
        nonlocal grid
        for row in range(piece.start_pos.Y, piece.end_pos.Y + 1):
            if pos_y + piece.end_pos.Y >= GRID_HEIGHT:
                return False
            for col in range(piece.start_pos.X, piece.end_pos.X + 1):
                if pos_y + row >= 0 and piece.template[row][col] != '.' and grid[pos_y + row][pos_x + col] != '.':
                    return False
        return True

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()
            elif event.type == KEYDOWN:
                if event.key == K_RETURN:
                    if game_over:
                        game_started = True
                        game_over = False
                        total_score = 0
                        last_drop_time = time.time()
                        last_keypress_time = time.time()
                        grid = [['.'] * GRID_WIDTH for _ in range(GRID_HEIGHT)]
                        current_piece = blocks.spawn_tetromino()
                        preview_piece = blocks.spawn_tetromino()
                        piece_x, piece_y = (GRID_WIDTH - current_piece.end_pos.X - 1) // 2, -1 - current_piece.end_pos.Y
                elif event.key == K_SPACE:
                    if not game_over:
                        is_paused = not is_paused
                elif event.key in (K_w, K_UP):
                    # Rotate piece
                    if 0 <= piece_x <= GRID_WIDTH - len(current_piece.template[0]):
                        next_rotation = blocks.get_next_rotation(current_piece)
                        if can_place(piece_x, piece_y, next_rotation):
                            current_piece = next_rotation

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                if not game_over and not is_paused:
                    if time.time() - last_keypress_time > 0.1:
                        last_keypress_time = time.time()
                        if piece_x > -current_piece.start_pos.X:
                            if can_place(piece_x - 1, piece_y, current_piece):
                                piece_x -= 1
            if event.key == pygame.K_RIGHT:
                if not game_over and not is_paused:
                    if time.time() - last_keypress_time > 0.1:
                        last_keypress_time = time.time()
                        if piece_x + current_piece.end_pos.X + 1 < GRID_WIDTH:
                            if can_place(piece_x + 1, piece_y, current_piece):
                                piece_x += 1
            if event.key == pygame.K_DOWN:
                if not game_over and not is_paused:
                    if time.time() - last_keypress_time > 0.1:
                        last_keypress_time = time.time()
                        if not can_place(piece_x, piece_y + 1, current_piece):
                            lock_piece()
                        else:
                            last_drop_time = time.time()
                            piece_y += 1

        # Fill background
        screen.fill(BACKGROUND_COLOR)
        
        # Draw border between game area and info panel
        pygame.draw.line(screen, BORDER_COLOR,
                         (CELL_SIZE * GRID_WIDTH + BORDER_SIZE // 2, 0),
                         (CELL_SIZE * GRID_WIDTH + BORDER_SIZE // 2, WINDOW_HEIGHT), BORDER_SIZE)

        # Draw locked pieces
        if grid:
            for row_idx, row_data in enumerate(grid):
                for col_idx, cell in enumerate(row_data):
                    if cell != '.':
                        pygame.draw.rect(screen, current_color, 
                                         (col_idx * CELL_SIZE, row_idx * CELL_SIZE, CELL_SIZE, CELL_SIZE), 0)

        # Draw grid lines
        for x in range(GRID_WIDTH):
            pygame.draw.line(screen, GRID_COLOR, (x * CELL_SIZE, 0), (x * CELL_SIZE, WINDOW_HEIGHT), 1)
        for y in range(GRID_HEIGHT):
            pygame.draw.line(screen, GRID_COLOR, (0, y * CELL_SIZE), (GRID_WIDTH * CELL_SIZE, y * CELL_SIZE), 1)

        if not game_over:
            current_time = time.time()
            if current_time - last_drop_time > current_drop_interval:
                if not is_paused:
                    if not can_place(piece_x, piece_y + 1, current_piece):
                        lock_piece()
                    else:
                        last_drop_time = current_time
                        piece_y += 1
        else:
            if game_started:
                draw_text(screen, gameover_font,
                          (WINDOW_WIDTH - gameover_dims[0]) // 2, 
                          (WINDOW_HEIGHT - gameover_dims[1]) // 2,
                          'GAME OVER', GAMEOVER_COLOR)

        # Draw falling piece
        if current_piece:
            for row in range(current_piece.start_pos.Y, current_piece.end_pos.Y + 1):
                for col in range(current_piece.start_pos.X, current_piece.end_pos.X + 1):
                    if current_piece.template[row][col] != '.':
                        pygame.draw.rect(screen, current_color,
                                         ((piece_x + col) * CELL_SIZE, 
                                          (piece_y + row) * CELL_SIZE, 
                                          CELL_SIZE, CELL_SIZE), 0)

        # Display score and next piece info
        draw_text(screen, score_font, info_panel_x, 10, f'Score: ')
        draw_text(screen, score_font, info_panel_x, 10 + font_height + 6, f'{total_score}')
        draw_text(screen, score_font, info_panel_x, 20 + (font_height + 6) * 2, f'Level: ')
        draw_text(screen, score_font, info_panel_x, 20 + (font_height + 6) * 3, f'{total_score // 10000}')
        draw_text(screen, score_font, info_panel_x, 30 + (font_height + 6) * 4, f'Next:')

        if preview_piece:
            preview_y = 30 + (font_height + 6) * 5
            for row in range(preview_piece.start_pos.Y, preview_piece.end_pos.Y + 1):
                for col in range(preview_piece.start_pos.X, preview_piece.end_pos.X + 1):
                    if preview_piece.template[row][col] != '.':
                        pygame.draw.rect(screen, current_color, 
                                         (info_panel_x + col * CELL_SIZE, 
                                          preview_y + row * CELL_SIZE, 
                                          CELL_SIZE, CELL_SIZE), 0)

        pygame.display.flip()


if __name__ == '__main__':
    main()
Tags: Python

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.