Leetcode 353. Design Snake Game

#Array #Hash Table #Design #Queue #Simulation

Table of Contents

Problem Informations

  • Problem Index: 353
  • Problem Link: Design Snake Game
  • Topics: Array, Hash Table, Design, Queue, Simulation

Problem Description

Design a Snake game that is played on a device with screen size height x width. Play the game online if you are not familiar with the game.

The snake is initially positioned at the top left corner $(0, 0)$ with a length of 1 unit.

You are given an array food where food[i] = (r_i, c_i) is the row and column position of a piece of food that the snake can eat. When a snake eats a piece of food, its length and the game’s score both increase by 1.

Each piece of food appears one by one on the screen, meaning the second piece of food will not appear until the snake eats the first piece of food.

When a piece of food appears on the screen, it is guaranteed that it will not appear on a block occupied by the snake.

The game is over if the snake goes out of bounds (hits a wall) or if its head occupies a space that its body occupies after moving (i.e. a snake of length 4 cannot run into itself).

Implement the SnakeGame class:

  • SnakeGame(int width, int height, int[][] food): Initializes the object with a screen of size height x width and the positions of the food.
  • int move(String direction): Returns the score of the game after applying one direction move by the snake. If the game is over, return -1.

Example 1:

Snake Game

Input

["SnakeGame", "move", "move", "move", "move", "move", "move"]
[[3, 2, [[1, 2], [0, 1]]], ["R"], ["D"], ["R"], ["U"], ["L"], ["U"]]

Output

[null, 0, 0, 1, 1, 2, -1]

Explanation

SnakeGame snakeGame = new SnakeGame(3, 2, [[1, 2], [0, 1]]);
snakeGame.move("R"); // return 0
snakeGame.move("D"); // return 0
snakeGame.move("R"); // return 1, snake eats the first piece of food. The second piece of food appears at (0, 1).
snakeGame.move("U"); // return 1
snakeGame.move("L"); // return 2, snake eats the second food. No more food appears.
snakeGame.move("U"); // return -1, game over because snake collides with border

Constraints:

  • $1 \leq \text{width}, \text{height} \leq 10^4$
  • $1 \leq \text{food.length} \leq 50$
  • $\text{food}[i].\text{length} == 2$
  • $0 \leq r_i < \text{height}$
  • $0 \leq c_i < \text{width}$
  • $\text{direction.length} == 1$
  • Direction is 'U', 'D', 'L', or 'R'.
  • At most $10^4$ calls will be made to move.

Intuition

The goal is to simulate the classic Snake game, where the snake moves around the screen, growing longer by consuming food and avoiding collisions with walls or itself. The game requires handling the dynamic growth of the snake and the appearance of new food items, ensuring real-time updates of the snake’s position and condition (whether it is alive or dead). The constraints highlight the importance of efficient querying and updating of the snake’s position.

Approach

The solution involves using a combination of a grid to represent the game screen, a deque to efficiently manage the dynamic body of the snake, and a queue to sequentially manage the food items.

  1. Initialization:

    • A grid, initialized to zero values, represents the game space, where 0 indicates an empty space, 1 indicates parts of the snake, and 2 represents food.
    • The deque snakeDeque is used to store the segments of the snake, with the head being at the front.
    • A queue foodQueue is initialized with the given food positions to keep track of the remaining food in the order they appear.
    • The initial position of the snake is set at the top-left corner, $(0, 0)$, of the grid, and its presence is marked on the grid.
  2. Processing Moves:

    • For each move, the snake’s head position is updated based on the direction specified (‘U’, ‘D’, ‘L’, ‘R’). The next position is calculated accordingly.
    • Boundary and Self-Collision Check: If the new head position goes out of the grid bounds or into a segment of its own body, the game ends with a score of -1.
    • Food Consumption: If the new head position contains food (grid value 2), the snake’s score increases by 1, and the food item is removed from the grid. If there are more food items, the next one in the queue is placed on the grid.
    • Snake Movement: If no food is at the next position, the tail segment of the snake is removed (to simulate movement), and its location is cleared from the grid.
    • The snake’s new head position is then added to the grid (now occupied).
  3. Returning the Score:

    • After processing each move, the current score (potentially adjusted by food consumption) is returned.

This approach efficiently maintains updates and checks in constant time for each move, leveraging the properties of a deque for dynamic length adjustment and queues for sequential access to food data.

Code

C++

class SnakeGame {
private:
    // Grid
    // 0: empty space
    // 1: snake
    // 2: food
    int width, height, score;
    vector<vector<int>> grid;
    deque<pair<int, int>> snakeDeque; // Deque to hold snake segments (head is front)
    queue<pair<int, int>> foodQueue;  // Queue to hold food positions

public:
    // Constructor to initialize the game with given width, height, and food positions
    SnakeGame(int width, int height, vector<vector<int>>& food) {
        this->width = width;
        this->height = height;
        score = 0;
        grid.resize(height, vector<int>(width, 0)); // Initialize grid with all zeros

        // Load the initial food positions into the queue
        for (vector<int>& f : food) {
            foodQueue.push({f[0], f[1]});
        }

        // Place the initial part of the snake on the grid
        grid[0][0] = 1;
        snakeDeque.push_front({0, 0});

        // Place the first piece of food on the grid
        if (!foodQueue.empty()) {
            auto f = foodQueue.front();
            foodQueue.pop();
            grid[f.first][f.second] = 2;
        }
    }
    
    // Method to move the snake in a given direction
    int move(string direction) {
        // Calculate the next position based on the current direction
        auto currentPosition = snakeDeque.front();
        auto nextPosition = currentPosition;

        if (direction == "U") {
            nextPosition.first--;
        } else if (direction == "D") {
            nextPosition.first++;
        } else if (direction == "L") {
            nextPosition.second--;
        } else {
            nextPosition.second++;
        }

        // Check for wall collisions
        if (nextPosition.first < 0 || nextPosition.first >= height ||
            nextPosition.second < 0 || nextPosition.second >= width) {
            return -1; // Game over
        }

        // Determine if the snake has found food
        bool foundFood = (grid[nextPosition.first][nextPosition.second] == 2);

        // If no food is found, snake moves forward: remove tail segment
        if (!foundFood) {
            auto tail = snakeDeque.back();
            grid[tail.first][tail.second] = 0;
            snakeDeque.pop_back();
        }

        // Check for self-collision
        if (grid[nextPosition.first][nextPosition.second] == 1) {
            return -1; // Game over
        }

        // Place new head position on the grid
        snakeDeque.push_front(nextPosition);
        grid[nextPosition.first][nextPosition.second] = 1;

        // If food was found, increase score and place new food if available
        if (foundFood) {
            score++;
            if (!foodQueue.empty()) {
                auto newFoodPosition = foodQueue.front();
                foodQueue.pop();
                grid[newFoodPosition.first][newFoodPosition.second] = 2;
            }
        }

        return score; // Return current score
    }
};

Python

from collections import deque
from queue import Queue

class SnakeGame:
    # Constructor to initialize the game with given width, height, and food positions
    def __init__(self, width, height, food):
        self.width = width
        self.height = height
        self.score = 0
        self.grid = [[0] * width for _ in range(height)]  # Initialize grid with all zeros

        # Load the initial food positions into the queue
        self.foodQueue = Queue()
        for f in food:
            self.foodQueue.put((f[0], f[1]))

        # Place the initial part of the snake on the grid
        self.grid[0][0] = 1
        self.snakeDeque = deque([(0, 0)])

        # Place the first piece of food on the grid
        if not self.foodQueue.empty():
            f = self.foodQueue.get()
            self.grid[f[0]][f[1]] = 2

    # Method to move the snake in a given direction
    def move(self, direction):
        # Calculate the next position based on the current direction
        currentPosition = self.snakeDeque[0]
        nextPosition = list(currentPosition)

        if direction == "U":
            nextPosition[0] -= 1
        elif direction == "D":
            nextPosition[0] += 1
        elif direction == "L":
            nextPosition[1] -= 1
        else:
            nextPosition[1] += 1

        # Check for wall collisions
        if nextPosition[0] < 0 or nextPosition[0] >= self.height or nextPosition[1] < 0 or nextPosition[1] >= self.width:
            return -1  # Game over

        # Determine if the snake has found food
        foundFood = (self.grid[nextPosition[0]][nextPosition[1]] == 2)

        # If no food is found, snake moves forward: remove tail segment
        if not foundFood:
            tail = self.snakeDeque.pop()
            self.grid[tail[0]][tail[1]] = 0

        # Check for self-collision
        if self.grid[nextPosition[0]][nextPosition[1]] == 1:
            return -1  # Game over

        # Place new head position on the grid
        self.snakeDeque.appendleft(tuple(nextPosition))
        self.grid[nextPosition[0]][nextPosition[1]] = 1

        # If food was found, increase score and place new food if available
        if foundFood:
            self.score += 1
            if not self.foodQueue.empty():
                newFoodPosition = self.foodQueue.get()
                self.grid[newFoodPosition[0]][newFoodPosition[1]] = 2

        return self.score  # Return current score

Complexity

  • Time complexity: $O(1)$

    The move function in the SnakeGame class involves updating the position of the snake’s head based on the direction, checking for collisions, and possibly updating the food position. Each of these operations — checking the grid for collision, updating the grid for the snake’s new position, and handling the food if consumed — all occur in constant time, $O(1)$, because they involve fixed operations on elements of the grid or the snake’s body, which are managed efficiently by the deque. The movement does not depend on the size of the grid or the number of food items since it always checks only a constant number of positions.

  • Space complexity: $O(w \cdot h)$

    The space complexity is determined by the size of the grid, which is maintained in memory to mark the snake’s position and food. The grid is of size $w \cdot h$, where $w$ is the width and $h$ is the height of the game area. Additionally, a deque is used to store the snake’s segments, which in the worst-case scenario, can store most of the cells, but it is still bounded by the grid’s size. Hence, the primary space complexity contribution is from the grid itself, which is $O(w \cdot h)$.

Disclaimer: All reference materials on this website are sourced from the internet and are intended for learning purposes only. If you believe any content infringes upon your rights, please contact me at csnote.cc@gmail.com, and I will remove the relevant content promptly.


Feedback Welcome: If you notice any errors or areas for improvement in the articles, I warmly welcome your feedback and corrections. Your input will help this blog provide better learning resources. This is an ongoing process of learning and improvement, and your suggestions are valuable to me. You can reach me at csnote.cc@gmail.com.