用Python做一个文字冒险游戏:从基础语法到完整项目实战

发布时间:2026/9/10 8:16:07
用Python做一个文字冒险游戏:从基础语法到完整项目实战 前几天有个朋友问我Python基础语法学完了但不知道拿它做什么。每天跟着教程敲例子敲完就忘遇到综合问题还是没思路。我给的建议一直没变过做个文字冒险游戏。不是因为它简单而是因为它能把变量、分支、循环、函数、文件读写这些零散知识点全部串起来逼着你把它们真正用一遍。这个项目做完你会发现自己突然能独立写点像样的东西了。文字冒险游戏简单说就是玩家通过输入命令和程序交互程序根据当前场景和玩家的选择推进剧情。它不需要图形界面不需要第三方框架一个Python文件就能跑起来。对初学者来说它是性价比最高的练手项目对有点基础但没做过完整项目的人它是第一次体验“从零到成品”的完整流程哪怕你已经会用Python写爬虫或做数据分析回头做一个小型文字游戏也能让你重新审视程序结构设计这件事。这篇文章从环境搭建开始到核心游戏逻辑到扩展玩法再到打包成exe发给朋友玩最后把我踩过的坑一并列出。全程用我实际测试过的代码和步骤说话你可以照着一步步做也可以直接跳到需要的部分。1. 为什么是文字冒险游戏一个看似简单却能把基础焊死的项目1.1 文字冒险游戏到底是个什么东西文字冒险游戏的核心并不是“文字”而是“状态机”。你维护一个当前状态比如玩家在哪个房间、背包里有什么、某个机关有没有被触发然后不断读取玩家输入的命令根据当前状态决定输出什么文本、把状态更新成什么样。这个流程和很多看起来高大上的系统本质上是同一个模式。我见过不少新手去啃那种几千行的爬虫框架源码看完了还是一头雾水。但文字冒险游戏不一样它的逻辑链条很短短到你可以随时在脑中跑完一遍程序输出当前场景描述玩家输入命令比如 go north、take key、help程序解析命令检查是否合法合法就更新状态并输出新场景不合法就提示错误这个循环从头到尾都发生在你自己写的代码里没有任何黑盒。你能直观感受到每个变量、每个函数在干吗。这种“全程可掌控”的感觉对建立编程信心太重要了。1.2 和计算器、爬虫这类练手项目比它的优势在哪很多入门教程让人做计算器做学生信息管理系统做爬虫。这些项目没有错但体验和效果差别很大。我整理了一个对比项目类型主要覆盖的知识点综合难度做完之后的感受可扩展性计算器函数、条件、基本运算很低会了但感觉没什么用弱做完就结束了学生信息管理系统列表、字典、文件读写中等功能重复界面枯燥一般往上加模块容易乱爬虫网络请求、HTML解析、异常处理高被反爬折磨知识断层严重强但新手很难独立调通文字冒险游戏变量、类型、字符串、分支、循环、函数、文件、数据结构可控能玩、能展示、能迭代极强从命令行到GUI都是一路延伸注意最后一行文字冒险游戏的难度完全由你自己控制。第一版可以只有三两个房间后面逐步加战斗、加背包、加存档、加随机事件。而爬虫一上来就要面对网络问题、编码问题、反爬问题任何一个坑都能让新手劝退。1.3 这个项目到底能练到什么学完能往哪走每次有朋友问我学Python学什么我都会说先做个文字冒险游戏。因为它几乎覆盖了Python基础语法里的所有关键点字符串处理解析玩家输入的指令条件分支判断当前状态、指令是否合法循环主游戏循环列表、字典、元组管理房间地图、背包物品函数把移动、拾取、查看等操作封装成功能块文件读写存档、读档让进度能够持久化这些点单独拿出来都很简单但组合在一起就考验你对程序整体结构的理解。而后面无论你想去做图形界面的游戏、还是做数据分析、写自动化脚本这些能力都是地基。等你把文字冒险游戏做熟再回头看那些“用Python做数据分析可视化”的教程会发现它们的底层逻辑并没有那么神秘。2. 环境准备先把Python装明白后面能省一半的折腾2.1 安装Python时的关键选择环境准备这一步看着简单实际上很多人都卡在了一道隐形门槛上。对于新学Python的人来说我建议直接去Python官网下载安装包不要用那些第三方魔改版也不要用Windows商店里那个版本。官网的安装包干净、稳定、好排查问题。选择版本时选当前最新的稳定版就行。在Windows上安装的时候有一个勾选项非常关键Add Python to PATH。很多人忽略这个勾选结果装完打开命令行一敲python直接提示“不是内部或外部命令”。如果你已经装完了并且没勾选也别急着卸载重装后面第2.2节我会说怎么补救。安装完成后打开命令行Windows按WinR输入cmd后回车敲一下python --version能正常打印类似Python 3.12.1这样的版本号就说明装好了。2.2 环境变量装了Python但命令行打不开的排查流程命令行里敲python没反应是新手遇到最多的问题。原理其实不复杂命令行会在PATH环境变量列表里依次查找“python”这个命令找到就执行找不到就报错。排查步骤确认Python是否真的装了。在开始菜单里找Python文件夹或者看安装目录比如C:\Users\你的用户名\AppData\Local\Programs\Python\Python312\。如果在开始菜单能找到但命令行打不开就是PATH配置问题。按Win键搜索“环境变量”点“编辑系统环境变量”在“高级”选项卡里点“环境变量”在“系统变量”或“用户变量”中找到Path点编辑把Python的安装目录和Scripts子目录加进去。注意要用;分隔。改完环境变量后必须新开一个命令行窗口才生效。老窗口不会自动刷新。还有一个常见情况Windows下敲python会弹出微软商店。这是因为系统开启了“应用执行别名”。解决办法是在“设置 - 应用 - 高级应用设置 - 应用执行别名”里把python.exe和python3.exe的开关关掉。2.3 编辑器VSCode配置Python环境代码编辑器的话我不建议一开始就用那些厚重的IDEVSCode轻量、免费、插件生态好作为Python入门编辑器非常合适。配置起来也不复杂在VSCode的扩展商店搜索“Python”安装微软官方那个Python扩展用VSCode打开你的项目文件夹按CtrlShiftP输入Python: Select Interpreter选中你刚安装的Python解释器写一个简单的.py文件点右上角的三角形运行按钮看是否能正常输出如果你安装了多个Python版本VSCode允许你在不同项目里选择不同解释器这是个很实用的功能后面做不同练手项目时不会互相干扰。2.4 虚拟环境项目隔离是必须养成的习惯我先说结论从你的第一个正式项目开始就使用虚拟环境。虚拟环境相当于给你这个项目一个独立的Python包空间你在这个项目里装的第三方库不会污染全局环境。文字冒险游戏本身用标准库就能写不装任何第三方库也能跑但既然要养成好习惯最好先把venv顺手用上。在项目目录下打开命令行执行python -m venv venvWindows下激活venv\Scripts\activatemacOS/Linux下激活source venv/bin/activate激活后命令行前面会出现(venv)字样说明你已经在虚拟环境里了。之后用pip install装的包都会被放在这个虚拟环境内部不会弄乱系统Python。3. 核心代码用一个可运行的完整示例讲透文字冒险游戏的骨架下面这部分我会带着你从零构建一个可运行的文字冒险游戏。核心代码没有用什么高深技巧全部是基础语法但胜在结构清晰能直接跑。3.1 先构思世界画地图、定物品、想命令动手写代码之前先在纸上画出游戏地图。我做的示例是一个“迷雾森林”三个房间森林入口是起始点有一根火把迷雾小径向北走连接营地废弃营地有一个宝箱需要铁钥匙才能打开地图上的每个房间需要记录三样信息描述文字、可通往的方向、房间里有哪些物品。用Python的字典来建模非常自然。rooms { 森林入口: { description: 你站在森林入口。北边有一条被雾气笼罩的小路。地上扔着一根火把。, exits: {北: 迷雾小径}, items: [火把] }, 迷雾小径: { description: 浓雾将视线压到了几米以内。东边隐约透出一点灯光。, exits: {南: 森林入口, 东: 废弃营地}, items: [] }, 废弃营地: { description: 营地里散落着旧工具一顶破帐篷里放着一个上了锁的宝箱。, exits: {西: 迷雾小径}, items: [铁钥匙] } }不要小看这一步。用数据结构先把世界描述清楚后面的游戏逻辑会顺畅很多。3.2 主循环与命令分发让玩家真正“玩起来”游戏的主循环本质上就是一个while循环。每轮循环做四件事显示当前房间描述读取玩家输入解析指令执行对应的操作。看这段完整代码然后我们逐段拆解current_room 森林入口 inventory [] def show_room(room_name): room rooms[room_name] print(\n room[description]) if room[items]: print(你看到地上有 、.join(room[items])) def move_player(direction): global current_room exits rooms[current_room][exits] if direction in exits: current_room exits[direction] print(f你向{direction}走去。) return True print(那边没有路。) return False def take_item(item): room rooms[current_room] if item in room[items]: room[items].remove(item) inventory.append(item) print(f你捡起了{item}。) else: print(这里没有这个东西。) def show_help(): print(可用命令) print(go 方向 —— 向指定方向移动比如 go 北) print(take 物品 —— 捡起地上的物品) print(inventory —— 查看背包) print(help —— 显示帮助) print(quit —— 退出游戏) print( 迷雾森林 ) print(输入 help 查看帮助。) while True: show_room(current_room) command input( ).strip().lower() if command in (quit, exit, q): print(再见冒险者。) break elif command in (help, h): show_help() elif command inventory: if inventory: print(你的背包里有 、.join(inventory)) else: print(你的背包是空的。) elif command.startswith(go ): move_player(command[3:]) elif command.startswith(take ): take_item(command[5:]) else: print(我听不懂你在说什么。输入 help 查看帮助。)这段代码的核心逻辑是命令分发。我用startswith来识别“go 北”和“take 火把”这类带参数的指令。这里有几个实现细节值得你注意command.strip().lower()把输入的多余空格去掉统一转成小写这样玩家输入“GO 北”“ Go 北”都能被正确识别。command[3:]取出go之后的部分也就是方向。take_item函数里操作列表的方式先判断物品是否在当前房间然后把物品从房间的items列表里移除再添加到背包。这就是一个非常典型的“状态转移”操作。把这段代码保存为adventure.py在命令行运行python adventure.py就能玩了。试着在森林入口输入go 北走进迷雾小径再go 东进入废弃营地捡到铁钥匙再走回去。已经是一个完整可玩的游戏了。3.3 让命令解析更健壮接受同义词和模糊输入实际写游戏时你会发现玩家不会和你约定俗成地输入标准命令。有人会输“拿钥匙”有人会输“捡起钥匙”还有人只输一个“key”。想让游戏更友好就得让命令解析更宽容。一个简单有效的方法建立同义词表把用户任意一个说法映射到标准动作。synonyms { 拿: take, 捡: take, 捡起: take, 获取: take, 查看: look, 帮助: help, 背包: inventory, 北: go, 南: go, }解析时先把输入拆成单词再逐词查同义词表最后合成标准指令。这个技巧能大大提升游戏体验也让你体验到“规则引擎”的雏形。对于更复杂的输入比如“把火把捡起来”还可以用in关键词来提取物品名def extract_item(text): for item_name in [火把, 铁钥匙, 地图, 药水]: if item_name in text: return item_name return None这段逻辑就是关键词匹配虽然简单却已经是很多聊天机器人、指令系统的起步原型。3.4 存档与读档用文件IO让进度能保存下来玩家玩到一半退出了下次得从头开始这就很难受。加一个存档功能这里正好用到Python的文件读写和JSON序列化。import json import os SAVE_FILE savegame.json def save_game(): data { current_room: current_room, inventory: inventory, rooms: rooms } with open(SAVE_FILE, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(游戏已保存。) def load_game(): global current_room, inventory if not os.path.exists(SAVE_FILE): print(没有找到存档文件。) return with open(SAVE_FILE, r, encodingutf-8) as f: data json.load(f) current_room data[current_room] inventory data[inventory] print(存档已加载。)然后在主循环里加上save和load两个命令对应分支。这里有个容易犯的错global current_room, inventory。如果一个函数只是“读”全局变量不需要声明global但如果函数内部要“重新赋值”或者调用方法修改全局变量指向的对象就要用global告诉Python你希望操作的是全局变量而不是在函数内部新建一个同名局部变量。在load_game里我们给current_room和inventory重新赋值所以必须加这个声明。我第6.4节会详细讲这个坑。用JSON的好处是结构清晰、可读性好。不需要引入数据库不需要复杂的文件格式对于文字冒险游戏这个体量正合适。4. 让游戏“活”起来难度与可玩性的进阶玩法基础版本的骨架搭好后游戏已经能玩了。但如果只是走来走去、拿东西玩家很快就会腻。这时候可以逐步往上加玩法。4.1 谜题与机关给游戏增加“要动脑子”的部分最简单的一种机关是设置一个开关状态。比如宝箱需要一个触发条件才能打开chest_opened False def use_item(item): global chest_opened if current_room 废弃营地 and item 铁钥匙: print(你用铁钥匙打开了宝箱里面是一枚发光的宝石。) inventory.append(发光宝石) chest_opened True else: print(现在用不了这个物品。)秘诀就在于用一个布尔变量chest_opened记录这个机关是否被触发。玩家尝试打开宝箱时程序检查这个布尔变量如果为False提示“宝箱锁着”如果为True进入后续剧情。整个游戏的“剧情分支”实际上就是大量布尔变量和字符串变量组合出来的状态空间。稍微复杂一点的机关比如密码锁可以要求玩家先收集多个线索再在特定房间输入数字。逻辑依然是条件判断只是嵌套的层次多一点。4.2 简易战斗系统数值设计不用复杂够用就行文字冒险游戏里的战斗不需要实时动作机制一个回合制就够。核心逻辑是双方轮流造成伤害随机数决定每次攻击的浮动数值。import random def fight(): player_hp 20 enemy_hp 12 enemy_name 森林狼 print(f你遇到了{enemy_name}) while player_hp 0 and enemy_hp 0: print(f\n你的生命{player_hp}{enemy_name}的生命{enemy_hp}) action input(攻击 / 防御 ).strip().lower() if action 攻击: damage random.randint(2, 5) enemy_hp - damage print(f你挥剑攻击造成 {damage} 点伤害。) elif action 防御: print(你摆出防御姿态准备承受攻击。) else: print(你还在犹豫错过了回合。) if enemy_hp 0: print(f你击败了{enemy_name}) return True if action 防御: enemy_damage random.randint(0, 2) else: enemy_damage random.randint(1, 3) player_hp - enemy_damage print(f{enemy_name}反击对你造成 {enemy_damage} 点伤害。) print(你被击败了……) return False这个战斗系统的数值设计很直白玩家20点血每次攻击2-5伤害敌人12点血每次攻击1-3伤害。防御时敌人的伤害降到0-2。简单推算一下可知玩家胜率很高这种“略微偏向玩家”的数值设计对新手游戏非常重要让玩家有紧张感但不会一直被虐。4.3 背包和道具组合让“收集”变得有意义光捡东西不利用背包就只是个装饰。给道具增加组合逻辑让“钥匙开宝箱”“火药炸掉石门”“把药草和泉水混合成药水”这类玩法加入游戏。以药水合成为例玩家在背包里有“药草”和“泉水”在指定房间执行组合操作生成“治疗药水”。def combine_items(item1, item2): if item1 in inventory and item2 in inventory: if {item1, item2} {药草, 泉水}: inventory.remove(药草) inventory.remove(泉水) inventory.append(治疗药水) print(你把药草和泉水混合在一起得到一瓶治疗药水。) return True print(这两个东西组合不了。) return False这类玩法的核心价值是让玩家感受到“收集到的每个东西都不是没用的”。而你在实现上要做的就是一张“组合配方表”加上条件判断。后面如果想加复杂的配方系统可以升级成字典比如recipes[(药草, 泉水)] 治疗药水这样新配方不用改函数逻辑只改数据就行。4.4 从字典到类什么时候应该重构代码当游戏越来越复杂比如有敌人、任务、多个NPC、随机事件如果你还是用一堆字典和全局变量代码会变得很难维护。这时候就该用类来重构。一个最简单的类化思路是从“房间”开始class Room: def __init__(self, name, description, exitsNone, itemsNone): self.name name self.description description self.exits exits if exits is not None else {} self.items items if items is not None else [] def describe(self): text self.description if self.items: text 地上有 、.join(self.items) return text再用一个Player类管理玩家的位置和背包class Player: def __init__(self, current_room): self.current_room current_room self.inventory [] self.hp 20类的好处是把数据和对数据的操作捆绑在一起不再依赖大量全局变量。代码变成“房间对象自己知道自己的出口”玩家对象自己管理自己的物品。对于刚学完类的学习者来说这个重构练习比课本上的“定义一个动物类”来得有意义得多。5. 分享出去把游戏打包成exe的完整流程游戏写完想发给朋友试玩但朋友电脑上大概率没装Python。这时候需要把Python脚本打包成可执行文件。5.1 为什么选PyInstallerPython生态里有好几个打包工具PyInstaller是其中兼容性最好、使用门槛最低的一个。它支持Windows、macOS、Linux也支持把程序打包成单文件。对你的文字冒险游戏来说一个单文件exe可以直接发给朋友双击就能跑体验好很多。5.2 打包实操从安装到出程序的每一步在已经激活的虚拟环境下执行pip install pyinstaller安装完成后在项目目录下执行pyinstaller --onefile --name AdventureGame adventure.py命令参数说明--onefile把所有依赖都打成一个文件。代价是启动时会有几秒解压时间但发给别人最方便。--name生成的文件名随意。--console保留命令行窗口。文字冒险游戏依赖命令行输入输出所以这个选项要保留不能加--noconsole。执行完后dist目录下会出现一个AdventureGame.exeWindows下。把这个exe发给任何人双击就能玩。如果后续代码改了重新打包即可。注意PyInstaller原生只支持打包当前平台的可执行文件你在Windows上只能生成Windows的exe在Mac上只能生成Mac程序跨平台打包需要借助别的方案比如在对应系统上分别打包或者用CI流程。5.3 打包后的常见问题闪退、资源文件缺失、误报毒我第一次打包完双击exe直接就闪退了窗口一闪而过什么都没看到。排查方法其实很简单打开一个命令行窗口手动输入exe的路径运行这样报错信息会保留在命令行里就能看到具体的异常栈。常见的问题有几种代码里用了open(savegame.json)这种相对路径打包后运行目录变成了系统临时目录导致找不到文件。解决办法是改用os.path.dirname(sys.executable)或sys._MEIPASS计算资源文件路径。游戏里放了配图、音乐等外部文件打包时需要用--add-data把文件一起打进去。资源路径要用前面提到的resource_path函数动态拼接。部分杀毒软件会误报PyInstaller打包出来的exe这个属于打包工具的已知特性。可以在发布时告诉朋友添加白名单或者考虑签名。6. 我踩过的坑环境与编码问题排查实录最后这部分我把自己实际踩过的坑集中整理一下。这些坑没有一个是教科书会教的但几乎每个Python新手都会遇到。6.1 最冤的坑Windows下pip和python找不到或python弹出商店有一次我在一台新电脑上装好Python确认安装目录里有python.exe但命令行里敲python却弹出了Windows商店。排查后发现问题出在“应用执行别名”。Windows把python.exe和python3.exe注册成了商店应用的启动别名。当你安装Python时没有重新刷PATH或者PATH里Python路径排在商店别名后面系统就会优先启动商店那个“假python”。解决办法打开“设置 - 应用 - 高级应用设置 - 应用执行别名”找到“python.exe”和“python3.exe”把开关都关掉新开一个命令行窗口再试python --version另外推荐以后统一用python -m pip而不是直接敲pip。因为这样能确保pip和当前的python解释器版本匹配避免“pip装的库import却说找不到”的诡异情况。6.2 中文乱码与UnicodeDecodeError文字冒险游戏大量使用中文如果代码文件没有用UTF-8编码保存同时在Windows命令行运行很容易出现乱码或者报错。我在写存档功能时就遇到过读取存档文件时报UnicodeDecodeError: gbk codec cant decode byte。原因很简单Windows下open()函数默认使用系统编码GBK打开文件而我保存存档时用的是UTF-8两边不一致所以读取失败。解决方法是始终在open()里显式指定编码with open(SAVE_FILE, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) with open(SAVE_FILE, r, encodingutf-8) as f: data json.load(f)同时Python默认会把源文件解析为UTF-8所以代码文件本身用UTF-8编码保存就行。但如果你在Windows命令行直接运行一个.py文件且命令行代码页不是UTF-8print的中文也可能乱码。最简单的办法是在代码开头加一行# -*- coding: utf-8 -*-虽然Python3默认UTF-8源码解析但这行注释在遇到某些老编码环境时仍是保险措施。但这并不能解决命令行代码页的问题最稳的是给exe打包后让玩家在正常的窗口里运行或者在代码里适配。6.3 打包后资源文件找不到sys._MEIPASS的经典用法用PyInstaller的--onefile模式打包后程序运行时会先把自身解压到一个临时目录运行完又删除。如果你在代码里用相对路径读取“地图数据.txt”或者“剧情.json”运行时大概率找不到。解决办法是写一个通用的资源路径函数import sys import os def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path)开发阶段sys里没有_MEIPASS属性程序使用当前目录打包后sys._MEIPASS指向临时解压目录资源文件被放在那里。用的时候把open(map.json)改成open(resource_path(map.json))即可。6.4 全局变量的“记忆丢失”作用域理解误区很多新手在写游戏时会遇到一个诡异现象函数里明明修改了变量但退出函数后值又变回去了。比如inventory [] def pickup(): inventory [火把] print(inventory) pickup() print(inventory) # 输出 []不是 [火把]原因是函数内部inventory [火把]创建了一个新的局部变量和全局的inventory不是一个东西。要修改全局变量必须在函数内声明global inventory。但注意inventory.append(火把)则不需要global声明因为append修改的是变量指向的列表对象本身而不是给变量重新赋值。这个坑相当经典我在做存档加载功能时踩得很彻底。排查思路也简单如果我预期的状态没有生效优先检查相关变量是否被局部同名变量遮蔽。最后再分享一点个人体会文字冒险游戏这个项目最打动我的地方是它的“完成感”。一个游戏从第一行代码到能玩、能存档、能打包发给朋友这个过程不会太长但完整得让你能真切感受到“我真的做出了一个软件”。我个人的经验是第一版不要贪多三五个房间、一两个物品、能走通一条完整的剧情线就好。做完这些基础功能再慢慢加战斗、加谜题、加存档。每一次扩展都是对代码结构的一次压力测试。你会发现有些东西一开始设计得好后面扩展就很轻松有些东西一开始图省事后面加功能就要返工。这种教训比看多少架构文章都来得深刻。等你去体验用类和对象重构字典版本的游戏时才能真正体会到面向对象设计的价值所在。项目本身虽小但从简单脚本到清晰工程的完整路径它全都包含在里面了。