当前位置:   article > 正文

353. Design Snake Game

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:

  1. Given width = 3, height = 2, and food = [[1,2],[0,1]].
  2. Snake snake = new Snake(width, height, food);
  3. Initially the snake appears at position (0,0) and the food at (1,2).
  4. |S| | |
  5. | | |F|
  6. snake.move("R"); -> Returns 0
  7. | |S| |
  8. | | |F|
  9. snake.move("D"); -> Returns 0
  10. | | | |
  11. | |S|F|
  12. snake.move("R"); -> Returns 1 (Snake eats the first food and right after that, the second food appears at (0,1) )
  13. | |F| |
  14. | |S|S|
  15. snake.move("U"); -> Returns 1
  16. | |F|S|
  17. | | |S|
  18. snake.move("L"); -> Returns 2 (Snake eats the second food)
  19. | |S|S|
  20. | | |S|
  21. 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
  1. class SnakeGame {
  2. private int width;
  3. private int height;
  4. private int[][] food;
  5. private int foodIndex = 0;
  6. private int score = 0;
  7. private Deque<Position> snake = new ArrayDeque<>();
  8. /** Initialize your data structure here.
  9. @param width - screen width
  10. @param height - screen height
  11. @param food - A list of food positions
  12. E.g food = [[1,1], [1,0]] means the first food is positioned at [1,1], the second is at [1,0]. */
  13. public SnakeGame(int width, int height, int[][] food) {
  14. this.width = width;
  15. this.height = height;
  16. this.food = food;
  17. this.snake.offerFirst(new Position(0, 0));
  18. }
  19. /** Moves the snake.
  20. @param direction - 'U' = Up, 'L' = Left, 'R' = Right, 'D' = Down
  21. @return The game's score after the move. Return -1 if game over.
  22. Game over when snake crosses the screen boundary or bites its body. */
  23. public int move(String direction) {
  24. if (this.score == -1) {
  25. return -1;
  26. }
  27. Position head = this.snake.peekFirst();
  28. Position newHead = new Position(head.x, head.y);
  29. switch (direction) {
  30. case "U":
  31. newHead.x--;
  32. break;
  33. case "D":
  34. newHead.x++;
  35. break;
  36. case "L":
  37. newHead.y--;
  38. break;
  39. default:
  40. newHead.y++;
  41. break;
  42. }
  43. if (newHead.x < 0 || newHead.x == this.height || newHead.y < 0 || newHead.y == this.width) {
  44. return score = -1;
  45. }
  46. Position tail = this.snake.pollLast();
  47. if (this.snake.contains(newHead)) {
  48. return score = -1;
  49. }
  50. this.snake.offerFirst(newHead);
  51. if (this.foodIndex < this.food.length && this.food[this.foodIndex][0] == newHead.x
  52. && this.food[this.foodIndex][1] == newHead.y) {
  53. this.snake.addLast(tail);
  54. this.foodIndex++;
  55. this.score++;
  56. }
  57. return this.score;
  58. }
  59. private class Position {
  60. private int x;
  61. private int y;
  62. public Position(int x, int y) {
  63. this.x = x;
  64. this.y = y;
  65. }
  66. @Override
  67. public boolean equals(Object other) {
  68. if (this == other) {
  69. return true;
  70. }
  71. if (other == null || this.getClass() != other.getClass()) {
  72. return false;
  73. }
  74. Position otherPosition = (Position) other;
  75. return this.x == otherPosition.x && this.y == otherPosition.y;
  76. }
  77. }
  78. }
  79. /**
  80. * Your SnakeGame object will be instantiated and called as such:
  81. * SnakeGame obj = new SnakeGame(width, height, food);
  82. * int param_1 = obj.move(direction);
  83. */

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
  1. class SnakeGame {
  2. private int width;
  3. private int height;
  4. private int[][] food;
  5. private int foodIndex = 0;
  6. private int score = 0;
  7. private Set<Position> snake = new LinkedHashSet<>();
  8. private Position snakeHead;
  9. /** Initialize your data structure here.
  10. @param width - screen width
  11. @param height - screen height
  12. @param food - A list of food positions
  13. E.g food = [[1,1], [1,0]] means the first food is positioned at [1,1], the second is at [1,0]. */
  14. public SnakeGame(int width, int height, int[][] food) {
  15. this.width = width;
  16. this.height = height;
  17. this.food = food;
  18. this.snakeHead = new Position(0, 0);
  19. this.snake.add(this.snakeHead);
  20. }
  21. /** Moves the snake.
  22. @param direction - 'U' = Up, 'L' = Left, 'R' = Right, 'D' = Down
  23. @return The game's score after the move. Return -1 if game over.
  24. Game over when snake crosses the screen boundary or bites its body. */
  25. public int move(String direction) {
  26. if (this.score == -1) {
  27. return -1;
  28. }
  29. Position newHead = new Position(snakeHead.x, snakeHead.y);
  30. switch (direction) {
  31. case "U":
  32. newHead.x--;
  33. break;
  34. case "D":
  35. newHead.x++;
  36. break;
  37. case "L":
  38. newHead.y--;
  39. break;
  40. default:
  41. newHead.y++;
  42. break;
  43. }
  44. if (newHead.x < 0 || newHead.x == this.height || newHead.y < 0 || newHead.y == this.width) {
  45. return score = -1;
  46. }
  47. Iterator<Position> iter = snake.iterator();
  48. Position tail = iter.next();
  49. if (!newHead.equals(tail) && this.snake.contains(newHead)) {
  50. return score = -1;
  51. }
  52. if (this.foodIndex < this.food.length && this.food[this.foodIndex][0] == newHead.x
  53. && this.food[this.foodIndex][1] == newHead.y) {
  54. this.foodIndex++;
  55. this.score++;
  56. } else {
  57. iter.remove();
  58. }
  59. this.snake.add(newHead);
  60. this.snakeHead = newHead;
  61. return this.score;
  62. }
  63. private class Position {
  64. private int x;
  65. private int y;
  66. public Position(int x, int y) {
  67. this.x = x;
  68. this.y = y;
  69. }
  70. @Override
  71. public boolean equals(Object other) {
  72. if (this == other) {
  73. return true;
  74. }
  75. if (other == null || this.getClass() != other.getClass()) {
  76. return false;
  77. }
  78. Position otherPosition = (Position) other;
  79. return this.x == otherPosition.x && this.y == otherPosition.y;
  80. }
  81. @Override
  82. public int hashCode() {
  83. return Objects.hash(this.x, this.y);
  84. }
  85. }
  86. }
  87. /**
  88. * Your SnakeGame object will be instantiated and called as such:
  89. * SnakeGame obj = new SnakeGame(width, height, food);
  90. * int param_1 = obj.move(direction);
  91. */

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/839682
推荐阅读
相关标签
  

闽ICP备14008679号