Share This
Связаться со мной
Крути в низ
Categories
//🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

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

pishem tetris na python s pomoshhju biblioteki pygame 95c8bce - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

Установка Pygame

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

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

pishem tetris na python s pomoshhju biblioteki pygame b3bc2b2 - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

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

pishem tetris na python s pomoshhju biblioteki pygame 087d4c0 - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

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

pishem tetris na python s pomoshhju biblioteki pygame 8e0a25c - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

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

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

pishem tetris na python s pomoshhju biblioteki pygame 56430bf - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

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

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

         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().

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

Если шрифт установлен и находится в папке WindowsFonts, как, например, стандартный 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. Чтобы показать «неактивность» программы во время паузы, нужно залить игровое поле цветом.

pishem tetris na python s pomoshhju biblioteki pygame d2c7e6c - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

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

Заливку сплошным цветом реализовать очень просто, но полупрозрачную заставку сделать сложнее – как ни странно, метод 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]      

pishem tetris na python s pomoshhju biblioteki pygame e857c28 - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

Допустимое и недопустимое положение фигуры в стакане

Функция 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     

pishem tetris na python s pomoshhju biblioteki pygame 764e525 - 🐍 Пишем Тетрис на Python с помощью библиотеки Pygame

Переменная указывает на удаленный ряд

Переменная 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 предоставляет немало дополнительных возможностей для дополнения программы: к примеру, в игру можно добавить звуковые эффекты, диалоговое окно для закрытия, фоновое изображение, запись рекордов в файл. Если какие-то моменты остались неясными – задавайте вопросы в комментариях.

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

  • 🕵 Пишем кейлоггер на Python для Windows за 5 минут
  • 🐍 Создание интерактивных панелей с Streamlit и Python
  • 🐍 Как сделать сайт на Python за 5 минут с помощью SSG-генератора Pelican

  • 0 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Свежие комментарии

    Рубрики

    About Author 01.

    blank
    Roman Spiridonov

    Моя специальность - Back-end Developer, Software Engineer Python. Мне 39 лет, я работаю в области информационных технологий более 5 лет. Опыт программирования на Python более 3 лет. На Django более 2 лет.

    Categories 05.

    © Speccy 2022 / All rights reserved

    Связаться со мной
    Close