Делаем тетрис на Python
easy

Делаем тетрис на Python

Можно играть, но пока без музыки

Однажды мы сделали тетрис на JavaScript — сначала написали основу, а потом добавили ускорение, подсчёт очков и всё остальное, что делает тетрис полноценной игрой. Сегодня мы повторим это на Python, причём местами логика будет отличаться от предыдущего проекта из-за разных возможностей языков программирования. 

Если вы ещё не умеете программировать на Python, но хотите разобраться, как тут всё устроено, вот с чего можно начать:

Старт в Python для новичков

Как установить Python на компьютер и начать на нём писать

Логика проекта

Всё будет как в обычном тетрисе: сверху падают случайные фигуры тетрамино — разные комбинации из 4 квадратов, а пока падают, их можно поворачивать и двигать влево и вправо. Когда фигура достигает нижней границы экрана, она останавливается. Когда набирается заполненный горизонтальный ряд из квадратов, он пропадает, а всё, что было выше, опускается на одну клетку вниз.

Управлять игрой можно будет стрелками на клавиатуре: перемещать тетрамино клавишами ← и →, поворачивать их клавишей ⭡ и ускорять движение, нажав ⭣.

Что используем

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

  • библиотека pygame,
  • модуль random,
  • модуль copy,
  • списки,
  • циклы,
  • проверка условий.

Библиотека pygame — это библиотека для создания приложений с графическим интерфейсом, не только игр. С pygame мы сделаем главное окно игры, управление клавишами и квадраты, из которых будут состоять тетрамино.

Встроенный модуль random позволит нам брать случайную фигуру из списка, в котором будут храниться фигуры. 

Встроенный модуль copy понадобится для глубокого копирования — специального механизма, который создаёт независимый дубликат исходного объекта. Если так не делать, то изменения в одной фигуре повлияют на другие такие же фигуры. В тетрисе всего 7 вариантов фигур, поэтому мы будем часто их копировать и изменять каждую копию: координаты, цвет. 

Списки. Список — это упорядоченный набор элементов, у каждого из которых есть свой номер — индекс. В списках можно хранить строки, числа, переменные и другие объекты Python. Можно хранить другие списки (такие списки будут называться вложенными). А ещё элементы списков можно изменять — это свойство нам тоже понадобится.

Циклы повторяют определённый код нужное количество раз. Можно задать это количество вручную, а можно запустить цикл по каждому из составного объекта. Например, можно пройтись по каждому элементу в списке и заменить его другим.

Проверка условий if, else и elif. Все действия будут выполняться после проверки условий. Например, если нажата какая-то клавиша, будет выполняться определённое действие. Фигура достигла нижней границы экрана — остановили её и запустили новую.

Импортируем библиотеку и модули

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

pip install pygame

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

Если пишете код в среде разработчика IDE, то терминал нужно запустить внутри неё:

Теперь библиотеку нужно подключить в коде. Здесь же подключаем модули random и copy:

# импортируем библиотеку pygame
import pygame
# импортируем библиотеку random для генерации случайных чисел
import random
# импортируем библиотеку copy для глубокого копирования
import copy

Создаём экран и задаём основные настройки

Некоторые части библиотеки pygame по умолчанию считаются необязательными. Их нужно принудительно включить, чтобы всё работало. Для этого пишем:

# инициализация всех модулей pygame
pygame.init()

Теперь нужно создать сетку главного экрана. Сетка задаётся количеством точек по ширине и высоте — каждая из этих точек будет верхним левым углом квадратной ячейки. Для классического тетриса используется поле 10 × 20.

Количество столбцов и рядов в коде будет использоваться несколькими способами. Чтобы упростить логику, мы пойдём на хитрость: сделаем 11 рядов и 21 строку, но на экране покажем только 10 и 20:

# количество столбцов сетки
columns = 11
# количество строк сетки
strings = 21

Зададим высоту и ширину экрана игры в пикселях. Высота в два раза больше ширины:

# ширина окна игры
screen_x = 250
# высота окна игры
screen_y = 500

Теперь можно создавать экран игры и придумать ему название. Сразу добавим параметр для работы со временем: нам понадобится разная скорость движения, а для её подсчёта нужно время. Чуть позже нужно будет задать частоту обновления кадров — она тоже связана со временем.

Добавляем такой код:

# создаём окно игры
screen = pygame.display.set_mode((screen_x, screen_y))
# устанавливаем заголовок окна
pygame.display.set_caption("Tetris CODE")
# создаём объект для отслеживания времени
clock = pygame.time.Clock()

Мы создали каркас сетки, но пока она не готова. Её нужно заполнить ячейками-квадратами, поэтому нам понадобится их ширина и высота:

# ширина одной ячейки сетки
cell_x = screen_x / (columns - 1)
# высота одной ячейки сетки
cell_y = screen_y / (strings - 1)

Теперь разберёмся с последним в списке основных настроек — частотой обновления кадров. По умолчанию pygame использует ресурсы компьютера на полную и включает максимально возможную частоту кадров. Но нам такое не нужно — сейчас в играх стандартом считается 60 кадров в секунду, мы тоже возьмём такую частоту.

Пока создадим только переменную с этим значением, использовать будем дальше:

# частота кадров в секунду
fps = 60

Теперь возьмёмся за сетку.

Создаём сетку игрового поля

Сначала создаём пустой список:

# создаём пустой список для хранения сетки
grid = []

Теперь создадим первый цикл. Он будет вложенным: сначала мы добавим столько пустых списков в уже существующий список grid, сколько у нас столбцов. Потом для каждого списка-столбца мы добавим столько ячеек, сколько есть строк. Получится список из 10 списков, и в каждом будет 20 значений.

Чтобы добавить ячейку, мы создадим для каждой объект из числа 1 — это будет наша отметка о том, что ячейка игрового поля не занята фигурой.

Добавляем такой код:

# проходим по каждому столбцу
for i in range(columns):
   # добавляем пустой список для каждого столбца
   grid.append([])
   # создаём количество ячеек, равное строкам,
   # и заполняем их значениями 1 (ячейка свободна)
   for j in range(strings):
       grid[i].append([1])

Теперь мы пройдём по этому списку из свободных ячеек и добавим в каждую ещё два параметра библиотеки pygame: прямоугольник и цвет.

Чтобы создать прямоугольную область, понадобится объект класса Rect. Это не инструмент рисования, а способ работы с прямоугольными областями. Для его создания нужно задать 4 параметра: 

  1. Начальная координата по горизонтали.
  2. Начальная координата по вертикали.
  3. Ширина.
  4. Высота.

Напомним, что у нас есть размеры поля в пикселях. В пикселях и надо указывать координаты. Например, 125, 250 — это координаты середины нашего игрового экрана.

Так выглядит создание квадратной области с нулевыми координатами и сторонами по 50 пикселей. Обратите внимание, что Rect здесь с большой буквы:

rect1 = pygame.Rect(0, 0, 50, 50)

С цветом всё проще: создаём отдельно объект и в скобках указываем цвет. Можно строкой, можно через набор RGB, можно CMYK. Мы для ячеек зададим серый цвет строкой "Gray"

Целиком эта часть выглядит вот так:

# проходим по каждой ячейке ещё раз
for i in range(columns):
   for j in range(strings):
       # добавляем ещё два параметра: создаём область прямоугольника и задаём цвет для каждой ячейки
       grid[i][j].append(pygame.Rect(i * cell_x, j * cell_y, cell_x, cell_y))
       grid[i][j].append(pygame.Color("Gray"))

Если перевести с программистского на русский, то работает это так:

  • Когда мы идём по первому списку for i in range(columns), переменная  i принимает значения от 0 до 10. В скобках у нас указано 11 (столько сейчас в переменной columns), но счёт идёт от 0 и не включает последнее число.
  • Каждый раз, когда i принимает какое-то значение, запускается вложенный цикл for j in range(strings), и j принимает значения от 0 до 20. Всё это время i остаётся равным одному значению.
  • На каждое значение j у нас уже есть ячейка со значением 1. Теперь к ней же мы добавляем объект прямоугольной области Rect и цвет Color. Всего получается по три характеристики для каждой ячейки.

Сетка получилась размером 11 × 21, но крайние ряд и столбец останутся за кадром и не будут отображаться. Вот как всё выглядит на финальном экране, где добавлены цвета:

Мы создали игровое поле, пора браться за фигуры.

Добавляем фигуры

Каждая фигура в Тетрисе состоит из 4 квадратов. Мы сделаем такой список, где каждый квадрат у разных фигур будет описываться координатами относительно какого-то центрального квадрата. Центральный нулевой квадрат каждой фигуры мы позже сделаем осью вращения, и он всегда будет третьим в списке:

Так это выглядит в коде:

# описание фигур Тетриса
details = [
   # линия
   [[-2, 0], [-1, 0], [0, 0], [1, 0]], 
   # L-образная
   [[-1, 1], [-1, 0], [0, 0], [1, 0]], 
   # обратная L-образная
   [[1, 1], [-1, 0], [0, 0], [1, 0]], 
   # квадрат
   [[-1, 1], [0, 1], [0, 0], [-1, 0]], 
   # Z-образная
   [[1, 0], [1, 1], [0, 0], [-1, 0]], 
   # обратная Z-образная
   [[0, 1], [-1, 0], [0, 0], [1, 0]], 
   # T-образная
   [[-1, 1], [0, 1], [0, 0], [1, 0]], 
]

Это ещё не фигуры, но список с координатами:

  • в одном большом списке лежат семь подсписков;
  • в каждом из семи списков второго уровня лежит ещё по четыре списка;
  • в каждом из четырёх списков третьего уровня лежат по два значения-координаты.

Теперь сделаем список с шаблонами. В нём будут храниться семь заготовок фигур тетрамино разного вида. По заготовкам мы потом будем рисовать окончательные детали:

# создаём список для хранения всех 7 фигур
det = [[], [], [], [], [], [], []]

Мы пойдём по списку det и заполним его фигурами разного типа. Для этого координаты мы умножим на стороны квадратов cell_x и cell_y, каждая из которых равна 50 пикселям.

# инициализация фигур: проходим по списку с описанием координат
for i in range(len(details)):
   for j in range(4):
       # создаём прямоугольные области для каждого составного квадрата
       det[i].append(pygame.Rect(details[i][j][0] * cell_x + cell_x * (columns // 2), details[i][j][1] * cell_y, cell_x, cell_y))

Осталось ещё несколько предварительных настроек, и можно будет писать основную часть. Для финального рисования квадрата нам понадобится прямоугольная область Rect. Это как трафарет, который мы потом заполним. Нам понадобится всего один, потому что мы будем переставлять его и заполнять новыми квадратами.

# создаём область Rect для одной ячейки фигуры
detail = pygame.Rect(0, 0, cell_x, cell_y)

Когда игра начнётся, мы будем вызывать новую случайную фигуру из списка шаблонов det. Чтобы ничего не сломать в самом списке, воспользуемся глубоким копированием:

# выбираем случайную фигуру
det_choice = copy.deepcopy(random.choice(det))

В тетрисе фигуры падают вниз с разной скоростью, но для начала мы сделаем только две: обычную скорость и ускоренную при нажатии на ⭣. Чтобы где-то хранить скорость, заведём переменную count. Она будет влиять на количество кадров в секунду.

# счётчик для управления скоростью падения фигур
count = 0

Создадим флаг, чтобы игра понимала, когда ей работать, а когда нет:

# флаг для управления игровым циклом
game = True

И ещё один флаг — он понадобится при настройке поворота фигур. По умолчанию они не поворачиваются, поэтому начальным значением будет False:

# флаг для управления поворотом фигур
rotate = False

На этом этапе мы подготовили всё, чтобы написать основную логику игры.

Рисуем сетку экрана и задаём границы

Запускаем бесконечный цикл, используя нашу переменную-флаг game:

# запускаем бесконечный цикл: он работает, пока флаг цикла равен True
while game:

Всё, что мы будем писать дальше, будет внутри этого цикла, а значит, будет иметь один отступ.

Например, сначала мы объявим переменные, в которых будет храниться смещение, выраженное в количестве квадратов. Переменные выглядят так:

# изменение по оси x
delta_x = 0
# изменение по оси y (движение вниз)
delta_y = 1 

Но в общем цикле они будут с отступом:

# запускаем бесконечный цикл: он работает, пока флаг цикла равен True
while game:
   # изменение по оси x
   delta_x = 0
   # изменение по оси y (движение вниз)
   delta_y = 1

Сделаем так, чтобы координаты менялись при нажатии клавиш клавиатуры. Для этого понадобится модуль pygame.event, который отслеживает внешние события — нажатия на клавиши клавиатуры, мыши или на крестик в главном окне игры. Выход из игры тоже добавим. 

Для всех событий сделаем один цикл:

# выход из игры при закрытии окна
for event in pygame.event.get():
   if event.type == pygame.QUIT:
       exit()
   # обработка нажатий клавиш
   if event.type == pygame.KEYDOWN:
       # движение влево
       if event.key == pygame.K_LEFT:
           delta_x = -1
       # движение вправо
       elif event.key == pygame.K_RIGHT:
           delta_x = 1
       # поворот
       elif event.key == pygame.K_UP:
           rotate = True

Реагировать на действия игрока мы научились, но пока не учли, что он может ускорить её падение. Реализуем такое действие через зажатую клавишу ⭣. Сначала получим состояние всех зажатых клавиш:

# получаем текущее состояние всех клавиш на клавиатуре
key = pygame.key.get_pressed() 

Такой командой мы получим список в переменной key. Теперь проверим, есть ли среди них клавиша ⭣, и если есть, увеличим счётчик скорости. Для этого умножим нашу частоту кадров fps на 31:

# ускорение падения фигуры
if key[pygame.K_DOWN]: 
   count = 31 * fps

Теперь про то, откуда здесь именно 31: ниже мы настроим стандартную скорость падения и сделаем так, что одно смещение вниз будет происходить за 30 кадров, то есть 2 смещения в секунду. Чтобы увеличить скорость принудительного падения, нам нужно превысить стандартную скорость в 30 кадров, поэтому берём 31.

Создаём сетку для экрана и задаём границы

Заполняем экран фоном. У нас это зелёный — мы используем не стандартный цвет из библиотеки, а выбираем свой. Для этого в скобках указываем четыре характеристики цвета по модели RGB:

# заполняем экран фоном
screen.fill(pygame.Color(222, 248, 116, 100)) 

Теперь нарисуем сетку поверх зелёного фона. Для рисования нам понадобятся два метода библиотеки pygame: draw и rect. В этот раз мы используем именно rect с маленькой буквы, потому что работаем не с областью, а с рисованием прямоугольника. 

Методы для рисования вызываются один за другим через точку:

pygame.draw.rect()

В скобках после rect указываем четыре параметра через запятую:

  1. Поверхность для рисования — это будет наш экран, сохранённый в переменной screen.
  2. Цвет прямоугольника.
  3. Объект области Rect, у которого уже есть координаты. 
  4. Толщина контура. Это необязательный параметр. Если в скобках мы укажем только три значения, Python нарисует залитый прямоугольник; если укажем ещё толщину контура, получим рамку заданной толщины.

Нам нужна только сетка, поэтому указываем толщину контура. Все значения уже подготовлены, поэтому берём их из вложенных списков, которые хранятся в grid:

# отрисовка сетки
for i in range(columns):
   for j in range(strings):
       pygame.draw.rect(screen, grid[i][j][2], grid[i][j][1], grid[i][j][0])

Чтобы взять нужный параметр из списка, спускаемся по уровням. Сначала обращаемся к списку i — это номер столбца от 0 до 10, потом к j — это номер строки от 0 до 20. В каждой из ячеек у нас лежат три значения: занятость ячейки, объект Rect и цвет. Теперь их можно расставить по нужным местам для описания rect:

  • grid[i][j][2] — это цвет, ставим его на второе место после screen;
  • grid[i][j][1] — объект прямоугольной области Rect, он идёт после цвета;
  • grid[i][j][0] ставим в конце — это маркировка занятости ячейки, которая меняется от 0 до 1 и вполне подойдёт для обозначения толщины контура.

Сетка готова. Чтобы на неё посмотреть, в код надо добавить ещё две строки:

# обновляем экран
pygame.display.flip()
# задаём частоту обновления кадров
clock.tick(fps) 

Если запустить код, увидим экран нашей игры с названием, пустую сетку, и возможность закрыть игру нажатием крестика в углу. Фон экрана зелёный, сама сетка серая:

Задаём границы движения

Настроим проверку координат у наших фигур, чтобы при движении они не улетали за границы экрана. Сначала добавим проверку горизонтальных x-координат. Для падающей фигуры у нас есть специальная переменная det_choice. В ней хранятся координаты четырёх квадратов. 

Чтобы задать координаты по экрану, в pygame у объекта нужно вызвать аргументы .x и .y. Если какой-то из квадратов фигуры, которые хранятся в det_choice, при следующем движении влево или вправо выйдет за отрезок, равный ширине экрана screen_x, то мы обнуляем движение. 

За перемещение по горизонтали отвечает переменная delta_x, поэтому при выполнении условия присвоим ей нулевое значение:

 # проверка границ
for i in range(4):
   # по горизонтали
   if ((det_choice[i].x + delta_x * cell_x < 0) or (det_choice[i].x + delta_x * cell_x >= screen_x)):
       delta_x = 0

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

Тут нам пригодится маркировка. Вспомним: 0 — ячейка занята, 1 — свободна.

# по вертикали
if ((det_choice[i].y + cell_y >= screen_y) or (
   grid[int(det_choice[i].x // cell_x)][int(det_choice[i].y // cell_y) + 1][0] == 0)):
   delta_y = 0

Сейчас не хватает ещё одного цикла: для новой маркировки ячеек. Для этого после остановки каждой фигуры будем проходить по её координатам и отмечать, что теперь эти места заняты. А ещё поменяем цвет установленной фигуры на синий.

# берём координаты x и y у каждого квадрата, из которых состоит остановившаяся фигура
for i in range(4):
   x = int(det_choice[i].x // cell_x)
   y = int(det_choice[i].y // cell_y)
   # отмечаем в основном списке сетки, что такая ячейка занята
   grid[x][y][0] = 0
   # меняем цвет установленной фигуры на синий
   grid[x][y][2] = pygame.Color(45, 109, 234, 100)

Когда всё расставлено, нужно обнулить координаты для той области, где появляется новая случайная деталь, и выбрать новую фигуру из списка шаблонов:

# сбрасываем координаты новой фигуры
detail.x = 0
detail.y = 0
# выбираем новую фигуру
det_choice = copy.deepcopy(random.choice(det))

Вот как будет выглядеть полная проверка границ:

# проверка границ
for i in range(4):
   # по горизонтали
   if ((det_choice[i].x + delta_x * cell_x < 0) or (det_choice[i].x + delta_x * cell_x >= screen_x)):
       delta_x = 0
   # по вертикали
   if ((det_choice[i].y + cell_y >= screen_y) or (
       grid[int(det_choice[i].x // cell_x)][int(det_choice[i].y // cell_y) + 1][0] == 0)):
       delta_y = 0
       # берём координаты x и y у каждого квадрата, из которых состоит остановившаяся фигура
       for i in range(4):
           x = int(det_choice[i].x // cell_x)
           y = int(det_choice[i].y // cell_y)
           # отмечаем в основном списке сетки, что такая ячейка занята
           grid[x][y][0] = 0
           # меняем цвет установленной фигуры на синий
           grid[x][y][2] = pygame.Color(45, 109, 234, 100)
       # сбрасываем координаты новой фигуры
       detail.x = 0
       detail.y = 0
       # выбираем новую фигуру
       det_choice = copy.deepcopy(random.choice(det))

Теперь фигуры будут перемещаться только в границах экрана, поэтому можно начинать их двигать.

Подключаем движение фигур

Нам нужно двигать фигуры строго на величину стороны ячейки. Поэтому количество движений всегда умножаем на cell_x или cell_y. У нас эти два значения одинаковые, но вы можете сделать свою сетку с ячейками разного размера.

Наш объект — это переменная det_choice. Перемещение по горизонтали будет выглядеть так:

# перемещение по x
for i in range(4):
   det_choice[i].x += delta_x * cell_x

C перемещением по вертикали сложнее, потому что оно должно происходить автоматически, как падение. Если запустить код сейчас, то деталь очень быстро упадёт вниз. Нам нужно замедлить движение, поэтому будем считать количество обновлений кадра и запускать одно движение в установленный промежуток времени.

Для этого мы будем считать количество кадров в каждой секунде и один раз за 30 кадров сдвигать фигуру вниз. Получается одно движение в 0,5 секунды:

# каждый цикл увеличиваем счётчик количества кадров на 1 секунду
count += fps

# перемещение по y
if count > 30 * fps:
   for i in range(4):
       det_choice[i].y += delta_y * cell_y
   count = 0

Сейчас мы посчитали координаты для фигуры в движении. Теперь отрисуем её. Для этого присвоим координаты каждой ячейке из переменной detail, в которой мы храним квадратный шаблон:

# отрисовка текущей фигуры по 4 квадратам
for i in range(4):
   detail.x = det_choice[i].x
   detail.y = det_choice[i].y
   pygame.draw.rect(screen, pygame.Color("White"), detail)

Фигура падает вниз, и её можно двигать влево и вправо. Но не хватает ещё возможности повернуть фигуру.

Добавляем повороты

Для поворота мы специально предусмотрели, что каждый третий квадрат у всех фигур имеет нулевые координаты. В списке у него будет индекс, равный 2.

Теперь этому центру мы присвоим отдельную переменную и будем работать с ней:

# определяем центр фигуры, у нас это всегда третий квадрат в списке
C = det_choice[2]

Как только игрок нажимает клавишу ⭡ , срабатывает условие: rotate = True, или проще — if rotate:. После этого мы считываем новые координаты для каждого квадрата на нашем поле и перерисовываем фигуру. В конце отчитываемся, что поворот завершён, и присваиваем переменной rotate значение False.

Так выглядит цикл для поворотов:

if rotate:
   # поворачиваем фигуру по часовой стрелке
   for i in range(4):
       # считаем новые координаты
       x = det_choice[i].y - C.y
       y = det_choice[i].x - C.x
       # присваиваем их каждому квадрату по очереди
       det_choice[i].x = C.x - x
       det_choice[i].y = C.y + y
   rotate = False

Осталось обнуление рядов, и можно играть.

Обнуляем ряды

Нам нужно проверить 20 строк в нашей сетке. Проверять будем снизу вверх, потому что так устроена игра. Для этого в цикле мы пойдём от предпоследнего значения к первому, потому что последняя строка находится за экраном. Это будет выглядеть так:

for j in range(strings - 1, -1, -1):

Чтобы ряд считался заполненным, нужно 10 занятых ячеек. Добавляем счётчик:

# создаём счётчик заполненных ячеек
count_cells = 0

В каждой строке мы будем проверять ячейки по номерам столбцов. Если находим маркировку заполненности, равную 0, добавляем её в счётчик. Если хоть одна ячейка пустая, игнорируем эту строку и переходим к следующей:

# идём по каждой ячейке из 10 в строке
for i in range(columns):
   # если ячейка заполнена, увеличиваем счётчик
   if grid[i][j][0] == 0:
       count_cells += 1
   # если какая-то ячейка не заполнена, то прерываем цикл и проверяем дальше
   elif grid[i][j][0] == 1:
       break

Если счётчик набрал 10 рядов, обнуляем всё ячейки. После этого идём уже от этой строки вверх и перемещаем вниз всё, что есть.

Вот как выглядит полный цикл для очистки заполненной строки:

# проверка на заполненные ряды — идём по строкам сетки снизу вверх
for j in range(strings - 1, -1, -1):
   # создаём счётчик заполненных ячеек
   count_cells = 0
   # идём по каждой ячейке из 10 в строке
   for i in range(columns):
       # если ячейка заполнена, увеличиваем счётчик
       if grid[i][j][0] == 0:
           count_cells += 1
       # если хоть одна ячейка не заполнена, то прерываем цикл и проверяем дальше
       elif grid[i][j][0] == 1:
           break
   # проверяем, набралось ли 10 полных ячеек
   if count_cells == (columns - 1):
       # если да, идём по этой строке...
       for l in range(columns):
           # ...и стираем заполненные ячейки
           grid[l][0][0] = 1
       # потом идём по строкам снизу вверх, начиная от удалённого ряда
       for k in range(j, -1, -1):
           for l in range(columns):
               # сдвигаем ряды вниз
               grid[l][k][0] = grid[l][k - 1][0]

На этом почти всё.

В конце нужно обновить экран и установить частоту обновления кадров. У нас для этого есть переменная fps:

# обновляем экран
pygame.display.flip()
# задаём частоту обновления кадров
clock.tick(fps)

Теперь можно играть:

Что можно улучшить

Играть можно, но можно кое-что добавить:

  • главный экран с информацией и подсчётом очков;
  • разный цвет фигур;
  • возможность поставить игру на паузу;
  • музыку;
  • предпросмотр следующей фигуры;
  • более аккуратное обнуление рядов.

В следующий раз доработаем наш тетрис и посмотрим, что ещё можно делать с библиотекой pygame.

# импортируем библиотеку pygame
import pygame
# импортируем библиотеку random для генерации случайных чисел
import random
# импортируем библиотеку copy для глубокого копирования
import copy

# инициализация всех модулей pygame
pygame.init()

# количество столбцов сетки
columns = 11
# количество строк сетки
strings = 21

# ширина окна игры
screen_x = 250
# высота окна игры
screen_y = 500

# создаём окно игры
screen = pygame.display.set_mode((screen_x, screen_y))
# устанавливаем заголовок окна
pygame.display.set_caption("Tetris CODE")
# создаём объект для отслеживания времени
clock = pygame.time.Clock()

# ширина одной ячейки сетки
cell_x = screen_x / (columns - 1)
# высота одной ячейки сетки
cell_y = screen_y / (strings - 1)

# частота кадров в секунду
fps = 60

# создаём пустой список для хранения сетки
grid = []

# проходим по каждому столбцу
for i in range(columns):
   # добавляем пустой список для каждого столбца
   grid.append([])
   # создаём кол-во ячеек, равное строкам,
   # и заполняем их значениями 1 (ячейка свободна)
   for j in range(strings):
       grid[i].append([1])

# проходим по каждой ячейке ещё раз
for i in range(columns):
   for j in range(strings):
       # добавляем ещё два параметра: создаём область прямоугольника и задаём цвет для каждой ячейки
       grid[i][j].append(pygame.Rect(i * cell_x, j * cell_y, cell_x, cell_y))
       grid[i][j].append(pygame.Color("Gray"))

# описание фигур Тетриса
details = [
   # линия
   [[-2, 0], [-1, 0], [0, 0], [1, 0]],
   # L-образная
   [[-1, 1], [-1, 0], [0, 0], [1, 0]],
   # обратная L-образная
   [[1, 1], [-1, 0], [0, 0], [1, 0]],
   # квадрат
   [[-1, 1], [0, 1], [0, 0], [-1, 0]],
   # Z-образная
   [[1, 0], [1, 1], [0, 0], [-1, 0]],
   # обратная Z-образная
   [[0, 1], [-1, 0], [0, 0], [1, 0]],
   # T-образная
   [[-1, 1], [0, 1], [0, 0], [1, 0]],
]

# создаём список для хранения всех 7 фигур
det = [[], [], [], [], [], [], []]

# инициализация фигур: проходим по списку с описанием координат
for i in range(len(details)):
   for j in range(4):
       # создаём прямоугольные области для каждого составного квадрата
       det[i].append(pygame.Rect(details[i][j][0] * cell_x + cell_x * (columns // 2), details[i][j][1] * cell_y, cell_x, cell_y))

# создаём область Rect для одной ячейки фигуры
detail = pygame.Rect(0, 0, cell_x, cell_y)
# выбираем случайную фигуру
det_choice = copy.deepcopy(random.choice(det))
# счётчик для управления скоростью падения фигур
count = 0
# флаг для управления игровым циклом
game = True
# флаг для управления поворотом фигур
rotate = False

# запускаем бесконечный цикл: он работает, пока флаг цикла равен True
while game:
   # изменение по оси x
   delta_x = 0
   # изменение по оси y (движение вниз)
   delta_y = 1

   # выход из игры при закрытии окна
   for event in pygame.event.get():
       if event.type == pygame.QUIT:
           exit()
       # обработка нажатий клавиш
       if event.type == pygame.KEYDOWN:
           # движение влево
           if event.key == pygame.K_LEFT:
               delta_x = -1
           # движение вправо
           elif event.key == pygame.K_RIGHT:
               delta_x = 1
           # поворот
           elif event.key == pygame.K_UP:
               rotate = True

   # получаем текущее состояние всех зажатых клавиш на клавиатуре
   key = pygame.key.get_pressed()

   # ускорение падения фигуры
   if key[pygame.K_DOWN]:
       count = 31 * fps

   # заполняем экран фоном
   screen.fill(pygame.Color(222, 248, 116, 100))

   # отрисовка сетки
   for i in range(columns):
       for j in range(strings):
           pygame.draw.rect(screen, grid[i][j][2], grid[i][j][1], grid[i][j][0])

   # проверка границ
   for i in range(4):
       # по горизонтали
       if ((det_choice[i].x + delta_x * cell_x < 0) or (det_choice[i].x + delta_x * cell_x >= screen_x)):
           delta_x = 0
       # по вертикали
       if ((det_choice[i].y + cell_y >= screen_y) or (
           grid[int(det_choice[i].x // cell_x)][int(det_choice[i].y // cell_y) + 1][0] == 0)):
           delta_y = 0
           # берём координаты x и y у каждого квадрата, из которых состоит остановившаяся фигура
           for i in range(4):
               x = int(det_choice[i].x // cell_x)
               y = int(det_choice[i].y // cell_y)
               # отмечаем в основном списке сетки, что такая ячейка занята
               grid[x][y][0] = 0
               # меняем цвет установленной фигуры на синий
               grid[x][y][2] = pygame.Color(45, 109, 234, 100)
           # сбрасываем координаты новой фигуры
           detail.x = 0
           detail.y = 0
           # выбираем новую фигуру
           det_choice = copy.deepcopy(random.choice(det))

   # перемещение по x
   for i in range(4):
       det_choice[i].x += delta_x * cell_x

   # каждый цикл увеличиваем счётчик количества кадров на 1 секунду
   count += fps

   # перемещение по y
   if count > 30 * fps:
       for i in range(4):
           det_choice[i].y += delta_y * cell_y
       count = 0

   # отрисовка текущей фигуры по 4 квадратам
   for i in range(4):
       detail.x = det_choice[i].x
       detail.y = det_choice[i].y
       pygame.draw.rect(screen, pygame.Color("White"), detail)

   # определяем центр фигуры, у нас это всегда третий квадрат в списке
   C = det_choice[2]
   if rotate:
       # поворачиваем фигуру по часовой стрелке
       for i in range(4):
           # считаем новые координаты
           x = det_choice[i].y - C.y
           y = det_choice[i].x - C.x
           # присваиваем их каждому квадрату по очереди
           det_choice[i].x = C.x - x
           det_choice[i].y = C.y + y
       rotate = False

   # проверка на заполненные ряды — идём по строкам сетки снизу вверх
   for j in range(strings - 1, -1, -1):
       # создаём счётчик заполненных ячеек
       count_cells = 0
       # идём по каждой ячейке из 10 в строке
       for i in range(columns):
           # если ячейка заполнена, увеличиваем счётчик
           if grid[i][j][0] == 0:
               count_cells += 1
           # если хоть одна ячейка не заполнена, то прерываем цикл и проверяем дальше
           elif grid[i][j][0] == 1:
               break
       # проверяем, набралось ли 10 полных ячеек
       if count_cells == (columns - 1):
           # если да, идём по этой строке...
           for l in range(columns):
               # ...и стираем заполненные ячейки
               grid[l][0][0] = 1
           # потом идём по строкам снизу вверх, начиная от удалённого ряда
           for k in range(j, -1, -1):
               for l in range(columns):
                   grid[l][k][0] = grid[l][k - 1][0]  # сдвигаем ряды вниз

   # обновляем экран
   pygame.display.flip()
   # задаём частоту обновления кадров
   clock.tick(fps)

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Маша Климентьева

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
easy
[anycomment]
Exit mobile version