How would you design the Tetris game, including board representation, tetromino design, game logic, and user interface considerations? Include code snippets to illustrate key aspects of the implementation. Let's discuss the object-oriented principles you might apply to this design. Also, discuss possible enhancements such as the Ghost piece, hold functionality, and increasing difficulty over time. How does line clearing work in more detail, including its complexity? How is game over determined? Provide examples of the data structures you might use for the board and the tetrominoes.

Medium
8 years ago

Let's design the classic game Tetris. Consider the key components and functionalities:

  1. 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.

  2. 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.

  3. Game Logic: Describe the core game loop. Explain how you would handle the following events:

    • Piece placement: How do you determine if a piece can be placed at a given location?
    • Collision detection: How do you detect collisions between a falling piece and the board or other pieces?
    • Line clearing: How do you detect and clear completed lines? What is the complexity of the line clearing algorithm?
    • Scoring: How would you implement the scoring system, including handling Tetris clears (clearing four lines at once)?
    • Game over: When and how is game over determined?
  4. 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?

  5. Enhancements (Optional):

    • Ghost piece: How would you implement a "ghost piece" to show where the current piece will land?
    • Hold functionality: How would you allow the player to hold a piece and swap it with the current piece?
    • Increasing difficulty: How would you increase the game's difficulty over time (e.g., increasing the falling speed)?

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.

Sample Answer

Tetris Game Design

Let's design the classic game Tetris. We will cover board representation, tetromino design, game logic, UI considerations, and potential enhancements.

1. Board Representation

Data Structure

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

Tradeoffs

  • 2D Array:
    • Pros: Simple to implement and understand. Easy to access and modify individual cells.
    • Cons: Can be memory-intensive for very large boards.
  • Bit Arrays:
    • Pros: Memory-efficient, especially for large boards.
    • Cons: More complex to implement. Bitwise operations can be less intuitive.

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.

2. Tetromino Design

Representation

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.

Example: 'T' 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.

3. Game Logic

Core Game Loop

The game loop consists of the following steps:

  1. Generate a new piece: Select a random Tetromino.
  2. Move piece down: Move the piece one row down.
  3. Collision detection: Check if the piece has collided with the bottom of the board or other pieces.
  4. Piece placement: If a collision is detected, place the piece on the board.
  5. Line clearing: Check for and clear completed lines.
  6. Scoring: Update the score based on the number of lines cleared.
  7. Game over: Check if the game is over (piece reaches the top of the board).
  8. Repeat: Go back to step 1.

Piece Placement

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

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

Line Clearing

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

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

Game Over

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)

4. User Interface

Text-Based UI

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)

Graphical UI

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.

  • Pygame: Suitable for more complex graphics and animations.
  • Tkinter: Simpler to use for basic UI elements.

5. Enhancements

Ghost Piece

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

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.

Increasing Difficulty

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.

Object-Oriented Principles

  • Encapsulation: Group data (e.g., piece shape, color) and methods (e.g., rotate) into classes (e.g., Tetromino).
  • Abstraction: Hide the complex details of the implementation from the user. For example, the user only needs to know how to move and rotate the piece, not how the collision detection works internally.
  • Polymorphism: Use inheritance to create different types of Tetrominoes, each with its own shape and behavior. This is useful for adding special pieces or game modes.

By applying these principles, we can create a well-structured and maintainable Tetris game.