Python 实现命令行界面的贪吃蛇小游戏教程(OOD)

RT,这是我最近的一道北美CS面试题,不同于传统的 LC 类型算法题,这类 Object-Oriented Design 的问题更侧重于 Object 的设计以及娴熟的代码能力。我准备的时候觉得非常有意思,于是用本文记录下来。

 

目录

1. 明确需求

2. 设计骨架

3. 实现逻辑

4. 完整代码

5. 游戏Demo


1. 明确需求

设计一个传统的贪吃蛇游戏,规则如下:

  1. 游戏背景为2D平面地图,四周有边界限制。
  2. 玩家将控制一条蛇,每一次可以选择向上下左右四个方向移动。
  3. 游戏会随机在空白位置上刷新一个苹果,如果蛇头吃到苹果,则蛇的长度 + 1,同时玩家得分 + 1。
  4. 如果蛇头触碰到蛇身,或者蛇头触碰到边界,则游戏结束。理论上还有一个要求是,当图内不存在空白格子时,游戏也结束,玩家胜利,不过我没有实现这一步。

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。

定义如下:

  1. 地图内空白点由 “O” 表示
  2. 地图四周的边界由 “X” 表示
  3. 蛇头由 “H” 表示
  4. 蛇身由 “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 录制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值