Наталья Кайда 04 ноября 2024

🐍🕹️🐸 Python + Pygame = Amazing Frogs: создаем крутую головоломку

Готов создать игру, от которой не оторвешься? Давай вместе сделаем Amazing Frogs – убийцу Тетриса на Python! Тут тебе и падающие блоки, и взрывающиеся цвета, и хитрая механика. Запасайся кофе, открывай IDE, и поехали превращать код в игровое безумие!
🐍🕹️🐸 Python + Pygame = Amazing Frogs: создаем крутую головоломку

Логика игры

Amazing Frogs – клон популярной винтажной игры Amazing Blocks:

Amazing Blocks и Amazing Frogs
Amazing Blocks и Amazing Frogs

Amazing Blocks – любопытный кроссовер между легендарным прародителем всех блочных головоломок Тетрисом и жанром «Три в ряд»:

  • Фигуры состоят из трех элементов (тримино) вместо четырех (тетрамино).
  • Тримино не поворачиваются вокруг своей оси – вместо этого меняются местами цветные блоки, из которых они состоят. Бывают и одноцветные тримино.
При нажатии клавиши ↑ блоки меняются местами
При нажатии клавиши ↑ блоки меняются местами
  • Блоки могут отделяться от тримино, если один или два блока сталкиваются с препятствием. Оставшаяся часть фигуры продолжает движение.
  • Вместо сгорания заполненных линий сгорают оказавшиеся по соседству 3 и более блоков одного цвета. При этом соседство определяется по горизонтали, вертикали и обеим диагоналям (справа налево и слева направо).
Amazing Frogs повторяет всю базовую функциональность оригинала
Amazing Frogs повторяет всю базовую функциональность оригинала

Остальная логика Amazing Frogs стандартна:

  • При наборе каждых 20 очков происходит переход на следующий уровень, и скорость падения фигур увеличивается.
  • Игра заканчивается, когда очередной фигуре некуда падать.
  • Нажатиесбрасывает тримино вниз, пробел ставит игру на паузу, а Esc приводит к выходу.
🐍 Библиотека питониста
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»
🐍🎓 Библиотека Python для собеса
Подтянуть свои знания по Python вы можете на нашем телеграм-канале «Библиотека Python для собеса»
🐍🧩 Библиотека задач по Python
Интересные задачи по Python для практики можно найти на нашем телеграм-канале «Библиотека задач по Python»

Внешний вид и основные настройки игрового окна

Окно имеет фиксированный размер 450x600, а игровое поле представляет собой сетку 6x12 клеток. Блоки тримино состоят из двух квадратных элементов 50х50, слегка различающихся по яркости (для создания 2,5-эффекта), и полупрозрачного png-изображения, которое масштабируется до размера 20х20 с применением сглаживания:

        import pygame
import random

pygame.init()
WIDTH, HEIGHT = 450, 600 
CELL_SIZE = 50  
GRID_WIDTH = 6  
GRID_HEIGHT = 12  
COLORS = [(0, 0, 225), (0, 225, 0), (225, 0, 0), (225, 225, 0)]
LIGHT_COLORS = [(30, 30, 255), (50, 255, 50), (255, 30, 30), (255, 255, 30)]
png_image = pygame.image.load('frog.png')
png_image = pygame.transform.smoothscale(png_image, (20, 20))
png_image.set_alpha(128)
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Amazing Frogs")
    

Классы Block и Trimino

Эти классы – ключевые элементы игры: Block управляет поведением блоков, а Trimino определяет поведение состоящих из блоков фигур – тримино.

Block обеспечивает отрисовку блоков, движение (вправо/влево) и падение блоков (как уже упоминалось выше, блоки могут двигаться по отдельности после разделения тримино):

        class Block:
    def __init__(self, x, y, color):
        self.x = x
        self.y = y
        self.color = color
        self.stopped = False  
    
    def fall(self):
        if not self.stopped:
            self.y += 1
    
    def move_left(self, grid):
        if not self.stopped and self.x > 0 and not grid[self.y][self.x - 1]:
            self.x -= 1
    
    def move_right(self, grid):
        if not self.stopped and self.x < GRID_WIDTH - 1 and not grid[self.y][self.x + 1]:
            self.x += 1

    def draw(self, screen):
        color_index = COLORS.index(self.color)
        light_color = LIGHT_COLORS[color_index]      
        pygame.draw.rect(screen, self.color, (self.x * CELL_SIZE + 1, self.y * CELL_SIZE + 1, CELL_SIZE - 1, CELL_SIZE - 1), 0, 3)     
        pygame.draw.rect(screen, light_color, (self.x * CELL_SIZE + 3, self.y * CELL_SIZE + 3, CELL_SIZE - 6, CELL_SIZE - 6), 0, 3)       
        png_pos = (self.x * CELL_SIZE + (CELL_SIZE // 2 - 10), self.y * CELL_SIZE + (CELL_SIZE // 2 - 10))  
        screen.blit(png_image, png_pos)
    

Trimino обеспечивает формирование тримино из трех цветных блоков, управляет перемещением всех блоков сразу (как цельной фигуры), и изменяет окраску элементов, которая внешне выглядит, как перестановка блоков внутри фигуры:

        class Trimino:
    def __init__(self):
        self.blocks = [
            Block(GRID_WIDTH // 2 - 1, 0, random.choice(COLORS)),
            Block(GRID_WIDTH // 2, 0, random.choice(COLORS)),
            Block(GRID_WIDTH // 2 + 1, 0, random.choice(COLORS)),
        ]
    
    def rotate_colors(self):        
        colors = [block.color for block in self.blocks]
        colors = [colors[-1]] + colors[:-1]
        for i in range(3):
            self.blocks[i].color = colors[i]
    
    def fall(self):
        for block in self.blocks:
            block.fall()

    def move_left(self, grid):
        if all(block.x > 0 and not grid[block.y][block.x - 1] for block in self.blocks):
            for block in self.blocks:
                block.move_left(grid)

    def move_right(self, grid):
        if all(block.x < GRID_WIDTH - 1 and not grid[block.y][block.x + 1] for block in self.blocks):
            for block in self.blocks:
                block.move_right(grid)

    def drop_down(self, grid):        
        while all(not block.stopped and not self.check_collision(block, grid) for block in self.blocks):
            self.fall()

    def check_collision(self, block, grid):
        if block.y >= GRID_HEIGHT - 1:
            return True
        if grid[block.y + 1][block.x]:
            return True
        return False

    def draw(self, screen):
        for block in self.blocks:
            block.draw(screen)
    

Оба класса взаимодействуют с сеткой (grid), которая представляет собой двумерный массив, отслеживающий положение блоков на поле. Это позволяет реализовывать проверку столкновений (например, для остановки падения) и движение блоков влево или вправо.

Окно завершения игры

Если игровое поле заполняется настолько, что очередной фигуре некуда падать, игра заканчивается: на экране появляются две кнопки – «Новая игра» и «Выйти». За визуализацию кнопок и клики по ним отвечает класс Button:

        class Button:
    def __init__(self, x, y, width, height, text, color, text_color):
        self.rect = pygame.Rect(x, y, width, height)
        self.text = text
        self.color = color
        self.text_color = text_color
        self.font = pygame.font.Font('MOSCOW2024.otf', 16)

    def draw(self, screen):
        pygame.draw.rect(screen, self.color, self.rect)
        text_surface = self.font.render(self.text, True, self.text_color)
        text_rect = text_surface.get_rect(center=self.rect.center)
        screen.blit(text_surface, text_rect)

    def is_clicked(self, pos):
        return self.rect.collidepoint(pos)
    

Класс Game

Этот класс реализует всю основную логику игры, включая управление тримино, обновление состояния игры, проверку столкновений и отрисовку. Основные методы Game:

  • reset_game – сбрасывает игру до начального состояния. Создает новую сетку, генерирует новый текущий и следующий тримино, обнуляет статистику и настройки.
  • check_collision – проверяет, столкнулся ли блок с нижней границей сетки или с другим блоком. Если блок достиг низа или под ним есть другой блок, возвращает True.
  • lock_trimino – когда тримино перестает двигаться, его блоки фиксируются в сетке, то есть цвет блоков сохраняется в массиве grid. Затем проверяются заполненные линии (метод check_lines), и если в верхнем ряду уже есть блоки, игра заканчивается.
  • draw_next_trimino – отображает следующую фигуру в панели статистики справа.
  • find_matches – ищет совпадения цветов блоков в сетке по горизонтали, вертикали и диагоналям (если три и более блоков одного цвета стоят рядом).
  • remove_matches – удаляет найденные совпадения из сетки и сдвигает блоки выше вниз, заполняя пустые места, после чего обновляется статистика.
  • level_up – увеличивает уровень игры, если было удалено достаточно блоков, после чего повышается скорость падения блоков.
  • update – обновляет состояние игры, заставляя текущий тримино падать и проверяя столкновения. Если все блоки тримино остановились, они фиксируются на поле, после чего проверяются линии (check_lines) на наличие 3 и более блоков одного цвета, расположенных рядом.
  • draw – отрисовывает текущий тримино и все заблокированные блоки на сетке. Также отображает статистику (счет, уровень) и следующий тримино, а если игра окончена, выводит окно с кнопками «Новая игра» и «Выйти».
        class Game:
    def __init__(self):
        self.reset_game()
        self.font = pygame.font.Font('MOSCOW2024.otf', 15)  
        self.font_color = (255, 255, 255)
        self.restart_button = Button(WIDTH // 2 - 105, HEIGHT // 2 + 50, 130, 40, "Новая игра", (0, 255, 0), (0, 0, 0))
        self.exit_button = Button(WIDTH // 2 + 45, HEIGHT // 2 + 50, 80, 40, "Выйти", (255, 0, 0), (255, 255, 255))

    def reset_game(self):
        self.grid = [[None for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
        self.current_trimino = Trimino()
        self.next_trimino = Trimino()  
        self.game_over = False
        self.fall_time = 0
        self.fall_speed = 500
        self.score = 0
        self.level = 1
        self.blocks_burned = 0

    def check_collision(self, block):
        if block.y >= GRID_HEIGHT - 1:
            return True
        if self.grid[block.y + 1][block.x]:
            return True
        return False

    def lock_trimino(self):
        for block in self.current_trimino.blocks:
            self.grid[block.y][block.x] = block.color
        self.check_lines()

        if any(self.grid[0][x] is not None for x in range(GRID_WIDTH)):
            self.game_over = True
        else:
            self.current_trimino = self.next_trimino
            self.next_trimino = Trimino()  

    def draw_next_trimino(self, screen):
        next_area_x = WIDTH - 140
        next_area_y = 280

        for i, block in enumerate(self.next_trimino.blocks):
            original_x, original_y = block.x, block.y
            block.x = (next_area_x // CELL_SIZE) + i 
            block.y = next_area_y // CELL_SIZE + 1    
            block.draw(screen) 
            block.x, block.y = original_x, original_y

    def find_matches(self):
        to_remove = set()

        for y in range(GRID_HEIGHT):
            for x in range(GRID_WIDTH - 2):
                if self.grid[y][x] and self.grid[y][x] == self.grid[y][x + 1] == self.grid[y][x + 2]:
                    to_remove.update([(y, x), (y, x + 1), (y, x + 2)])

        for x in range(GRID_WIDTH):
            for y in range(GRID_HEIGHT - 2):
                if self.grid[y][x] and self.grid[y][x] == self.grid[y + 1][x] == self.grid[y + 2][x]:
                    to_remove.update([(y, x), (y + 1, x), (y + 2, x)])

        for y in range(GRID_HEIGHT - 2):
            for x in range(GRID_WIDTH - 2):
                if self.grid[y][x] and self.grid[y][x] == self.grid[y + 1][x + 1] == self.grid[y + 2][x + 2]:
                    to_remove.update([(y, x), (y + 1, x + 1), (y + 2, x + 2)])
                if self.grid[y][x + 2] and self.grid[y][x + 2] == self.grid[y + 1][x + 1] == self.grid[y + 2][x]:
                    to_remove.update([(y, x + 2), (y + 1, x + 1), (y + 2, x)])

        return to_remove

    def remove_matches(self, to_remove):
        for x in range(GRID_WIDTH):
            col_blocks = [(y, x) for y, cx in to_remove if cx == x]
            col_blocks.sort(reverse=True)  

            for y, _ in col_blocks:
                for row in range(y, 0, -1):
                    self.grid[row][x] = self.grid[row - 1][x]
                self.grid[0][x] = None  

        burned_blocks = len(to_remove)
        self.blocks_burned += burned_blocks
        self.score += burned_blocks
        if self.blocks_burned >= 20:
            self.level_up()

    def level_up(self):
        self.level += 1
        self.blocks_burned = 0
        self.fall_speed = max(100, self.fall_speed - 50)  

    def check_lines(self):
        to_remove = self.find_matches()
        while to_remove:
            self.remove_matches(to_remove)
            to_remove = self.find_matches()  

    def update(self):
        if self.game_over:
            return  
        
        current_time = pygame.time.get_ticks()
        if current_time - self.fall_time > self.fall_speed:
            self.fall_time = current_time
            for block in self.current_trimino.blocks:
                if not block.stopped and self.check_collision(block):
                    block.stopped = True

            if all(block.stopped for block in self.current_trimino.blocks):
                self.lock_trimino()  
                self.check_lines()  
            else:
                self.current_trimino.fall()

    def draw(self, screen):
        if not self.game_over:
            self.current_trimino.draw(screen)

            for y in range(GRID_HEIGHT):
                for x in range(GRID_WIDTH):
                    if self.grid[y][x]:
                        block = Block(x, y, self.grid[y][x])
                        block.draw(screen)

            stats_background_color = (50, 50, 50)  
            stats_rect = pygame.Rect(WIDTH - 150, 0, 150, HEIGHT)  
            pygame.draw.rect(screen, stats_background_color, stats_rect)  
            score_text = self.font.render(f"Счет: {self.score}", True, self.font_color)
            level_text = self.font.render(f"Уровень: {self.level}", True, self.font_color)
            next_text = self.font.render("Следующая:", True, self.font_color)
            pause_text = self.font.render("Пауза: пробел", True, self.font_color)
            esc_text = self.font.render("Выход: Esc", True, self.font_color)
            screen.blit(score_text, (WIDTH - 140, 20))
            screen.blit(level_text, (WIDTH - 140, 60))
            screen.blit(next_text, (WIDTH - 140, 250))
            screen.blit(pause_text, (WIDTH - 140, 400))
            screen.blit(esc_text, (WIDTH - 140, 450))
            self.draw_next_trimino(screen)
        else:
            screen.fill((0, 0, 0))
            text = self.font.render("Игра закончена", True, (255, 0, 0))
            text_rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2))
            screen.blit(text, text_rect)
            self.restart_button.draw(screen)
            self.exit_button.draw(screen)     

    

Основной игровой цикл и обработка событий

Заключительный блок кода отвечает за основной игровой цикл, в котором происходят обработка событий, обновление состояния игры, отрисовка объектов на экране и управление паузами:

  • clock = pygame.time.Clock() – используется для контроля частоты обновления игры (FPS).
  • running = True – переменная-флаг для основного цикла игры, пока она True, цикл продолжается. Внутри этого цикла происходит все взаимодействие с игроком и обновление игры.
  • paused = False – переменная для отслеживания, поставлена ли игра на паузу.
  • Обработка событий for event in pygame.event.get(). Здесь происходит проверка на различные события (нажатие клавиш или закрытие окна).
        game = Game()
clock = pygame.time.Clock()
running = True
paused = False

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            elif event.key == pygame.K_SPACE and not game.game_over:
                paused = not paused
            elif not paused and not game.game_over:
                if event.key == pygame.K_LEFT:
                    game.current_trimino.move_left(game.grid)
                elif event.key == pygame.K_RIGHT:
                    game.current_trimino.move_right(game.grid)
                elif event.key == pygame.K_UP:
                    game.current_trimino.rotate_colors()
                elif event.key == pygame.K_DOWN:
                    game.current_trimino.drop_down(game.grid)
        elif event.type == pygame.MOUSEBUTTONDOWN and game.game_over:
            if game.restart_button.is_clicked(event.pos):
                game.reset_game()
            elif game.exit_button.is_clicked(event.pos):
                running = False

    screen.fill((0, 0, 0))
    if not paused and not game.game_over:
        game.update()
    game.draw(screen)
    if paused:
        font = pygame.font.Font('MOSCOW2024.otf', 20)
        text = font.render("Нажмите пробел для продолжения", True, (255, 255, 255))
        text_rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2))
        screen.blit(text, text_rect)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

    

Полный исходный код игры находится в репозитории Amazing Frogs.

В заключение

Pygame отлично подходит для разработки простых логических игр: обработку многих событий библиотека берет на себя. Однако если вы планируете создание сложной игры с более продвинутым интерфейсом, есть смысл использовать библиотеку Arcade или фреймворк Flet, основанный на Flutter.

***

Python: от новичка до junior-разработчика

Хочешь освоить Python и создать свой первый проект? Наш курс предлагает пошаговое обучение от основ до создания ботов и парсеров, с индивидуальной обратной связью от экспертов.

  • 90+ часов обучения
  • 4 проекта для портфолио
  • Работа с PyCharm и Jupyter Notebook
  • Основы ООП и алгоритмов
  • Создание ботов для Telegram и Instagram
  • Парсинг веб-страниц

Курс подходит как для начинающих, так и для тех, кто хочет углубить свои знания. Учитесь в своем темпе с поддержкой опытных преподавателей.

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Go-разработчик
по итогам собеседования
Java Team Lead
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ