🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

Изучаем основные возможности Pygame в процессе создания lite-версии одной из самых популярных игр в мире.

Pygame – самое популярное решение для создания 2D игр на Python: библиотека включает в себя удобные инструменты для рисования, работы с изображениями, видео, спрайтами, шрифтами и звуком, для обработки событий клавиатуры и мыши. Главные преимущества Pygame – легкость обучения и скорость разработки. И хотя Pygame не используется для коммерческой разработки игр, это идеальный вариант для обучения начинающих. Здесь мы рассмотрим создание клона Тетриса. Полный код игры находится здесь.

Установка Pygame

Pygame не входит в стандартную поставку Python. Для установки достаточно выполнить в cmd команду py -m pip install -U pygame --user. Полный размер пакета – чуть более 8 Мб.

Обзор проекта

Основной экран Тетриса

Игровое поле представляет собой прямоугольный «стакан», в который сверху падают фигуры – стилизованные буквы L, S, Z, J, O, I и T.

Буквы-фигуры в Тетрисе

Каждая буква состоит из 4 блоков:

Фигуры и варианты поворотов описаны в 2D-списках 5 х 5

Игрок управляет движением фигур вниз – двигает их вправо и влево (но не вверх), поворачивает на 90 градусов, при желании ускоряет падение нажатием/удержанием клавиши или мгновенно сбрасывает фигуры на дно нажатием Enter.

Приземлением считается момент, когда фигура падает на дно стакана или на элемент предыдущих фигур. После этого программа проверяет, вызвало ли приземление полное (без пустот) заполнение ряда элементов. Заполненные ряды (их может быть от 1 до 4 включительно) удаляются; находящиеся над ними элементы перемещаются вниз на столько рядов, сколько было заполнено и удалено; вверху стакана добавляется соответствующее количество пустых рядов. После удаления 10 заполненных рядов происходит переход на следующий уровень, и падение фигур ускоряется.

Все экраны игры
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

Основные параметры игры

Прежде всего импортируем нужные модули:

import pygame as pg
import random, time, sys
from pygame.locals import *

Затем определяем основные константы – кадровую частоту fps, высоту и ширину окна программы, размер базового элемента фигур-букв block (20 х 20 пикселей), параметры стакана, символ для обозначения пустых ячеек на игровом поле:

fps = 25
window_w, window_h = 600, 500
block, cup_h, cup_w = 20, 20, 10

К размеру базового элемента block привязываются остальные параметры игрового поля: ширина и высота стакана, к примеру, равны 10 и 20 блоков соответственно; каждый раз, когда игрок нажимает клавишу или , фигура перемещается на 1 блок в нужную сторону.

Параметры side_freq и down_freq задают время, которое затрачивается на перемещение фигуры в сторону или вниз, если игрок удерживает клавишу нажатой:

side_freq, down_freq = 0.15, 0.1

Для размещения стакана и информационных надписей, а также для конвертации координат нам также понадобятся константы side_margin и top_margin – первая задает дистанцию между правой и левой сторонами окна программы и стаканом; вторая определяет расстояние между верхней границей стакана и окном:

side_margin = int((window_w - cup_w * block) / 2)
top_margin = window_h - (cup_h * block) - 5

Шаблоны и цвет фигур

Поскольку каждую фигуру-букву можно поворачивать на 90 градусов, все возможные варианты поворотов описаны в словаре figures с помощью вложенных списков, элементы которых состоят из строк: символом x отмечены занятые ячейки, o – пустые. Количество вращений зависит от формы буквы: у O, к примеру, будет всего один вариант:

'O': [['ooooo',
       'ooooo',
       'oxxoo',
       'oxxoo',
       'ooooo']]

Поскольку каждая фигура состоит из 4 блоков, размер шаблона должен быть 5 х 5: fig_w, fig_h = 5, 5.

Цвета фигур задаются двумя кортежами: colors и lightcolors. Последний включает чуть более светлые оттенки тех же цветов, что и colors – для создания псевдо 2.5 D эффекта.

FPS и производительность

Pygame немилосердно нагружает процессор: можно столкнуться с ситуацией, когда небольшая игра с простейшей графикой использует CPU на 100% и нагревает достаточно мощный компьютер гораздо сильнее, чем 3D-шутер, написанный не на Python:). Проблема решается созданием объекта pygame.time.Clock(), который вызывается в основном цикле программы с нужной fps – кадровой частотой.

Шрифты

Модуль Pygame поставляется с одним шрифтом – freesansbold.ttf. При этом Pygame способен использовать любые другие шрифты – как установленные в системе, так и используемые только в рамках конкретного проекта. Чтобы получить список всех шрифтов, установленных в системе, достаточно выполнить pygame.font.get_fonts().

Подключить шрифт можно тремя способами:

Если шрифт установлен и находится в папке Windows\Fonts\, как, например, стандартный Arial – нужно воспользоваться методом pygame.font.SysFont: pygame.font.SysFont('arial', 15).

Если шрифт используется только в проекте – укажите к нему путь в pygame.font.Font('/User/Tetris/game.ttf', 18).

Чтобы не указывать путь, можно поместить шрифт в одну папку с проектом: pygame.font.Font('game.ttf', 18)

Пауза, экран паузы и прозрачность

Пауза в нашей игре возникает при нажатии пробела event.key == K_SPACE. Чтобы показать «неактивность» программы во время паузы, нужно залить игровое поле цветом.

Во время паузы экран заливается полупрозрачным синим цветом

Заливку сплошным цветом реализовать очень просто, но полупрозрачную заставку сделать сложнее – как ни странно, метод draw в Pygame до сих пор не поддерживает эту опцию. Есть несколько способов решения этой проблемы. Мы воспользуемся методом, который предусматривает создание дополнительной поверхности с попиксельным альфа-смешением, и последующую заливку экрана паузы цветом с наложением на поверхность окна игры:

pause = pg.Surface((600, 500), pg.SRCALPHA)  
pause.fill((0, 0, 255, 127)) 
display_surf.blit(pause, (0, 0))

Экран паузы также активируется в случае проигрыша, вместе с сообщением Игра закончена.

Функция main()

Эта функция отвечает за создание нескольких дополнительных глобальных констант, инициализирует модуль Pygame, рисует стартовое окно игры, вызывает запуск Тетриса runTetris() и в случае необходимости отображает сообщение о проигрыше:

def main():
	global fps_clock, display_surf, basic_font, big_font
	pg.init()
	fps_clock = pg.time.Clock()
	display_surf = pg.display.set_mode((window_w, window_h))
	basic_font = pg.font.Font('freesansbold.ttf', 18)
	big_font = pg.font.Font('freesansbold.ttf', 45)
	pg.display.set_caption('Тетрис Lite')
	showText('Тетрис Lite')
	while True: # начинаем игру
    	runTetris()
    	pauseScreen()
    	showText('Игра закончена')

Основной код Тетриса

Код игры располагается в функции runTetris():

def runTetris():
	cup = emptycup()
	last_move_down = time.time()
	last_side_move = time.time()
	last_fall = time.time()
	going_down = False
	going_left = False
	going_right = False
	points = 0
	level, fall_speed = calcSpeed(points)
	fallingFig = getNewFig()
	nextFig = getNewFig()

При запуске вызывается функция рисования пустого стакана emptycup(), а возможности движения влево, вправо и вниз устанавливаются на False:

	going_down = False
	going_left = False
	going_right = False

Эти значения будут изменяться на True во время обработки событий клавиатуры, если будет установлено, что движение в нужном направлении возможно:

for event in pg.event.get():
    if event.type == KEYUP:

Главный цикл игры

Основной цикл обрабатывает все основные события, связанные с генерацией фигур, движением вниз и показом следующей фигуры:

while True:
    	if fallingFig == None:
        	fallingFig = nextFig
        	nextFig = getNewFig()
        	last_fall = time.time()
        	if not checkPos(cup, fallingFig):
            	return
    	quitGame()

После приземления каждой фигуры значение fallingFig устанавливается на None, после чего «следующая фигура» nextFig, уже показанная в превью, становится «падающей» fallingFig. Следующая фигура для превью генерируется функцией getNewFig(). Каждая новая падающая фигура генерируется в позиции, которая расположена чуть выше стакана. Функция checkPos() вернет False, если стакан уже заполнен настолько, что движение вниз невозможно, после чего появится сообщение Игра закончена. Эта же функция checkPos() проверяет, находится ли фигура в границах стакана и не натыкается ли на элементы других фигур.

Управление движением

Обработка всех событий происходит в уже упомянутом цикле:

for event in pg.event.get():
    if event.type == KEYUP:

Цикл обрабатывает паузу и определяет момент, когда пользователь нажимает и отпускает клавиши со стрелками. Если клавиши , и не нажаты, значения соответствующих переменных меняются на False:

            	elif event.key == K_LEFT:
                	going_left = False
            	elif event.key == K_RIGHT:
                	going_right = False
            	elif event.key == K_DOWN:
                	going_down = False

Управление движением фигур происходит в ветке elif event.type == KEYDOWN: если нажата клавиша со стрелкой и функция checkPos() возвращает True, положение фигуры изменяется на один блок в соответствующем направлении:

if event.key == K_LEFT and checkPos(cup, fallingFig, adjX=-1):
    fallingFig['x'] -= 1
    going_left = True
    going_right = False
    last_side_move = time.time()

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

При нажатии происходит вращение фигуры – варианты берутся из словаря figures. Чтобы не получить ошибку IndexError: list index out of range, мы используем конструкцию, которая обнуляет индекс элемента, когда инкремент достигает максимального значения: fallingFig['rotation'] + 1) % len(figures[fallingFig['shape']]. Если функция checkPos() сообщает, что очередное вращение невозможно из-за того, что фигура натыкается на какой-то блок, нужно вернуться к предыдущему варианту из списка:

if not checkPos(cup, fallingFig):
    fallingFig['rotation'] = (fallingFig['rotation'] - 1) % len(figures[fallingFig['shape']])

Для ускорения падения игрок нажимает и удерживает клавишу :

            	elif event.key == K_DOWN:
                	going_down = True
                	if checkPos(cup, fallingFig, adjY=1):
                    	    fallingFig['y'] += 1
  	                last_move_down = time.time()

Если пользователь хочет мгновенно сбросить фигуру на дно, он может нажать Enter. Цикл for здесь определяет максимально низкую свободную позицию в стакане:

            	elif event.key == K_RETURN:
                	going_down = False
                	going_left = False
                	going_right = False
                	for i in range(1, cup_h):
                    	    if not checkPos(cup, fallingFig, adjY=i):
                      	        break
                	fallingFig['y'] += i - 1

Удержание клавиш

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

if (going_left or going_right) and time.time() - last_side_move > side_freq:

и

if going_down and time.time() - last_move_down > down_freq and checkPos(cup, fallingFig, adjY=1):

В этих условиях программа проверяет, нажимает ли пользователь клавишу дольше, чем 0.15 или 0.1 секунды – в этом случае условие соответствует True, и фигура продолжит движение в заданном направлении. Эти условия избавляют игрока от необходимости многократно нажимать клавиши передвижения – для продолжения движения достаточно их удерживать.

Свободное падение

Если пользователь никак не вмешивается в управление фигурой, движение вниз происходит так:

    	if time.time() - last_fall > fall_speed: # свободное падение фигуры           
        	if not checkPos(cup, fallingFig, adjY=1): # проверка "приземления" фигуры
            	    addToCup(cup, fallingFig) # фигура приземлилась, добавляем ее в содержимое стакана
            	    points += clearCompleted(cup)
            	    level, fall_speed = calcSpeed(points)
            	    fallingFig = None
        	else: # фигура пока не приземлилась, продолжаем движение вниз
                    fallingFig['y'] += 1
                    last_fall = time.time()

Отрисовка, обновление окна игры и вывод надписей

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

    	display_surf.fill(bg_color)
    	drawTitle()
    	gamecup(cup)
    	drawInfo(points, level)
    	drawnextFig(nextFig)
    	if fallingFig != None:
        	drawFig(fallingFig)
    	pg.display.update()
    	fps_clock.tick(fps)

Вспомогательные функции

Функция txtObjects() принимает текст, шрифт и цвет, и с помощью метода render() возвращает готовые объекты Surface (поверхность) и Rect (прямоугольник). Эти объекты в дальнейшем обрабатываются методом blit в функции showText(), выводящей информационные надписи и название игры.

Выход из игры обеспечивает функция stopGame(), в которой используется sys.exit() из импортированного в начале кода модуля sys.

За добавление фигур к содержимому стакана отвечает addToCup():

def addToCup(cup, fig):
	for x in range(fig_w):
            for y in range(fig_h):
        	if figures[fig['shape']][fig['rotation']][y][x] != empty:
            	    cup[x + fig['x']][y + fig['y']] = fig['color']

Пока фигура двигается, ее блоки не принадлежат к содержимому стакана – добавление происходит после приземления. Этот процесс мы рассмотрим чуть ниже.

Генерация и заполнение стакана

Пустой стакан создается функцией emptycup():

def emptycup():
    cup = []
    for i in range(cup_w):
        cup.append([empty] * cup_h)
    return cup

Пустой стакан представляет собой двумерный список, заполненный символами o. Занятые ячейки в дальнейшем принимают значения 0, 1, 2, 3 – в соответствии с индексами цветов фигур в кортеже colors. Так выглядит массив cup после приземления нескольких фигур:

['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 2, 2, 1, 1]
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 3, 2, 2, 1, 1]
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 2, 2, 'o', 'o', 'o', 1]
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 3, 2, 2, 0, 2, 1]
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 3, 0, 2, 0, 0, 2, 'o']
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 0, 0, 0, 0, 0, 0, 1, 'o']
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 0, 0, 2, 1, 0, 1, 1]
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 2, 2, 1, 1, 1, 'o']
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 1, 0, 0, 0, 0, 0]
['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 1, 1, 1, 2, 2, 2, 0, 0]
Допустимое и недопустимое положение фигуры в стакане

Функция checkPos() следит за тем, чтобы падающая фигура оставалась в пределах игрового поля и не накладывалась на предыдущие. На примере слева фигура остается в допустимой области, на примере справа – ошибочно накладывается на предыдущую. Чтобы определить положение фигуры в стакане, нужно суммировать собственные координаты фигуры со «стаканными»:

Собственные координаты – (2, 1), (3, 1), (2, 2), (2, 3).

Стаканные координаты фигуры – (2, 3) на примере слева и (1, 11) на примере справа. Суммирование дает следующие результаты:

(2+2, 1+3), (3+2, 1+3), (2+2, 2+3), (2+2, 3+3) = (4, 4), (5, 4), (4, 5), (4, 6). Значит, фигура находится в пределах стакана и не наталкивается ни на один элемент предыдущих фигур.

На примере слева ситуация обратная:

(2+1, 2+11), (3+1, 2+11), (2+1, 3+11), (2+1, 4+11) = (3, 13), (4, 13), (3, 14), (3, 15) – две последние координаты в массиве cup уже заняты блоками предыдущих фигур. Именно такие ситуации и предотвращают checkPos() вместе с incup():

        	if not incup(x + fig['x'] + adjX, y + fig['y'] + adjY):
                    return False
        	if cup[x + fig['x'] + adjX][y + fig['y'] + adjY] != empty:
            	    return False

Удаление заполненных рядов и сдвиг блоков вниз

За обнаружение и удаление заполненных рядов отвечает функция clearCompleted() вместе со вспомогательной isCompleted(). Если isCompleted() возвращает True, программе нужно последовательно переместить вниз все ряды, располагающиеся над удаляемым, после чего заполнить нулевой ряд empty-значениями о:

def clearCompleted(cup):
    # Удаление заполенных рядов и сдвиг верхних рядов вниз
    removed_lines = 0
    y = cup_h - 1 
    while y >= 0:
        if isCompleted(cup, y):
           for pushDownY in range(y, 0, -1):
                for x in range(cup_w):
                    cup[x][pushDownY] = cup[x][pushDownY-1]
           for x in range(cup_w):
                cup[x][0] = empty
           removed_lines += 1
        else:
            y -= 1 
    return removed_lines
Переменная указывает на удаленный ряд

Переменная y после удаления одного ряда продолжает указывать на его номер – это нужно для того, чтобы перейти к удалению других заполненных рядов, если они смещаются на место только что удаленного. В случае если других заполненных рядов пока нет, происходит уменьшение у.

Рисование блоков фигур

Каждая фигура состоит из 4 элементов – блоков. Блоки рисует функция drawBlock(), которая получает координаты из convertCoords():

def drawBlock(block_x, block_y, color, pixelx=None, pixely=None):
    #отрисовка квадратных блоков, из которых состоят фигуры
    if color == empty:
        return
    if pixelx == None and pixely == None:
        pixelx, pixely = convertCoords(block_x, block_y)
    pg.draw.rect(display_surf, colors[color], (pixelx + 1, pixely + 1, block - 1, block - 1), 0, 3)
    pg.draw.rect(display_surf, lightcolors[color], (pixelx + 1, pixely + 1, block - 4, block - 4), 0, 3)
    pg.draw.circle(display_surf, colors[color], (pixelx + block / 2, pixely + block / 2), 5)
    

Для рисования блоков используются примитивы rect (прямоугольник) и circle (круг). При желании верхний квадрат можно конвертировать в поверхность (Surface), после чего наложить на эту поверхность изображение или текстовый символ. Функция drawBlock() также используется в drawnextFig() для вывода следующей фигуры справа от игрового поля.

Заключение

Напоминаем, что полный код игры можно скачать здесь. Это полностью функциональный Тетрис с простым интерфейсом. Pygame предоставляет немало дополнительных возможностей для дополнения программы: к примеру, в игру можно добавить звуковые эффекты, диалоговое окно для закрытия, фоновое изображение, запись рекордов в файл. Если какие-то моменты остались неясными – задавайте вопросы в комментариях.

Материалы по теме


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

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
28 июня 2018

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...