RT,这是我最近的一道北美CS面试题,不同于传统的 LC 类型算法题,这类 Object-Oriented Design 的问题更侧重于 Object 的设计以及娴熟的代码能力。我准备的时候觉得非常有意思,于是用本文记录下来。
目录
1. 明确需求
设计一个传统的贪吃蛇游戏,规则如下:
- 游戏背景为2D平面地图,四周有边界限制。
- 玩家将控制一条蛇,每一次可以选择向上下左右四个方向移动。
- 游戏会随机在空白位置上刷新一个苹果,如果蛇头吃到苹果,则蛇的长度 + 1,同时玩家得分 + 1。
- 如果蛇头触碰到蛇身,或者蛇头触碰到边界,则游戏结束。理论上还有一个要求是,当图内不存在空白格子时,游戏也结束,玩家胜利,不过我没有实现这一步。
2. 设计骨架
明确基本要求之后,就可以设计基本的 Object 骨架了。
首先对于地图,很明显用一个 2-D list 表示即可,每个点对应一个格子。
接着是🐍,蛇将会由很多个格子组成,而且我们需要在蛇头的位置进行插入操作(当蛇吃到苹果时),所以应该用Python 的双向队列 Deque,这样可以实现O(1)的头部插入时间。
对于苹果,我们需要的功能是,在已有的空白(在图内且不是蛇的一部分)格子里,随机选择一个格子生成苹果。因此我们需要一个集合,来储存所有的空白格子。这样生成苹果的时候利用 random 库随机选一个点就好了。
空白格子集合还有一个好处,当我们移动蛇头的时候,可以快速判断出来游戏是否结束。
所以我们可以得到以下代码:
import random
class snakeGame():
def __init__(self, width=8, height=8) -> None:
# initilize all fields
self.width = width
self.height = height
self.score = 0
self.direction2change = {"UP": (-1, 0), "DOWN": (1, 0),
"LEFT": (0, -1), "RIGHT": (0, 1)}
self.snake = self._init_snake()
# Initialize all spots as free
self.free_spots = {(x, y) for x in range(width) for y in range(height)}
self.free_spots.remove(self.snake[0])
self.food = self._init_food()
def _init_snake(self,):
return deque([(self.width // 2, self.height // 2)])
def _init_food(self,):
self.food = random.sample(self.free_spots, 1)[0]
self.free_spots.remove(self.food)
return self.food
3. 实现逻辑
首先是接收用户从命令行的输入,这部分比较简单,不再赘述。
def _get_user_input(self,):
"""
get the next movement direction from user input
"""
UP_SET = {"UP", "u", "U", "up"}
DOWN_SET = {"DOWN", "d", "D", "down"}
LEFT_SET = {"LEFT", "l", "L", "left"}
RIGHT_SET = {"RIGHT", "r", "R", "right"}
while 1:
dir = input(
"Please select the next movement direction from U, D, L and R:")
# check invalid input
if dir in UP_SET:
return "UP"
elif dir in DOWN_SET:
return "DOWN"
elif dir in LEFT_SET:
return "LEFT"
elif dir in RIGHT_SET:
return "RIGHT"
print("Sorry, your input is not VALID. Please try to input again!")
接着是游戏的主体逻辑,即蛇的移动。每次移动之前需要用户先选择下一次移动的方向。
def play(self,):
self._display_instructions()
while 1:
self._display_board()
# make a movement
direction = self._get_user_input()
接着是移动蛇。注意蛇移动一步的话,实际上只需要蛇头和蛇尾移动即可,中间的身体部分都可以保持不变,因为由后一个格子补上。
head = self.snake[0]
tail = self.snake[-1]
new_head = (head[0] + self.direction2change[direction][0],
head[1] + self.direction2change[direction][1])
if not self._eats_apple(new_head):
# move the tail first and then the head
else:
# grow when snake eats the apple
对于吃没吃苹果,判定很简单:
def _eats_apple(self, head):
return head == self.food
如果没吃到苹果,我们需要先移动蛇尾,再移动蛇头。移动的过程中需要同时考虑:
a. 储存蛇的的队列,让蛇尾出队,新的蛇头入队
b. 空白格子的集合,蛇尾入集合,蛇头出集合
还要考虑游戏是否结束:如果蛇头触碰到蛇身,或者蛇头触碰到边界,则游戏结束。
if not self._eats_apple(new_head):
# move the tail first and then the head
self.free_spots.add(tail)
self.snake.pop()
self.snake.appendleft(new_head)
# check if end
if self._end_game(new_head):
print("The game has ended, and your final score is:", self.score)
break
self.free_spots.remove(new_head)
判定游戏结束的逻辑比较简单:
def _end_game(self, head):
"""
if the snake hits any wall or its body, then the game will end
"""
return head not in self.free_spots
当蛇吃到苹果时,蛇的长度加一,相当于之前苹果的位置变成了新的蛇头。
if not self._eats_apple(new_head):
# move the tail first and then the head
else:
# grow when snake eats the apple
self.snake.appendleft(new_head)
self.score += 1
self._init_food()
self._display_score()
所以整体主体逻辑为:
def play(self,):
self._display_instructions()
while 1:
self._display_board()
# make a movement
direction = self._get_user_input()
head = self.snake[0]
tail = self.snake[-1]
new_head = (head[0] + self.direction2change[direction][0],
head[1] + self.direction2change[direction][1])
if not self._eats_apple(new_head):
# move the tail first and then the head
self.free_spots.add(tail)
self.snake.pop()
self.snake.appendleft(new_head)
# check if end
if self._end_game(new_head):
print("The game has ended, and your final score is:", self.score)
break
self.free_spots.remove(new_head)
else:
# grow when snake eats the apple
self.snake.appendleft(new_head)
self.score += 1
self._init_food()
self._display_score()
然后为了让游戏可玩,我们还需要实现一个打印地图的 method。
定义如下:
- 地图内空白点由 “O” 表示
- 地图四周的边界由 “X” 表示
- 蛇头由 “H” 表示
- 蛇身由 “S” 表示
def _display_board(self,):
"""print the current status of the board, including snake and food
"""
print("The game board status is as follow:")
print("X" * self.width + "XX")
for i in range(self.height):
print("X", end="")
for j in range(self.width):
if (i, j) == self.food:
print("A", end="")
elif (i, j) in self.free_spots:
print("O", end="")
elif (i, j) == self.snake[0]:
print("H", end="")
else:
print("S", end="")
print("X")
print("X" * self.width + "XX")
4. 完整代码
import random
from collections import deque
class snakeGame():
def __init__(self, width=8, height=8) -> None:
# initilize all fields
self.width = width
self.height = height
self.score = 0
self.direction2change = {"UP": (-1, 0), "DOWN": (1, 0),
"LEFT": (0, -1), "RIGHT": (0, 1)}
self.snake = self._init_snake()
# Initialize all spots as free
self.free_spots = {(x, y) for x in range(width) for y in range(height)}
self.free_spots.remove(self.snake[0])
self.food = self._init_food()
def _init_snake(self,):
return deque([(self.width // 2, self.height // 2)])
def _init_food(self,):
self.food = random.sample(self.free_spots, 1)[0]
self.free_spots.remove(self.food)
return self.food
def _end_game(self, head):
"""
if the snake hits any wall or its body, then the game will end
"""
return head not in self.free_spots
def _eats_apple(self, head):
return head == self.food
def _display_board(self,):
"""print the current status of the board, including snake and food
"""
print("The game board status is as follow:")
print("X" * self.width + "XX")
for i in range(self.height):
print("X", end="")
for j in range(self.width):
if (i, j) == self.food:
print("A", end="")
elif (i, j) in self.free_spots:
print("O", end="")
elif (i, j) == self.snake[0]:
print("H", end="")
else:
print("S", end="")
print("X")
print("X" * self.width + "XX")
def _display_score(self,):
"""display current score
"""
print("Your current score is:", self.score)
def _display_instructions(self):
print()
def play(self,):
self._display_instructions()
while 1:
self._display_board()
# make a movement
direction = self._get_user_input()
head = self.snake[0]
tail = self.snake[-1]
new_head = (head[0] + self.direction2change[direction][0],
head[1] + self.direction2change[direction][1])
if not self._eats_apple(new_head):
# move the tail first and then the head
self.free_spots.add(tail)
self.snake.pop()
self.snake.appendleft(new_head)
# check if end
if self._end_game(new_head):
print("The game has ended, and your final score is:", self.score)
break
self.free_spots.remove(new_head)
else:
# grow when snake eats the apple
self.snake.appendleft(new_head)
self.score += 1
self._init_food()
self._display_score()
def _get_user_input(self,):
"""
get the next movement direction from user input
"""
UP_SET = {"UP", "u", "U", "up"}
DOWN_SET = {"DOWN", "d", "D", "down"}
LEFT_SET = {"LEFT", "l", "L", "left"}
RIGHT_SET = {"RIGHT", "r", "R", "right"}
while 1:
dir = input(
"Please select the next movement direction from U, D, L and R:")
# check invalid input
if dir in UP_SET:
return "UP"
elif dir in DOWN_SET:
return "DOWN"
elif dir in LEFT_SET:
return "LEFT"
elif dir in RIGHT_SET:
return "RIGHT"
print("Sorry, your input is not VALID. Please try to input again!")
game = snakeGame()
game.play()
5. 游戏Demo
以下 GIF 由 LICECap 录制。