Chat
Ask me anything
Ithy Logo

Developing a Mobile-Friendly Tetris Game in Python

A Comprehensive Guide to Building Tetris for Mobile Devices Using Python

mobile tetris game

Key Takeaways

  • Utilizing Pygame for Game Development: Pygame serves as a powerful library for creating 2D games in Python, offering functionalities essential for developing Tetris.
  • Mobile Optimization Techniques: Adapting the game for mobile involves adjusting screen dimensions, implementing touch controls, and ensuring responsive design.
  • Core Game Mechanics Implementation: Defining Tetrominoes, managing the game grid, handling user input, and implementing collision detection are crucial for a functional Tetris game.

Introduction

Tetris, a classic puzzle game, has remained popular across various platforms due to its simple yet engaging gameplay. Developing a mobile-friendly version of Tetris using Python involves leveraging libraries such as pygame and potentially integrating kivy for enhanced touch controls. This guide provides a step-by-step approach to creating a comprehensive Tetris game optimized for mobile devices.

Setting Up the Development Environment

Installing Required Libraries

Before diving into coding, ensure that you have the necessary libraries installed. The primary library used for game development in this guide is pygame. Additionally, for touch controls and mobile optimization, kivy can be integrated.

Use the following commands to install these libraries:

pip install pygame
pip install kivy

If you're targeting mobile devices, consider using platforms like Pydroid 3 for Android to run Python scripts seamlessly.

Project Structure

Organize your project directory as follows to maintain clarity and manageability:

Directory/File Description
tetris_mobile/ Root directory for the Tetris project.
└── main.py Main Python script containing the game logic.
└── assets/ Directory for storing images, sounds, and other assets.

Defining Tetrominoes

Tetromino Shapes and Colors

Tetrominoes are the fundamental building blocks of Tetris, each consisting of four blocks arranged in different shapes. Defining these shapes and their corresponding colors is essential for differentiating them during gameplay.

# Define colors
BLACK = (0, 0, 0)
CYAN = (0, 255, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 165, 0)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
PURPLE = (128, 0, 128)

# Tetromino shapes
SHAPES = [
    [[1, 5, 9, 13], [4, 5, 6, 7]],                      # I
    [[4, 5, 9, 10], [2, 6, 5, 9]],                     # Z
    [[6, 7, 9, 10], [1, 5, 6, 10]],                    # S
    [[1, 2, 5, 9], [0, 4, 5, 6], [1, 5, 9, 8], [4, 5, 6, 10]], # J
    [[1, 2, 6, 10], [5, 6, 7, 9], [2, 6, 10, 11], [3, 5, 6, 7]], # L
    [[1, 2, 5, 6]],                                      # O
    [[1, 4, 5, 6], [1, 4, 5, 9], [4, 5, 6, 9], [1, 5, 6, 9]]      # T
]
COLORS = [CYAN, YELLOW, ORANGE, BLUE, GREEN, RED, PURPLE]

Tetromino Class

Creating a Tetromino class encapsulates the properties and behaviors of each Tetromino, including position, shape, color, and rotation.

import random

class Tetromino:
    def __init__(self, x, y, shape):
        self.x = x
        self.y = y
        self.shape = shape
        self.color = random.choice(COLORS)
        self.rotation = 0

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.shape)

Creating the Game Grid

Grid Initialization

The game grid serves as the playfield where Tetrominoes fall and are placed. It is typically a matrix representing rows and columns.

def create_grid(locked_positions={}):
    grid = [[BLACK for _ in range(10)] for _ in range(20)]
    for (x, y), color in locked_positions.items():
        grid[y][x] = color
    return grid

Drawing the Grid

Rendering the grid involves drawing each block based on its color and position. Additionally, grid lines can be drawn for better visual separation.

import pygame

def draw_grid(surface, grid):
    for y in range(len(grid)):
        for x in range(len(grid[y])):
            pygame.draw.rect(surface, grid[y][x], (x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)
    for y in range(len(grid)):
        pygame.draw.line(surface, WHITE, (0, y * BLOCK_SIZE), (SCREEN_WIDTH, y * BLOCK_SIZE))
    for x in range(len(grid[0])):
        pygame.draw.line(surface, WHITE, (x * BLOCK_SIZE, 0), (x * BLOCK_SIZE, SCREEN_HEIGHT))

Handling User Input

Keyboard Controls

For desktop development, handling keyboard inputs such as arrow keys allows users to move and rotate Tetrominoes.

def handle_input(event, current_piece, grid):
    if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_LEFT:
            current_piece.x -= 1
            if not valid_space(current_piece, grid):
                current_piece.x += 1
        elif event.key == pygame.K_RIGHT:
            current_piece.x += 1
            if not valid_space(current_piece, grid):
                current_piece.x -= 1
        elif event.key == pygame.K_DOWN:
            current_piece.y += 1
            if not valid_space(current_piece, grid):
                current_piece.y -= 1
        elif event.key == pygame.K_UP:
            current_piece.rotate()
            if not valid_space(current_piece, grid):
                current_piece.rotate()

Touch Controls for Mobile

Adapting to mobile requires replacing keyboard inputs with touch gestures. Libraries like kivy facilitate implementing touch controls such as swipes and taps.

def handle_touch(pos, current_piece, grid):
    x, y = pos
    if touch_left.collidepoint(x, y):
        current_piece.x -= 1
        if not valid_space(current_piece, grid):
            current_piece.x += 1
    elif touch_right.collidepoint(x, y):
        current_piece.x += 1
        if not valid_space(current_piece, grid):
            current_piece.x -= 1
    elif touch_rotate.collidepoint(x, y):
        current_piece.rotate()
        if not valid_space(current_piece, grid):
            current_piece.rotate()

Implementing Game Logic

Collision Detection

Ensuring that Tetrominoes do not overlap or exceed the grid boundaries is critical. The valid_space function checks if a Tetromino's position is valid.

def valid_space(piece, grid):
    accepted_positions = [[(x, y) for x in range(10) if grid[y][x] == BLACK] for y in range(20)]
    accepted_positions = [x for sub in accepted_positions for x in sub]
    formatted = convert_shape_format(piece)
    
    for pos in formatted:
        if pos not in accepted_positions:
            if pos[1] > -1:
                return False
    return True

def convert_shape_format(piece):
    positions = []
    format = piece.shape[piece.rotation % len(piece.shape)]
    
    for i, line in enumerate(format):
        row = list(map(int, list(f"{line:04b}")))
        for j, column in enumerate(row):
            if column:
                positions.append((piece.x + j, piece.y + i))
    return positions

Row Clearing

Clearing complete rows and updating the grid accordingly enhances gameplay by providing points and freeing up space for new Tetrominoes.

def clear_rows(grid, locked_positions):
    cleared_rows = 0
    for y in range(len(grid)-1, -1, -1):
        if BLACK not in grid[y]:
            cleared_rows += 1
            del grid[y]
            grid.insert(0, [BLACK for _ in range(10)])
            keys_to_remove = [key for key in locked_positions if key[1] == y]
            for key in keys_to_remove:
                del locked_positions[key]
    return cleared_rows

Game Over Condition

The game concludes when new Tetrominoes cannot be placed on the grid, signaling a game-over scenario.

def check_game_over(locked_positions):
    for pos in locked_positions:
        _, y = pos
        if y < 1:
            return True
    return False

Rendering the Game

Drawing the Game Window

Rendering involves drawing the grid, active Tetromino, locked positions, and additional UI elements like the score.

def draw_window(surface, grid, score=0):
    surface.fill(BLACK)
    
    # Draw Score
    font = pygame.font.SysFont('comicsans', 30)
    label = font.render(f"Score: {score}", True, WHITE)
    surface.blit(label, (SCREEN_WIDTH - 150, 20))
    
    # Draw Grid
    draw_grid(surface, grid)
    
    pygame.display.update()

Rendering Current and Next Tetromino

Displaying the current falling Tetromino and the next one enhances user experience by providing foresight into upcoming pieces.

def draw_next_piece(surface, piece):
    font = pygame.font.SysFont('comicsans', 30)
    label = font.render("Next Piece:", True, WHITE)
    surface.blit(label, (SCREEN_WIDTH + 50, 50))
    
    format = piece.shape[piece.rotation % len(piece.shape)]
    
    for i, line in enumerate(format):
        row = list(map(int, list(f"{line:04b}")))
        for j, column in enumerate(row):
            if column:
                pygame.draw.rect(surface, piece.color, (SCREEN_WIDTH + 50 + j*BLOCK_SIZE, 100 + i*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)

Optimizing for Mobile

Adjusting Screen Dimensions

Mobile devices have varying screen sizes and resolutions. Adjusting the screen dimensions ensures that the game is displayed correctly across different devices.

# Mobile-optimized screen dimensions
SCREEN_WIDTH = 360
SCREEN_HEIGHT = 640
BLOCK_SIZE = SCREEN_WIDTH // 12

Implementing Touch Controls

Replacing keyboard inputs with touch gestures involves detecting touch regions for actions like moving left/right and rotating Tetrominoes. The kivy library facilitates handling multi-touch inputs.

# Define touch control zones
touch_left = pygame.Rect(0, SCREEN_HEIGHT*2//3, SCREEN_WIDTH//3, SCREEN_HEIGHT//3)
touch_right = pygame.Rect(SCREEN_WIDTH*2//3, SCREEN_HEIGHT*2//3, SCREEN_WIDTH//3, SCREEN_HEIGHT//3)
touch_rotate = pygame.Rect(SCREEN_WIDTH//3, SCREEN_HEIGHT*2//3, SCREEN_WIDTH//3, SCREEN_HEIGHT//3)

def handle_touch(pos, current_piece, grid):
    x, y = pos
    if touch_left.collidepoint(x, y):
        current_piece.x -= 1
        if not valid_space(current_piece, grid):
            current_piece.x += 1
    elif touch_right.collidepoint(x, y):
        current_piece.x += 1
        if not valid_space(current_piece, grid):
            current_piece.x -= 1
    elif touch_rotate.collidepoint(x, y):
        current_piece.rotate()
        if not valid_space(current_piece, grid):
            current_piece.rotate()

Responsive Design Considerations

Ensuring that the game's UI elements scale appropriately based on the device's screen size enhances user experience. Utilizing relative sizing and positioning helps maintain consistency across devices.

# Example: Dynamic block size based on screen width
BLOCK_SIZE = SCREEN_WIDTH // 12

# Positioning score and next piece dynamically
score_position = (SCREEN_WIDTH - 150, 20)
next_piece_position = (SCREEN_WIDTH + 50, 50)

Main Game Loop

Initializing the Game

The main game loop handles the game's runtime behavior, including updating the game state, handling user input, and rendering the visuals.

def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH + 200, SCREEN_HEIGHT))
    pygame.display.set_caption("Mobile Tetris")
    clock = pygame.time.Clock()
    
    grid = create_grid()
    current_piece = Tetromino(5, 0, random.choice(SHAPES))
    next_piece = Tetromino(5, 0, random.choice(SHAPES))
    locked_positions = {}
    score = 0
    fall_time = 0
    fall_speed = 0.5  # Seconds

    running = True
    game_over = False

    while running:
        grid = create_grid(locked_positions)
        fall_time += clock.get_rawtime()
        clock.tick()

        if fall_time / 1000 >= fall_speed:
            fall_time = 0
            current_piece.y += 1
            if not valid_space(current_piece, grid):
                current_piece.y -= 1
                change_piece = True

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.KEYDOWN:
                handle_input(event, current_piece, grid)
            if event.type == pygame.FINGERDOWN:
                touch_pos = (event.x * SCREEN_WIDTH, event.y * SCREEN_HEIGHT)
                handle_touch(touch_pos, current_piece, grid)

        shape_pos = convert_shape_format(current_piece)
        for pos in shape_pos:
            if pos[1] > -1:
                grid[pos[1]][pos[0]] = current_piece.color

        if change_piece:
            for pos in shape_pos:
                locked_positions[pos] = current_piece.color
            current_piece = next_piece
            next_piece = Tetromino(5, 0, random.choice(SHAPES))
            change_piece = False
            score += clear_rows(grid, locked_positions) * 100

            if check_game_over(locked_positions):
                game_over = True
                running = False

        draw_window(screen, grid, score)
        draw_next_piece(screen, next_piece)

        pygame.display.update()

    pygame.quit()

Conclusion

Developing a mobile-friendly Tetris game in Python involves a blend of game development principles and mobile optimization techniques. By leveraging libraries like pygame and kivy, developers can create engaging and responsive gaming experiences tailored for mobile devices. This guide provided a comprehensive overview, from setting up the environment to implementing core game mechanics and optimizing for mobile, ensuring a robust foundation for your Tetris project.

References


Last updated January 18, 2025
Ask Ithy AI
Download Article
Delete Article