Before attempting this problem, you should be comfortable with:
The snake game requires tracking the snake's body as it moves and grows. A deque (double-ended queue) is perfect because we add to the front (new head position) and remove from the back (tail moves forward). To quickly check whether the snake bites itself, we also maintain a hash set of all occupied cells. When the snake eats food, the tail stays in place so the snake grows. The score equals the number of food items eaten, which is the snake length minus 1.
(0, 0). Add this position to a hash set. Store the grid dimensions, food list, and a food index starting at 0. Define movement deltas for U, D, L, R.-1.-1.1).class SnakeGame:
def __init__(self, width: int, height: int, food: List[List[int]]):
self.snake = collections.deque([(0,0)]) # snake head is at the front
self.snake_set = {(0,0) : 1}
self.width = width
self.height = height
self.food = food
self.food_index = 0
self.movement = {'U': [-1, 0], 'L': [0, -1], 'R': [0, 1], 'D': [1, 0]}
def move(self, direction: str) -> int:
newHead = (self.snake[0][0] + self.movement[direction][0],
self.snake[0][1] + self.movement[direction][1])
# Boundary conditions.
crosses_boundary1 = newHead[0] < 0 or newHead[0] >= self.height
crosses_boundary2 = newHead[1] < 0 or newHead[1] >= self.width
# Checking if the snake bites itself.
bites_itself = newHead in self.snake_set and newHead != self.snake[-1]
# If any of the terminal conditions are satisfied, then we exit with rcode -1.
if crosses_boundary1 or crosses_boundary2 or bites_itself:
return -1
# Note the food list could be empty at this point.
next_food_item = self.food[self.food_index] if self.food_index < len(self.food) else None
# If there's an available food item and it is on the cell occupied by the snake after the move, eat it
if self.food_index < len(self.food) and \
next_food_item[0] == newHead[0] and \
next_food_item[1] == newHead[1]: # eat food
self.food_index += 1
else: # not eating food: delete tail
tail = self.snake.pop()
del self.snake_set[tail]
# A new head always gets added
self.snake.appendleft(newHead)
# Also add the head to the set
self.snake_set[newHead] = 1
return len(self.snake) - 1Time Complexity:
move function is .bites_itself is constant since we are using a dictionary to search for the element.Space Complexity:
food data structure.snake and the snake_set data structures. At most, we can have snake that occupies all the cells of the grid.Where represents the width of the grid, represents the height of the grid, and represents the number of food items in the list.
The grid is defined as width x height, but positions are given as [row, col] where row corresponds to height and column corresponds to width. Mixing these up causes incorrect boundary checks.
# Wrong: swapped width/height in boundary check
crosses_boundary1 = newHead[0] < 0 or newHead[0] >= self.width # Should be height
crosses_boundary2 = newHead[1] < 0 or newHead[1] >= self.height # Should be width
# Correct
crosses_boundary1 = newHead[0] < 0 or newHead[0] >= self.height # row vs height
crosses_boundary2 = newHead[1] < 0 or newHead[1] >= self.width # col vs widthWhen the snake moves without eating, the tail vacates its position. The new head can legally occupy the old tail position. Failing to exclude the tail from collision detection causes false game-overs.
# Wrong: doesn't account for tail moving
bites_itself = newHead in self.snake_set
# Correct: tail position will be vacated (unless eating)
bites_itself = newHead in self.snake_set and newHead != self.snake[-1]When eating food, you must not remove the tail (so the snake grows). When not eating, you must remove the tail before adding the new head. Getting this order wrong causes the snake to grow incorrectly or the set to become inconsistent.
# Wrong: always removes tail, then checks food
tail = self.snake.pop()
del self.snake_set[tail]
if is_food:
# Too late - tail already removed!
pass
# Correct: check food first, only remove tail if not eating
if is_food:
self.food_index += 1
else:
tail = self.snake.pop()
del self.snake_set[tail]
self.snake.appendleft(newHead)
self.snake_set[newHead] = 1