之前一直觉得 Python 只是用来写爬虫、处理表格、做数据分析的工具,直到在公司内部看到一个同事用几十行代码写出了一个人狗追逐的小游戏,才发现原来 Python 做游戏开发也能这么有意思。那种“狗越追越近、人拼命逃跑”的紧张感,配合终端里不断刷新的地图,居然真的能让人玩上瘾。本文就从零开始拆解这个“人狗大作战”小游戏的完整开发流程,包含游戏逻辑设计、核心代码实现、AI 追踪算法、异常处理、打包成 exe 的完整步骤,以及新手最容易踩的坑。
如果你是刚开始学 Python 的初学者,这篇文章能帮你把random、列表、循环、函数这些零散知识点串起来;如果你已经有一些 Python 基础,也可以重点看第 3 节的敌人 AI 寻路逻辑和第 6 节的代码工程化优化,这部分对后续做更复杂的游戏项目非常有帮助。
1. 背景与核心概念
1.1 这个游戏到底在玩什么
“人狗大作战”本质上是一个回合制追逐游戏。玩家控制“人”在地图上移动,电脑控制“狗”自动追踪玩家。玩家需要在规定回合内到达安全出口,或者坚持足够多的回合不被狗追上;而狗每回合都会根据玩家的位置向玩家靠近一步,直到抓住玩家。
听起来简单,但真正实现起来,你至少要处理下面几个核心问题:
- 地图如何用代码表示?
- 玩家和狗的初始位置如何生成?
- 玩家如何输入移动指令?
- 狗如何自动追踪玩家?
- 游戏胜负条件如何判断?
- 玩家输入非法字符时怎么办?
这些问题分别对应 Python 中的列表操作、随机数、条件判断、循环、函数封装和异常处理,正好覆盖了 Python 入门阶段最重要的知识点。这也是为什么这个游戏非常适合用来提升对 Python 的兴趣——知识点没有一个是孤立的,全部服务于“做一个能玩的东西”。
1.2 为什么推荐用 Python 做小游戏
很多人的第一反应是:做游戏不是应该用 Unity 或者 Godot 吗?为什么要用 Python?
其实 Python 在游戏开发中有自己的独特定位。对于学习阶段来说,Python 的语法接近自然语言,不需要理解复杂的指针、内存管理等底层概念,可以让你把注意力集中在“游戏逻辑”本身。更重要的是,Python 社区有非常丰富的游戏开发库,比如pygame可以做图形界面游戏,turtle可以做简单的绘图动画,而本文先不引入任何第三方库,只用 Python 自带的random库和标准输入输出,就能实现一个终端版本的追逐游戏。
这样做有两个好处:
- 环境门槛低,只要有 Python 环境就能跑。
- 代码可读性强,每一行都能看懂,适合逐行分析和修改。
当你理解了终端版本的实现思路后,后续切换到pygame做 2D 图形版,会发现逻辑几乎可以复用,只是把“输出字符地图”换成“绘制图片方块”而已。
1.3 游戏的基本规则设计
在设计代码之前,先把规则写清楚。这里的规则不需要多复杂,但必须明确,否则后续写代码时容易逻辑混乱。
本游戏设定的基本规则如下:
- 地图大小为 5×5 网格。
- 玩家用字符
P表示,狗用D表示,出口用E表示。 - 玩家先行动,每回合可以向上、下、左、右四个方向移动一格。
- 玩家移动后,狗根据玩家位置自动移动一格。
- 如果狗移动后和玩家位于同一格,游戏失败。
- 如果玩家到达出口,游戏胜利。
- 如果玩家坚持 15 回合仍未失败,也判定胜利。
这个规则可以随时调整。比如把地图扩大、增加狗的数量、增加障碍物,这些都是游戏设计层面的扩展,不影响核心代码框架。
2. 环境准备与版本说明
2.1 安装 Python
本游戏不依赖任何第三方库,只需要 Python 3.6 及以上版本即可。如果你还没有安装 Python,可以参考下面的步骤。
Windows 系统:
- 打开 Python 官网下载页面。
- 下载 Windows 安装包,建议选择 64 位版本。
- 安装时务必勾选“Add Python to PATH”。
- 安装完成后,打开命令行,输入
python --version验证。
macOS 系统:
python3 --versionLinux 系统(以 Ubuntu/Debian 为例):
sudo apt update sudo apt install python3验证安装:
python --version如果输出类似下面的内容,说明安装成功:
Python 3.10.122.2 IDE 或编辑器推荐
写这种小游戏,用轻量级编辑器就够了。常用的选择有:
- VS Code:免费,插件丰富,支持 Python 调试,配置好 Python 环境后非常顺手。
- PyCharm:JetBrains 出品,功能强大,社区版免费,适合长期学习 Python。
- 终端 + 系统自带文本编辑器:最轻量,但不推荐新手使用,因为缺少语法高亮和错误提示。
本教程的代码不依赖 IDE 特性,用任何编辑器都可以。重点是代码本身。
2.3 项目结构说明
为了后续扩展方便,项目文件建议按下述结构组织:
dog_game/ ├── main.py # 游戏主程序 ├── game_functions.py # 游戏核心函数(可选拆分) └── README.md # 项目说明本文为了演示方便,先使用单个main.py文件。当代码量增大后,再拆分成多个模块。
3. 核心语法与关键实现思路
在写完整代码之前,需要把几个核心知识点单独拎出来讲清楚。
3.1 二维列表表示地图
地图本质是一个二维网格。在 Python 中,最自然的表示方式是“列表的列表”,也就是二维列表。
map_data = [ [' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' '] ]每个位置可以用map_data[row][col]访问。这里的row是行号(从 0 开始),col是列号(从 0 开始)。
为什么不用一维列表?
因为二维列表在逻辑上更贴近地图的物理结构。当玩家输入“向上移动”时,我们只需要把玩家的行号减 1;输入“向右移动”时,把列号加 1。这种坐标操作非常直观。
# 玩家当前位置 player_row = 2 player_col = 2 # 向上移动 player_row -= 1 # 向右移动 player_col += 1但需要注意,直接对玩家坐标加减后,必须判断新坐标是否越界。比如玩家已经在第 0 行,再向上移动就会变成 -1,这是非法的。
3.2 random 随机数生成初始位置
玩家和狗的初始位置不应该写死,否则每次游戏都一样,乐趣也会减少。这时使用random模块生成随机坐标。
import random player_row = random.randint(0, 4) player_col = random.randint(0, 4) dog_row = random.randint(0, 4) dog_col = random.randint(0, 4)但这里有一个问题:玩家和狗的初始位置可能会相同,或者狗正好出生在出口位置。这样游戏一开始就结束了,体验不好。
所以在生成坐标后,需要加一层判断:如果玩家位置、狗位置、出口位置三者有重叠,就重新生成。
def generate_position(exclude_positions): while True: row = random.randint(0, 4) col = random.randint(0, 4) if (row, col) not in exclude_positions: return row, col这个方法的核心思路是:把已经占用的位置传给函数,函数在生成随机坐标后检查是否冲突,如果冲突就重新生成。这是一个非常通用的“避免随机冲突”方案,在后续开发中会经常用到。
3.3 玩家输入处理与边界检查
玩家输入通常使用input()函数。我们需要约定输入指令的含义:
w或W:向上s或S:向下a或A:向左d或D:向右q或Q:退出游戏
拿到输入后,先统一转换为小写,再判断方向。与此同时,还要检查移动后是否越界:
move = input("请输入移动方向 (w/a/s/d): ").lower() new_row, new_col = player_row, player_col if move == 'w': new_row -= 1 elif move == 's': new_row += 1 elif move == 'a': new_col -= 1 elif move == 'd': new_col += 1 elif move == 'q': print("游戏已退出") break else: print("无效输入,请重新输入") continue # 判断是否越界 if new_row < 0 or new_row >= 5 or new_col < 0 or new_col >= 5: print("无法移动到边界外,请重新输入") continue这个逻辑的要点是:先计算出“如果移动,新位置是什么”,再统一判断合法性,而不是直接修改玩家坐标。这样可以避免移动到非法位置后还需要回滚的麻烦。
3.4 狗 AI 的最简实现:曼哈顿距离追踪
狗要怎么追踪玩家?最简答的方式是“曼哈顿距离”追踪,也就是分别比较行号和列号,向玩家所在方向靠近一步。
具体逻辑:
# 如果狗的行号小于玩家的行号,狗向下移动 if dog_row < player_row: dog_row += 1 # 如果狗的行号大于玩家的行号,狗向上移动 elif dog_row > player_row: dog_row -= 1 # 行号相同,则尝试在列方向上靠近 else: if dog_col < player_col: dog_col += 1 elif dog_col > player_col: dog_col -= 1这个逻辑实现的就是“先走行方向对齐,再走列方向对齐”。理解起来非常简单,而且效果已经足够好,狗会以最短的直线路径向玩家逼近。
如果想增加游戏难度,可以加入“狗每次移动时有一定概率走斜线”的逻辑。不过二维网格中“斜线”需要同时改变行和列,这里先不展开。
3.5 游戏主循环结构
终端游戏本质上是一个“渲染-输入-更新-渲染”的循环。这个循环会一直执行,直到游戏结束。
while True: # 1. 绘制当前地图 # 2. 检查游戏状态 # 3. 获取玩家输入 # 4. 更新玩家位置 # 5. 更新狗位置 # 6. 检查是否结束游戏循环的写法非常固定,关键在于顺序不能乱。如果先更新狗位置再获取玩家输入,玩家就会“慢一步”,游戏难度会变大。本文采用“玩家先动,狗再动”的顺序,保证规则明确。
4. 完整实战案例:人狗大作战
接下来是本文的核心部分,逐步写出完整的游戏代码。请先创建项目文件夹,然后在文件夹中创建main.py文件。
4.1 创建项目结构
mkdir dog_game cd dog_game touch main.pyWindows 下可以使用:
mkdir dog_game cd dog_game type nul > main.py4.2 完整代码
下面是main.py的完整代码。为了方便阅读,先写完整版本,再逐段解释。
import random # 定义地图大小 MAP_SIZE = 5 # 定义最大回合数 MAX_ROUND = 15 def generate_position(occupied_positions): """在地图范围内生成一个未被占用的随机位置""" while True: row = random.randint(0, MAP_SIZE - 1) col = random.randint(0, MAP_SIZE - 1) if (row, col) not in occupied_positions: return row, col def draw_map(player_pos, dog_pos, exit_pos): """根据玩家、狗、出口的位置绘制当前地图""" for row in range(MAP_SIZE): line = "" for col in range(MAP_SIZE): if (row, col) == player_pos: line += "P " elif (row, col) == dog_pos: line += "D " elif (row, col) == exit_pos: line += "E " else: line += ". " print(line) print() def move_player(current_pos, direction): """根据方向移动玩家,返回新位置;如果非法返回原位置""" row, col = current_pos if direction == 'w': row -= 1 elif direction == 's': row += 1 elif direction == 'a': col -= 1 elif direction == 'd': col += 1 else: return current_pos # 检查是否越界 if row < 0 or row >= MAP_SIZE or col < 0 or col >= MAP_SIZE: return current_pos return row, col def move_dog(dog_pos, player_pos): """狗向玩家方向移动一步""" dog_row, dog_col = dog_pos player_row, player_col = player_pos if dog_row < player_row: dog_row += 1 elif dog_row > player_row: dog_row -= 1 elif dog_col < player_col: dog_col += 1 elif dog_col > player_col: dog_col -= 1 return dog_row, dog_col def main(): print("===== 人狗大作战 =====") print("规则:P 代表你,D 代表狗,E 代表出口。") print("使用 W/A/S/D 移动,坚持 15 回合或到达出口即可获胜。") print("输入 Q 退出游戏。\n") occupied_positions = set() # 生成玩家位置 player_pos = generate_position(occupied_positions) occupied_positions.add(player_pos) # 生成出口位置 exit_pos = generate_position(occupied_positions) occupied_positions.add(exit_pos) # 生成狗位置 dog_pos = generate_position(occupied_positions) occupied_positions.add(dog_pos) round_count = 0 while True: draw_map(player_pos, dog_pos, exit_pos) print(f"当前回合: {round_count + 1} / {MAX_ROUND}") # 检查是否到达出口 if player_pos == exit_pos: print("恭喜!你成功到达出口,游戏胜利!") break # 检查回合数 if round_count >= MAX_ROUND: print("你坚持了足够多的回合,游戏胜利!") break # 获取玩家输入 direction = input("请输入移动方向 (w/a/s/d): ").lower() if direction == 'q': print("游戏已退出") break # 玩家移动 new_player_pos = move_player(player_pos, direction) if new_player_pos == player_pos: print("无法移动,请重新输入方向。") continue player_pos = new_player_pos # 玩家到达出口判断 if player_pos == exit_pos: draw_map(player_pos, dog_pos, exit_pos) print("恭喜!你成功到达出口,游戏胜利!") break # 狗移动 dog_pos = move_dog(dog_pos, player_pos) # 检查狗是否抓到玩家 if dog_pos == player_pos: draw_map(player_pos, dog_pos, exit_pos) print("你被狗抓住了!游戏结束。") break round_count += 1 if __name__ == "__main__": main()4.3 代码分段讲解
这段代码看上去不长,但包含了多个重要设计。下面逐个模块讲解。
生成随机位置
generate_position函数接收一个集合参数occupied_positions,集合中存储的是已经被占用的坐标。函数通过while True不断生成随机位置,直到生成一个没有被占用的坐标。
def generate_position(occupied_positions): while True: row = random.randint(0, MAP_SIZE - 1) col = random.randint(0, MAP_SIZE - 1) if (row, col) not in occupied_positions: return row, col这里使用集合而不是列表,是因为集合的in判断效率更高。虽然在这个小地图中体验不出差别,但这是一个好的编程习惯。
绘制地图
draw_map函数使用两层循环遍历地图的每个格子,根据坐标内容决定输出什么字符。
def draw_map(player_pos, dog_pos, exit_pos): for row in range(MAP_SIZE): line = "" for col in range(MAP_SIZE): if (row, col) == player_pos: line += "P " elif (row, col) == dog_pos: line += "D " elif (row, col) == exit_pos: line += "E " else: line += ". " print(line) print()这里有个小技巧:先把一行内容累积到字符串line中,最后统一print(line),而不是每打印一个字符就换行。这样做可以保证每行字符显示在一行中。
玩家移动
move_player函数接收当前坐标和方向,返回新坐标。如果移动越界,则返回原来的坐标。
def move_player(current_pos, direction): row, col = current_pos if direction == 'w': row -= 1 elif direction == 's': row += 1 elif direction == 'a': col -= 1 elif direction == 'd': col += 1 else: return current_pos if row < 0 or row >= MAP_SIZE or col < 0 or col >= MAP_SIZE: return current_pos return row, col狗移动
move_dog函数是游戏 AI 的核心。它的逻辑是:先比较行号,如果狗在玩家上方就向下走,在下方就向上走;只有在行号相同时才比较列号。
def move_dog(dog_pos, player_pos): dog_row, dog_col = dog_pos player_row, player_col = player_pos if dog_row < player_row: dog_row += 1 elif dog_row > player_row: dog_row -= 1 elif dog_col < player_col: dog_col += 1 elif dog_col > player_col: dog_col -= 1 return dog_row, dog_col这种 AI 实现虽然简单,却产生了真实的“追击感”。玩家在逃跑时,需要提前预判狗的位置,否则很容易被逼到角落。
主函数与游戏循环
main()函数负责初始化游戏状态并执行游戏循环。循环内的顺序如下:
- 绘制当前地图。
- 检查玩家是否到达出口。
- 检查回合数是否超出上限。
- 获取玩家输入。
- 更新玩家位置。
- 检查玩家是否到达出口。
- 更新狗位置。
- 检查狗是否抓到玩家。
- 回合数加 1。
4.4 运行与验证
在终端中运行:
python main.py首次运行后,界面会输出类似下面的内容:
===== 人狗大作战 ===== 规则:P 代表你,D 代表狗,E 代表出口。 使用 W/A/S/D 移动,坚持 15 回合或到达出口即可获胜。 输入 Q 退出游戏。 D . . . . . . . P . . . . . . . . E . . . . . . . 当前回合: 1 / 15 请输入移动方向 (w/a/s/d):此时输入s,玩家向下移动,狗也会同步移动。如果运气好,几步内就能到达出口;如果运气差,可能会被狗逼到角落。多玩几局后,你对“坐标变化”和“循环控制”的理解会非常深刻。
4.5 预期结果说明
游戏的胜负分支包括:
- 玩家到达出口:输出“恭喜!你成功到达出口,游戏胜利!”
- 回合数超过 15:输出“你坚持了足够多的回合,游戏胜利!”
- 狗抓住玩家:输出“你被狗抓住了!游戏结束。”
- 玩家主动退出:输出“游戏已退出”
当你运行代码后,可以根据输出判断代码逻辑是否正确。如果发现不管怎么输入都报错,优先检查input().lower()这一步是否拼写正确。
5. 常见问题与排查思路
5.1 常见报错对照表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ModuleNotFoundError: No module named 'random' | 项目里创建了名为random.py的文件 | 删除或重命名 local 文件,避免覆盖标准库 |
ValueError: too many values to unpack (expected 2) | 坐标变量与返回值数量不匹配 | 检查generate_position是否同时返回 row 和 col |
| 输入方向后不移动 | move_player返回原位置,但玩家没有区分“原地不动”和“移动到了原位置” | 在调用处判断new_player_pos == player_pos |
| 游戏直接胜利或直接失败 | 初始位置生成时没有排除出口或狗的位置 | 检查generate_position的冲突排除逻辑 |
| 地图显示错位 | Windows 终端字符宽度问题 | 将字符间隔调整为两个空格或使用英文标点 |
| 运行闪退 | 使用了input()但在 IDE 中运行方式不对 | 在终端命令行中运行,不要直接双击 py 文件 |
5.2 为什么地图一直不更新
一个常见的误区是在draw_map中直接print了地图,但在循环开始前没有清屏。终端输出会一直累积,导致旧地图叠在新地图上。解决方式有两种:
方式一:不清屏,每次输出后空一行。这种方式最简单,代码可读性也最好,适合学习阶段。
方式二:调用系统清屏命令。在 Windows 上是os.system('cls'),在 Linux/macOS 上是os.system('clear')。需要注意跨平台兼容,封装成一个函数:
import os def clear_screen(): os.system('cls' if os.name == 'nt' else 'clear')在游戏循环开始处调用clear_screen()即可。但清屏后用户看不到历史记录,各有优劣。
5.3 狗为什么不追我
如果狗在原地不动,首先检查move_dog函数是否在循环中被调用。常见错误是少写了dog_pos = move_dog(dog_pos, player_pos)这一步,或者把狗移动后没有赋值回dog_pos。
另外要注意,狗移动的方向判断顺序是“先列后行”还是“先行后列”。本文实现的是“先行后列”,如果修改成“先列后行”,狗的行为会有细微差别,但都能实现追踪效果。
5.4 输入 q 无法退出
这个问题多半是因为输入时使用了中文输入法,input()接收到的可能是中文符号。建议在提示信息中明确要求“请切换到英文输入法”。另外,可以在判断前对输入做标准化处理:
direction = input("请输入移动方向 (w/a/s/d): ").strip().lower()加上.strip()可以去除首尾空格,避免用户误打空格导致判断失败。
6. 扩展优化与工程化建议
当核心功能跑通后,就可以考虑扩展了。扩展的方向有很多,下面列几个性价比比较高的优化点。
6.1 增加障碍物
地图全是空白区域,玩久了会觉得单调。可以在地图上随机生成一些障碍物,用字符#表示。玩家和狗都不能穿越障碍物。
具体实现时,在初始位置生成后,再随机选择几个空白位置变成障碍物。玩家移动时判断目标位置是否是障碍物,如果是则无法移动。狗移动时同样需要判断。
这会让游戏策略性大大提升,因为障碍物会改变狗的追击路径。
6.2 增加多只狗
当玩家掌握了基本操作后,一只狗已经不够刺激。可以让游戏支持多只狗,例如把dog_pos从单个坐标变成坐标列表。
dog_positions = [(1, 2), (3, 4), (0, 1)]每回合每只狗依次移动,如果任意一只狗追上玩家,游戏结束。这个扩展需要把move_dog的调用改成循环,并且每只狗在移动时都要判断是否与玩家相遇。
6.3 加入计分和关卡系统
可以设计一个简单的计分系统:玩家每成功存活一回合加 10 分,到达出口额外加 50 分。连续游玩时,分数累计。再配合关卡系统,每过一关地图变大、狗的数量增加。
score = 0 level = 1这样游戏的耐玩性会成倍提升,也能帮助你练习“状态管理”和“模块化设计”。
6.4 打包成 exe
如果想把游戏分享给没有安装 Python 的朋友,可以使用PyInstaller打包成 exe 文件。这个操作对于很多初学者来说非常有成就感。
首先安装 PyInstaller:
pip install pyinstaller然后在项目目录下执行:
pyinstaller -F main.py打包完成后,在dist目录下可以找到main.exe。双击即可运行,无需安装 Python 环境。
需要注意,如果使用了os.system('cls')清屏,打包后的 exe 在部分精简 Windows 环境下可能找不到cls命令,建议打包前把清屏逻辑改成不依赖外部命令的方式。
6.5 代码结构调整建议
当代码量增长到几百行后,建议拆分模块。比如把地图生成、角色移动、AI 逻辑、游戏循环分别放到不同文件中:
dog_game/ ├── main.py # 游戏入口 ├── map.py # 地图生成与绘制 ├── characters.py # 玩家与狗的逻辑 ├── ai.py # 狗 AI 策略 └── config.py # 常量配置模块拆分后,修改 AI 逻辑时不需要动游戏主循环,新增道具时也不需要改动地图生成代码。这是从“脚本”走向“项目”的关键一步。
6.6 安全与健壮性建议
在实际项目中,还需要考虑下面几点:
- 所有用户输入必须做有效性校验,不能让非法输入导致程序崩溃。
- 对随机初始位置做冲突检测,避免角色重叠导致开局即结束。
- 使用
if __name__ == "__main__":保护主逻辑,方便导入模块进行单元测试。 - 写一个简单的测试用例,比如验证狗在同行时能否正确靠近玩家。
7. 总结与下一步学习路线
通过“人狗大作战”这个小项目,你覆盖了 Python 中非常重要的几个知识点:二维列表操作、随机数生成、函数设计与模块化思维、循环控制、输入校验、坐标运算、简单的 AI 算法,以及游戏循环的基本框架。这些知识不仅在游戏开发中有用,在爬虫、数据分析、自动化脚本等领域同样会反复出现。
如果接下来想继续深入学习,建议按以下顺序推进:
- 先试着自己调整游戏参数,比如扩大地图、增加回合数、增加狗的数量,观察游戏行为的变化。
- 把游戏从终端版迁移到
pygame图形版,体会“逻辑层”和“表现层”如何分离。 - 学习 Python 单元测试,给
move_player和move_dog函数写测试用例。 - 尝试加入存档功能,用 JSON 或文本文件保存最高分和游戏进度。
- 学习 Git,把项目托管到远程仓库,管理不同版本的游戏迭代。
这个项目最大的价值在于:每一行代码都是可运行、可验证、可修改的。遇到报错不用慌,按着报错信息回看对应函数,基本都能定位问题。多做几次这样的排错练习,你对 Python 的信心就会完全不同。
如果本文对你有帮助,可以收藏备用,后续我会继续分享从终端游戏到 pygame 图形游戏的升级教程,包括地图编辑、精灵动画、碰撞检测这些更进阶的内容。