AI Отладка «Тетрисом»: Пошаговый гайд по созданию и сокрытию классической игры в вашем проекте

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 819
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1
Привет, Хабр! У каждого разработчика в серьезном проекте наступает момент, когда хочется отвлечься и написать что-то для души. Что-то простое, классическое, но в то же время увлекательное. Часто такие "внутренние пет-проекты" становятся «пасхальными яйцами» — секретами для самых любопытных пользователей.

Сегодня мы расскажем, как и зачем мы спрятали в нашем приложении для стеганографии «ChameleonLab» классический «Тетрис». Это не просто история о «пасхалке», а пошаговый гайд с подробным разбором кода на Python и PyQt6, который покажет, что, несмотря на кажущуюся простоту, создание «Тетриса» — это интересная задача с множеством подводных камней.


Зачем нужны «пасхальные яйца»?


«Пасхальное яйцо» — это визитная карточка команды, способ оставить свой след в проекте и подмигнуть пользователю. Это создает особую связь, превращая программу из бездушного инструмента в продукт, сделанный людьми для людей. От about:robots в Firefox до симулятора полетов в старом Excel — «пасхалки» делают цифровой мир немного теплее. Вдохновившись этой идеей, мы тоже решили добавить в наше приложение небольшой сюрприз.

Разбираем «Тетрис»: пошаговая реализация на PyQt6


На первый взгляд, «Тетрис» кажется простой игрой. Но когда начинаешь писать код, сталкиваешься с интересными задачами: как описать фигуры и их вращение? Как эффективно проверять столкновения? Как организовать игровой цикл? Давайте разберем наше решение по шагам на основе файла page_tetris.py.


Программа "ChameleonLab". «Пасхальное яйцо»
Шаг 1: Архитектура игры


Наша реализация состоит из четырех основных классов:


  • Tetromino: Описывает одну фигурку (ее форму, цвет, вращение).


  • TetrisBoard: Главный класс, содержащий всю игровую логику: поле, падение фигур, проверку столкновений и удаление линий.


  • NextPieceWidget: Небольшой виджет для отображения следующей фигуры.


  • TetrisPage: Собирает все элементы вместе в единый интерфейс.
Шаг 2: Строительные блоки — Tetromino


Каждая из семи фигур состоит из 4 блоков. Описываем их координаты относительно центральной точки в статическом кортеже. Номер в coords_table соответствует фигуре и в дальнейшем будет использоваться для ее цвета.

# ui/page_tetris.py
class Tetromino:
coords_table = (
((0, 0), (0, 0), (0, 0), (0, 0)), # Пустая фигура
((0, -1), (0, 0), (-1, 0), (-1, 1)), # Z
((0, -1), (0, 0), (1, 0), (1, 1)), # S
((0, -1), (0, 0), (0, 1), (0, 2)), # I (Палка)
((-1, 0), (0, 0), (1, 0), (0, 1)), # T
((0, 0), (1, 0), (0, 1), (1, 1)), # O (Квадрат)
((-1, -1), (0, -1), (0, 0), (0, 1)), # L
((1, -1), (0, -1), (0, 0), (0, 1)) # J
)

def __init__(self, shape=0):
self.coords = [[0,0] for _ in range(4)]
self.piece_shape = 0
self.set_shape(shape)

def set_shape(self, shape):
table = self.coords_table[shape]
for i in range(4):
self.coords = list(table)
self.piece_shape = shape


Вращение фигуры — это классическое преобразование координат (x, y) в (-y, x). Квадрат (shape = 5) вращать не нужно, поэтому его мы пропускаем.

# ui/page_tetris.py
def rotated(self):
if self.piece_shape == 5: # Квадрат
return self
result = Tetromino(self.piece_shape)
for i in range(4):
result.coords[0] = -self.y(i)
result.coords[1] = self.x(i)
return result

Шаг 3: Игровое поле TetrisBoard — мозг игры


Это самый сложный и насыщенный логикой класс.

Представление поля и игровой цикл: Поле 10x22 клетки представляем в виде одномерного списка, где 0 — пустая ячейка. За движение отвечает QBasicTimer, который вызывает событие timerEvent каждые 400 миллисекунд и двигает фигуру вниз.

# ui/page_tetris.py
class TetrisBoard(QtWidgets.QFrame):
BoardWidth = 10
BoardHeight = 22
Speed = 400

def __init__(self):
super().__init__()
self.timer = QtCore.QBasicTimer()
self.board = [] # Будет инициализирован в init_game()
# ...

def timerEvent(self, event):
if event.timerId() == self.timer.timerId() and self.is_started and not self.is_paused:
self.one_line_down() # Двигаем фигуру вниз
else:
super().timerEvent(event)


Обработка ввода и столкновений: Это самая нетривиальная часть. На каждое нажатие клавиши мы не просто двигаем фигуру, а вызываем try_move. Эта функция проверяет, будет ли новое положение фигуры корректным (в границах поля и не поверх других блоков). И только если проверка пройдена, реальные координаты фигуры (self.cur_x, self.cur_y) обновляются.

# ui/page_tetris.py
def keyPressEvent(self, event):
# ...
key = event.key()
if key == QtCore.Qt.Key.Key_Left:
self.try_move(self.cur_piece, self.cur_x - 1, self.cur_y)
elif key == QtCore.Qt.Key.Key_Right:
self.try_move(self.cur_piece, self.cur_x + 1, self.cur_y)
elif key == QtCore.Qt.Key.Key_Up:
self.try_move(self.cur_piece.rotated(), self.cur_x, self.cur_y)
# ...

def try_move(self, new_piece, new_x, new_y):
# Проверяем каждый из 4 блоков новой фигуры
for i in range(4):
x = new_x + new_piece.x(i)
y = new_y + new_piece.y(i)
# Если хотя бы один блок выходит за границы или попадает на занятую клетку...
if not (0 <= x < self.BoardWidth and 0 <= y < self.BoardHeight and self.shape_at(x, y) == 0):
return False # ...то движение невозможно

# Если все проверки пройдены, обновляем фигуру и ее позицию
self.cur_piece = new_piece
self.cur_x = new_x
self.cur_y = new_y
self.update() # Перерисовываем поле
return True


Падение и "замораживание" фигуры: Когда try_move возвращает False при движении вниз, это значит, что фигура приземлилась. Мы вызываем piece_dropped, которая "впечатывает" блоки фигуры в массив self.board, а затем запускает проверку на заполненные линии.

Удаление линий и подсчет очков: Этот процесс требует аккуратности. Мы ищем все ряды, где нет ни одной пустой ячейки (0). Затем, для каждого такого ряда (в порядке снизу вверх), мы сдвигаем все находящиеся выше ряды на одну клетку вниз.

# ui/page_tetris.py
def remove_full_lines(self):
num_full_lines = 0
full_rows = []
for i in range(self.BoardHeight):
# Если в ряду i нет нулей (пустых клеток), он полный
if all(self.shape_at(j, i) != 0 for j in range(self.BoardWidth)):
full_rows.append(i)

if full_rows:
for row in sorted(full_rows, reverse=True):
# Начиная с удаляемого ряда и до самого верха...
for r in range(row, 0, -1):
# ...копируем в каждую клетку значение из клетки выше
for c in range(self.BoardWidth):
self.set_shape_at(c, r, self.shape_at(c, r - 1))

# Обновляем счет и перерисовываем
self.score += 10 * len(full_rows)
self.scoreChanged.emit(self.score)


Отрисовка: Визуализация происходит в paintEvent. Мы проходим по всему нашему списку self.board и, если ячейка не пустая, рисуем в ее координатах квадрат нужного цвета с помощью draw_square. Поверх "замороженных" блоков рисуется текущая падающая фигура.

Шаг 4: Сборка интерфейса TetrisPage


На последнем шаге мы просто собираем все виджеты вместе в трехколоночном макете и связываем сигналы (например, scoreChanged) с соответствующими слотами (обновление текста в QLabel).

Как спрятать игру в приложении?


Теперь самое интересное. В главном окне программы (main_window.py) мы установили фильтр событий на логотип приложения. Код отслеживает клики мыши, запоминая время каждого. Если пользователь совершает 5 или более кликов за 2 секунды, программа считает, что секретный код введен, и переключает вид на TetrisPage.

# ui/main_window.py
def eventFilter(self, obj, event):
# Если кликнули по логотипу...
if obj is self.logo_label and event.type() == QtCore.QEvent.Type.MouseButtonPress:
now = datetime.datetime.now()
# Отсеиваем старые клики (старше 2 секунд)
self.logo_clicks = [t for t in self.logo_clicks if (now - t).total_seconds() < 2]
self.logo_clicks.append(now)

# Если накопилось 5 быстрых кликов — запускаем игру!
if len(self.logo_clicks) >= 5:
self.logo_clicks = []
self.stacked_widget.setCurrentWidget(self.tetris_page)
return True # Событие обработано
return super().eventFilter(obj, event)

Заключение


Как видите, за кажущейся простотой «Тетриса» скрывается немало интересной логики. Его создание — прекрасное упражнение, которое затрагивает управление состоянием, обработку пользовательского ввода, и базовую игровую физику.

«Пасхальное яйцо» — это не просто скрытая игра, а знак уважения к классике, способ развлечь пользователя и показатель того, что команда вкладывает в свой продукт не только код, но и душу. Надеемся, наш гайд был полезен, и, конечно, попробуйте найти наш «Тетрис» сами!

Последнюю версию программы «Steganographia» от ChameleonLab для Windows и macOS можно скачать на нашем официальном сайте.

А чтобы быть в курсе обновлений, обсуждать новые функции и общаться с единомышленниками, присоединяйтесь к нашему Telegram-каналу: https://t.me/ChameleonLab
 
Сверху Снизу