Let's design the classic game Tetris. Consider the key components and functionalities:
Board Representation: How would you represent the Tetris board in memory? What data structure would you use, and why? Consider the tradeoffs between different representations (e.g., 2D array, bit arrays) in terms of memory usage and access time.
Tetromino Design: How would you represent the different Tetromino shapes (I, O, T, S, Z, L, J)? How would you store their rotations? Provide an example of how you might represent the 'T' tetromino and its rotations.
Game Logic: Describe the core game loop. Explain how you would handle the following events:
User Interface: How would you approach designing a simple text-based or graphical user interface for the game? Consider how you would display the board, the current piece, and the next piece. What libraries or frameworks might you use?
Enhancements (Optional):
Discuss the object-oriented principles you might apply to the design, and provide code snippets (in your preferred language) to illustrate key aspects of the implementation.
Let's design the classic game Tetris. We will cover board representation, tetromino design, game logic, UI considerations, and potential enhancements.
A 2D array is the most straightforward and commonly used data structure to represent the Tetris board. Each element in the array represents a cell on the board. The value of the element indicates whether the cell is empty or occupied by a block.
Example (in Python):
class TetrisBoard:
def __init__(self, width, height):
self.width = width
self.height = height
self.board = [[' ' for _ in range(width)] for _ in range(height)]
def display(self):
for row in self.board:
print('|' + ''.join(row) + '|')
print('-' * (self.width + 2))
# Example usage
board = TetrisBoard(10, 20)
board.display()
For a standard Tetris board size (e.g., 10x20), a 2D array offers a good balance between simplicity and performance. Therefore, it is a preferred choice.
Each Tetromino can be represented as a list of coordinates relative to a central point. Rotations can be pre-calculated and stored as different states for each Tetromino.
class Tetromino:
def __init__(self, shape, color):
self.shape = shape
self.color = color
self.rotation_index = 0
def rotate(self):
self.rotation_index = (self.rotation_index + 1) % len(self.shape)
def get_current_rotation(self):
return self.shape[self.rotation_index]
# T-Tetromino
T_SHAPE = [
[(0, 0), (-1, 0), (1, 0), (0, -1)], # Initial rotation
[(0, 0), (0, -1), (0, 1), (-1, 0)], # Rotate 90 degrees
[(0, 0), (-1, 0), (1, 0), (0, 1)], # Rotate 180 degrees
[(0, 0), (0, -1), (0, 1), (1, 0)] # Rotate 270 degrees
]
tetromino_T = Tetromino(T_SHAPE, 'purple')
# Example Usage
print(tetromino_T.get_current_rotation())
tetromino_T.rotate()
print(tetromino_T.get_current_rotation())
Each shape is defined by a list of coordinates representing the blocks that make up the shape, relative to a pivot point (0, 0). The rotate
function changes the rotation_index
to select the next rotation state.
The game loop consists of the following steps:
To determine if a piece can be placed at a given location, we check if all the cells that the piece would occupy are empty. This involves iterating through the piece's coordinates and checking the corresponding cells on the board.
def can_place_piece(board, piece, offset_x, offset_y):
for x, y in piece.get_current_rotation():
board_x = x + offset_x
board_y = y + offset_y
if (board_x < 0 or board_x >= board.width or
board_y < 0 or board_y >= board.height or
board.board[board_y][board_x] != ' '):
return False
return True
Collision detection is similar to piece placement. We check if any of the cells that the piece would occupy after moving down are already occupied or are outside the board boundaries.
def detect_collision(board, piece, offset_x, offset_y):
for x, y in piece.get_current_rotation():
board_x = x + offset_x
board_y = y + offset_y + 1 # Check one row below
if (board_y >= board.height or # Check for bottom collision first!
board_x < 0 or board_x >= board.width or
board.board[board_y][board_x] != ' '):
return True
return False
To detect and clear completed lines, we iterate through each row of the board. If all cells in a row are occupied, we clear the row and shift all rows above it down by one position.
def clear_lines(board):
lines_cleared = 0
for y in range(board.height):
if all(board.board[y][x] != ' ' for x in range(board.width)):
# Clear the line
lines_cleared += 1
del board.board[y]
board.board.insert(0, [' ' for _ in range(board.width)]) # Insert empty line at the top
return lines_cleared
Complexity of Line Clearing Algorithm: O(w * h) where w is the width and h is the height of the board. We iterate over each cell on the board in the worst case to check for full lines.
Scoring can be implemented based on the number of lines cleared at once. A Tetris clear (clearing four lines at once) should award a significantly higher score.
def calculate_score(lines_cleared):
if lines_cleared == 1:
return 40
elif lines_cleared == 2:
return 100
elif lines_cleared == 3:
return 300
elif lines_cleared == 4:
return 1200 # Tetris!
return 0
The game is over when a new piece cannot be placed at the top of the board because the cells are already occupied.
def is_game_over(board, piece):
return not can_place_piece(board, piece, board.width // 2 - 1, 0)
A simple text-based UI can be implemented using terminal output. We can represent the board as a grid of characters, where each character represents a cell. The current piece can be drawn on the board using different characters.
# (Example already shown in Board Representation section for displaying the board)
For a graphical UI, libraries like Pygame or Tkinter (in Python) can be used. These libraries provide functions for drawing shapes, handling user input, and updating the display.
The ghost piece shows where the current piece will land. To implement this, we simulate the piece falling until it collides, without actually placing it. The ghost piece is then drawn in a different color or style.
Hold functionality allows the player to store the current piece and swap it with the next piece. This can be implemented by storing the held piece in a separate variable. When the player uses the hold function, we swap the current piece with the held piece. If there is no held piece, we generate a new piece.
The game's difficulty can be increased over time by increasing the falling speed of the pieces. This can be achieved by reducing the delay between each move of the piece down.
Tetromino
).By applying these principles, we can create a well-structured and maintainable Tetris game.