353. Design Snake Game

353. Design Snake Game

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

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

You are given a list of food's positions in row-column order. When a snake eats the food, its length and the game's score both increase by 1.

Each food appears one by one on the screen. For example, the second food will not appear until the first food was eaten by the snake.

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

Example:

Given width = 3, height = 2, and food = [[1,2],[0,1]].

Snake snake = new Snake(width, height, food);

Initially the snake appears at position (0,0) and the food at (1,2).

|S| | |
| | |F|

snake.move("R"); -> Returns 0

| |S| |
| | |F|

snake.move("D"); -> Returns 0

| | | |
| |S|F|

snake.move("R"); -> Returns 1 (Snake eats the first food and right after that, the second food appears at (0,1) )

| |F| |
| |S|S|

snake.move("U"); -> Returns 1

| |F|S|
| | |S|

snake.move("L"); -> Returns 2 (Snake eats the second food)

| |S|S|
| | |S|

snake.move("U"); -> Returns -1 (Game over because snake collides with border)

State the problem (as you understand it): 

  • Return -1 if the game is over. Compute new head position for the move. Test if the new head of the snake crosses the screen boundary or bites it's body (excluding the tail), if yes return -1. Add the new head to the body of the snake. If the snake eats a food, then increase the game's score and the food index. Otherwise, remove it's tail.

Solve examples that are big enough and are normal cases (manually to figure out the general solution): 

  1. if game is over, return -1
  2. compute new head position for the snake, and check the following edge cases
    1. return -1 if the new position crosses the boundary
    2. return -1 if the new positions reaches the snake body (excluding the tail position)
  3. Add new head to the snake's body
  4. if there's food at the new head positon, keep tail and increment food index and score
  5. if there's no food at the new head position, remove tail
  6. return game escore

Ask clarification questions:

State assumptions:

Consider several test cases (edge cases, base cases):

  • new head position is out of the boundary, return -1
  • new head position reach the snake body, remember to delete the tail first

See if a Data Structure fits the problem:

  1. use a deque to represent the snake
    1. front is the head
    2. the middle is the body
    3. back is the tail
    4. when moving to a new head, push to the front
    5. when removing tail from snake, remove from the back

See if an Algorithm fits the problem:

  1. ad-hoc algorithm

Extract the core problem:

  • use a deque to store positons for the head, body and tail of a snake, and add head and/or remove tail based on movement direction and the presence of food to simualte movement of the snake

Try breaking it down into subproblems:

Explain Potential Solutions (see solutin section):

Other solutions:

  1. use a deque and a set to keep two copies of the positions of the head, body, and tail of the snake. The deque copy is good for updating tail. And the set is good for checking eating body case, since traversing a queue would cost O(n). All you need to do is to check if the new position of the head is in the set (excluding the tail). If it is in, the snake bites itself, and if not, it is safe.

Compare solutions:

  1. Using queue + hash set achieves the same goal of reducing the time complexity of checking eating body case to O(1). However that would require some efforts keeping them consistent, and having two data structures representing the same info would result in some redundancy.

Follow Up (TODO):

  1. In the description it says "When a food does appear on the screen, it is guaranteed that it will not appear on a block occupied by the snake", so no need to check if the food is on the snake! However, how is this really guaranteed? In real snake game applications, I don't think the food positions are known beforehand and passed to the constructor. Also as the food may appear at the same position multiple times, storing all of them may take too much space. A better way would be to generate a new food position randomly when the old food is eaten. How do we do this efficiently and guarantee the new position will not overlap the snake? We could just generate a random position and retry until it not longer overlap the snake, but this could be inefficient if the snake has grown too big. Or maybe we can maintain an array of available coordinates, keep updating it after every move, and utilize the idea in "Insert Delete GetRandom O(1)" to pick one from it randomly, so that it is guaranteed O(1) time.

Solution 1:


Ideas: 

Steps:

  1. create a class Position to represent the positions of the head, body, and tail of the snake
  2. create instance variables 
    1. int width to represent screen width
    2. int height to represenrt screen height
    3. int[][] food to represent positions of foods
    4. int score to represent score
    5. int foodIndex to represent food index
    6. Queue<Position> to represent positions of the head, body, and tail of the snake

Validate the correctness (of your pseudocode with a test case):

Data Structure:

  • use a deque

Time Complexity:

  • O(1)

Space Complexity:

  • O(N), where N represents the size of the snake
class SnakeGame {
    
    private int width;
    private int height;
    
    private int[][] food;
    private int foodIndex = 0;
    
    private int score = 0;
    private Deque<Position> snake = new ArrayDeque<>();

    /** Initialize your data structure here.
        @param width - screen width
        @param height - screen height 
        @param food - A list of food positions
        E.g food = [[1,1], [1,0]] means the first food is positioned at [1,1], the second is at [1,0]. */
    public SnakeGame(int width, int height, int[][] food) {
        this.width = width;
        this.height = height;
        
        this.food = food;
        this.snake.offerFirst(new Position(0, 0));
    }
    
    /** Moves the snake.
        @param direction - 'U' = Up, 'L' = Left, 'R' = Right, 'D' = Down 
        @return The game's score after the move. Return -1 if game over. 
        Game over when snake crosses the screen boundary or bites its body. */
    public int move(String direction) {
        if (this.score == -1) {
            return -1;    
        }
        
        Position head = this.snake.peekFirst();
        Position newHead = new Position(head.x, head.y);
        
        switch (direction) {
            case "U":
                newHead.x--;
                break;
            case "D":
                newHead.x++;
                break;
            case "L":
                newHead.y--;
                break;
            default:
                newHead.y++;
                break;
        }
                
        if (newHead.x < 0 || newHead.x == this.height || newHead.y < 0 || newHead.y == this.width) {
            return score = -1;
        }
        
        Position tail = this.snake.pollLast();
        if (this.snake.contains(newHead)) {
            return score = -1;
        }
        this.snake.offerFirst(newHead);
        
        if (this.foodIndex < this.food.length && this.food[this.foodIndex][0] == newHead.x 
            && this.food[this.foodIndex][1] == newHead.y) {
            this.snake.addLast(tail);
            this.foodIndex++;
            this.score++;
        }
        
        return this.score;
    }
    
    private class Position {
        private int x;
        private int y;
        
        public Position(int x, int y) {
            this.x = x;
            this.y = y;
        }
        
        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || this.getClass() != other.getClass()) {
                return false;
            }
            
            Position otherPosition = (Position) other;
            return this.x == otherPosition.x && this.y == otherPosition.y;
        }
    }
}

/**
 * Your SnakeGame object will be instantiated and called as such:
 * SnakeGame obj = new SnakeGame(width, height, food);
 * int param_1 = obj.move(direction);
 */

Test your code again to make sure you don't have any bugs.

Solution 2:


Ideas: 

  • LinkedHashSet is used here to store the position of snake body. Since it is essentially a combination of hash set and doubly linked list, it is good for both look-up(O(1)) and iteration(O(1) for each step).  One drawback I found with LinkedHashSet is that it doesn't support getting the last element in O(1) time. I deal with it by caching the snake head position.

Steps:

Validate the correctness (of your pseudocode with a test case):

Data Structure:

  • use a LinkedHashSet

Time Complexity:

  • O(1)

Space Complexity:

  • O(N), where N is the size of snake
class SnakeGame {
    
    private int width;
    private int height;
    
    private int[][] food;
    private int foodIndex = 0;
    
    private int score = 0;
    private Set<Position> snake = new LinkedHashSet<>();
    private Position snakeHead;

    /** Initialize your data structure here.
        @param width - screen width
        @param height - screen height 
        @param food - A list of food positions
        E.g food = [[1,1], [1,0]] means the first food is positioned at [1,1], the second is at [1,0]. */
    public SnakeGame(int width, int height, int[][] food) {
        this.width = width;
        this.height = height;
        
        this.food = food;
        this.snakeHead = new Position(0, 0);
        this.snake.add(this.snakeHead);
    }
    
    /** Moves the snake.
        @param direction - 'U' = Up, 'L' = Left, 'R' = Right, 'D' = Down 
        @return The game's score after the move. Return -1 if game over. 
        Game over when snake crosses the screen boundary or bites its body. */
    public int move(String direction) {
        if (this.score == -1) {
            return -1;    
        }
        
        Position newHead = new Position(snakeHead.x, snakeHead.y);
        
        switch (direction) {
            case "U":
                newHead.x--;
                break;
            case "D":
                newHead.x++;
                break;
            case "L":
                newHead.y--;
                break;
            default:
                newHead.y++;
                break;
        }
                
        if (newHead.x < 0 || newHead.x == this.height || newHead.y < 0 || newHead.y == this.width) {
            return score = -1;
        }
        
        Iterator<Position> iter = snake.iterator();
        Position tail = iter.next();
        if (!newHead.equals(tail) && this.snake.contains(newHead)) {
            return score = -1;
        }
        if (this.foodIndex < this.food.length && this.food[this.foodIndex][0] == newHead.x 
            && this.food[this.foodIndex][1] == newHead.y) {
            this.foodIndex++;
            this.score++;
        } else {
            iter.remove();
        }
        
        this.snake.add(newHead);
        this.snakeHead = newHead;
        return this.score;
    }
    
    private class Position {
        private int x;
        private int y;
        
        public Position(int x, int y) {
            this.x = x;
            this.y = y;
        }
        
        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || this.getClass() != other.getClass()) {
                return false;
            }
            
            Position otherPosition = (Position) other;
            return this.x == otherPosition.x && this.y == otherPosition.y;
        }
        
        @Override
        public int hashCode() {
            return Objects.hash(this.x, this.y);
        }
    }
}

/**
 * Your SnakeGame object will be instantiated and called as such:
 * SnakeGame obj = new SnakeGame(width, height, food);
 * int param_1 = obj.move(direction);
 */

Test your code again to make sure you don't have any bugs.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值