- Security
- A
Debugging with “Tetris”: A step-by-step guide to creating and hiding the classic game in your project
Hello, tekkix! Every developer reaches a point in a serious project when they want to take a break and write something just for fun. Something simple, classic, and yet captivating. Often, these internal pet projects become “Easter eggs”—secrets for the most curious users.
Today we will talk about how and why we hid the classic "Tetris" in our steganography app "ChameleonLab". This is not just a story about an "Easter egg," but a step-by-step guide with a detailed breakdown of the code in Python and PyQt6, showing that, despite its apparent simplicity, creating "Tetris" is an interesting task with many pitfalls.
Why do we need "Easter eggs"?
An "Easter egg" is the calling card of the team, a way to leave a mark on the project and wink at the user. It creates a special connection, turning a program from a soulless tool into a product made by people for people. From about:robots
in Firefox to the flight simulator in old Excel — "Easter eggs" make the digital world a little warmer. Inspired by this idea, we also decided to add a little surprise to our app.
Breaking down "Tetris": step-by-step implementation in PyQt6
At first glance, "Tetris" seems like a simple game. But when you start writing the code, you encounter interesting challenges: how to describe the shapes and their rotation? How to efficiently check for collisions? How to organize the game loop? Let's break down our solution step by step based on the page_
tetris.py
file.
Step 1: Game Architecture
Our implementation consists of four main classes:
Tetromino
: Describes a single piece (its shape, color, rotation).TetrisBoard
: The main class containing all the game logic: the field, falling pieces, collision checking, and line removal.NextPieceWidget
: A small widget to display the next piece.TetrisPage
: Combines all elements into a single interface.
Step 2: Building Blocks — Tetromino
Each of the seven shapes consists of 4 blocks. We describe their coordinates relative to the central point in a static tuple. The number in the coords_table
corresponds to the shape and will later be used for its color.
# ui/page_tetris.py
class Tetromino:
coords_table = (
((0, 0), (0, 0), (0, 0), (0, 0)), # Empty piece
((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 (Line)
((-1, 0), (0, 0), (1, 0), (0, 1)), # T
((0, 0), (1, 0), (0, 1), (1, 1)), # O (Square)
((-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[i] = list(table[i])
self.piece_shape = shape
Rotating the piece is a classic coordinate transformation (x, y)
to (-y, x)
. The square (shape = 5
) doesn't need to be rotated, so we skip it.
# ui/page_tetris.py
def rotated(self):
if self.piece_shape == 5: # Square
return self
result = Tetromino(self.piece_shape)
for i in range(4):
result.coords[i][0] = -self.y(i)
result.coords[i][1] = self.x(i)
return result
Step 3: TetrisBoard — the brain of the game
This is the most complex and logic-heavy class.
Field representation and game loop: The 10x22 field is represented as a one-dimensional list, where 0
is an empty cell. Movement is handled by the QBasicTimer
, which triggers the timerEvent
every 400 milliseconds and moves the piece down.
# 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 = [] # Initialized in 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() # Move piece down
else:
super().timerEvent(event)
Input and collision handling: This is the most non-trivial part. For each key press, we don't just move the piece, but call try_move
. This function checks if the new position is valid (within the field boundaries and not overlapping with other blocks). Only if the check passes, the actual coordinates of the piece (self.cur_x
, self.cur_y
) are updated.
# 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):
# Check each of the 4 blocks of the new piece
for i in range(4):
x = new_x + new_piece.x(i)
y = new_y + new_piece.y(i)
# If at least one block goes out of bounds or overlaps with an occupied cell...
if not (0 <= x < self.BoardWidth and 0 <= y < self.BoardHeight and self.shape_at(x, y) == 0):
return False # ...then movement is not possible
# If all checks pass, update the piece and its position
self.cur_piece = new_piece
self.cur_x = new_x
self.cur_y = new_y
self.update() # Redraw the field
return True
Piece falling and "freezing": When try_move
returns False
while moving down, it means the piece has landed. We call piece_dropped
, which "stamps" the blocks of the piece into the self.board
array, then checks for filled lines.
Line removal and score counting: This process requires accuracy. We look for all rows that have no empty cells (0
). Then, for each such row (from bottom to top), we shift all rows above it down by one cell.
# ui/page_tetris.py
def remove_full_lines(self):
num_full_lines = 0
full_rows = []
for i in range(self.BoardHeight):
# If row i has no zeros (empty cells), it is full
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):
# Starting from the removed row and going upwards...
for r in range(row, 0, -1):
# ...copy each cell's value from the cell above
for c in range(self.BoardWidth):
self.set_shape_at(c, r, self.shape_at(c, r - 1))
# Update score and redraw
self.score += 10 * len(full_rows)
self.scoreChanged.emit(self.score)
Rendering: Visualization happens in paintEvent
. We go through the entire self.board
list, and if a cell is not empty, we draw a square of the corresponding color at its coordinates using draw_square
. The current falling piece is drawn on top of the "frozen" blocks.
Step 4: Building the TetrisPage Interface
At the final step, we simply put all the widgets together in a three-column layout and connect signals (such as scoreChanged
) to the corresponding slots (like updating the text in QLabel
).
How to hide the game in the app?
Now for the fun part. In the main window of the application (main_
window.py
) we set an event filter on the app logo. The code tracks mouse clicks, remembering the time of each. If the user makes 5 or more clicks within 2 seconds, the program considers the secret code entered and switches the view to TetrisPage
.
# ui/main_window.py
def eventFilter(self, obj, event):
# If the logo was clicked...
if obj is self.logo_label and event.type() == QtCore.QEvent.Type.MouseButtonPress:
now = datetime.datetime.now()
# Filter out old clicks (older than 2 seconds)
self.logo_clicks = [t for t in self.logo_clicks if (now - t).total_seconds() < 2]
self.logo_clicks.append(now)
# If there are 5 quick clicks — start the game!
if len(self.logo_clicks) >= 5:
self.logo_clicks = []
self.stacked_widget.setCurrentWidget(self.tetris_page)
return True # Event handled
return super().eventFilter(obj, event)
Conclusion
As you can see, behind the apparent simplicity of “Tetris” lies quite a bit of interesting logic. Making it is a great exercise that involves state management, user input handling, and basic game physics.
The “Easter egg” is not just a hidden game, but a sign of respect for the classics, a way to entertain the user, and proof that the team puts soul, not just code, into their product. We hope this guide was helpful, and definitely try to find our “Tetris” yourself!
You can download the latest version of the “Steganographia” program by ChameleonLab for Windows and macOS on our official website.
To stay updated, discuss new features, and chat with fellow enthusiasts, join our Telegram channel: https://t.me/ChameleonLab
Write comment