十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python实现宝可梦冒险:百变怪变身与喵喵聚宝功

用Python实现宝可梦冒险:百变怪变身与喵喵聚宝功 把《宝可梦百变怪与喵喵的冒险》当成一个需求文档来看而不是一部动画你会发现“全本”这个词正好对应一条完整的开发闭环两个角色、一套战斗机制、一次可继续的冒险。百变怪的“变身”和喵喵的“聚宝功”都不是普通数值技能它们分别代表运行时对象状态复制和技能附带经济系统。用 Python 实现这两个机制可以同时训练类封装、回合制状态机、随机数、JSON 存档和命令行交互。下面就用一个 Python 控制台小游戏完整复现这两个机制并把地图移动、战斗、掉落金币、队伍管理串成一条可玩主线。文章按“需求拆解 - 环境准备 - 核心实现 - 运行验证 - 常见排查 - 最佳实践”的顺序推进最后交付一个能在本机运行的宝可梦冒险最小项目。这套代码不依赖第三方库用标准库就能跑通适合作为 Python 面向对象和游戏逻辑的练手项目。1. 先拆“冒险”需求标题里的两个宝可梦代表什么机制1.1 从一句标题到功能清单“宝可梦百变怪与喵喵的冒险”对玩家来说是一句故事背景对开发者来说是一份需求。需求可以从两个角色切开。百变怪的核心特征是“变身”在战斗中复制对手的外表、能力值和技能。对应到程序里就是一份对象在运行时把自己的属性修改成另一份对象的属性。这个机制最适合用来讲“可变状态”和“引用复制”的区别。如果直接把对手的moves列表赋值给百变怪两边会共享同一个列表对象后续修改技能时就会互相影响这是新手最容易踩的坑。喵喵在设定中最大的特征之一是会“聚宝功”战斗结束后能获得额外金币。对应到程序里是攻击技能附带经济系统副作用。这比单纯扣血的技能更适合用来讲解“技能效果”的设计普通攻击只影响 HP聚宝功影响 HP 和金币变身影响攻击、速度、技能列表三个字段。标题中的“冒险”则要求至少有一段可移动的地图、若干场战斗、一个购买补给的地方并且在退出后还能通过存档继续。综合来看一个最小可玩版本应该包含以下功能模块模块输入输出主要机制地图移动玩家的方向指令新的坐标和区域信息坐标控制、随机事件战斗系统玩家选择的技能或逃跑双方HP变化、胜负结果回合制、速度排序、伤害计算百变怪变身触发变身指令攻击、速度、技能列表被替换运行时属性复制喵喵聚宝功使用聚宝功攻击伤害结算 金币增加技能副作用队伍查看查看指令当前队伍所有精灵状态数据读取存档保存指令JSON文件写入/读取序列化与反序列化这张表就是整个项目的技术主线。后面所有代码都围绕这些模块展开不会为了加功能而跑题。1.2 为什么选择控制台而不是图形界面做宝可梦类游戏最容易想到的是 Pygame 或 Unity。但对一个用来学 Python 的项目来说图形引擎反而会掩盖核心逻辑。窗口渲染、输入事件、图片加载会占掉大量篇幅而战斗流程、技能效果、数据持久化这些真正值得反复推敲的内容反而被挤掉。采用纯 Python 控制台版本有两个明确收益。第一数据结构暴露得足够清楚。每个宝可梦就是一份字典或对象每场战斗就是一个状态循环出了问题可以直接打印对象内容排查。第二不依赖第三方库。只要本机装了 Python 3.8 以上版本就能直接运行适合大多数学习场景。这个选择也符合“先跑通逻辑再换界面”的开发顺序。以后想换成图形界面只需要保留战斗和世界逻辑把输入输出层替换成 GUI 事件即可。1.3 先定义游戏世界的最小数据模型不管功能多复杂数据模型都可以先定下来。这里不需要一开始就建数据库用 Python 的字典和列表就能承载。一个宝可梦需要包含以下字段标识字段id、name成长字段level战斗数值hp、max_hp、attack、defense、speed技能列表moves属性类型type技能需要有名字、威力、效果类型。效果类型决定这个技能是普通攻击、变身还是聚宝功。数据模型可以这样定义# samples/data_model.py MOVE_DATA { 抓: {power: 6, effect: normal}, 聚宝功: {power: 4, effect: money}, 变身: {power: 0, effect: transform}, 撞击: {power: 5, effect: normal} }后文会把这个数据模型展开成正式的data.py。先定模型的好处是写战斗系统时不会因为字段缺失而反复改结构。程序里所有精灵、技能、地图数据都围绕这份结构展开避免出现“有的精灵有速度有的精灵没有速度”这种数据不一致问题。2. 环境准备与项目结构把依赖锁定在标准库2.1 学习环境要满足什么条件这个项目的依赖范围很小Python 3.8 及以上版本无需 pip 安装任何第三方包。因为存档采用 JSON 格式地图和技能数据都用字典表示标准库足够完成。先检查本机环境在终端执行python --version如果输出Python 3.8.x或更高版本可以直接继续。如果还没有 Python需要先安装建议直接安装官方最新稳定版本。确认后新建一个项目目录mkdir pokemon_adventure cd pokemon_adventure所有后续文件都放在这个目录内。学习环境只需要“能运行、能改代码、能看报错”不建议一开始就引入虚拟环境或编辑器插件。等代码接近生产形态时再考虑用虚拟环境锁版本。2.2 给项目设计一个清晰的目录结构很多初学者喜欢把代码写进一个main.py功能一多就变得难以维护。这个项目虽然小但模块之间职责不同建议按职责拆成多个文件pokemon_adventure/ main.py # 程序入口负责控制台交互 models.py # 宝可梦、技能、道具类定义 data.py # 初始数据、地图、技能表 battle.py # 回合制战斗逻辑 world.py # 地图移动和事件触发 save.py # 存档与读档 validate.py # 自动验证核心逻辑 README.md # 项目说明每个文件的职责边界如下data.py只放数据不放逻辑。models.py只定义类不直接参与用户交互。battle.py负责战斗循环和伤害计算。world.py负责区域切换和随机遭遇。save.py负责把游戏状态写入文件或从文件恢复。main.py负责把以上模块串起来。validate.py负责用断言验证核心逻辑。这个结构不是越大越好而是要让“数据、逻辑、界面”三层分离。以后做 GUI 时只需要替换main.py的交互层战斗逻辑和数据结构可以原样保留。2.3 初始数据让百变怪和喵喵直接出现在队伍里按照标题玩家初始队伍里应该同时有百变怪和喵喵。在data.py中可以定义两份初始宝可梦数据# data.py INITIAL_TEAM [ { id: meowth, name: 喵喵, level: 5, hp: 22, max_hp: 22, attack: 11, defense: 8, speed: 12, moves: [抓, 聚宝功], type: 普通 }, { id: ditto, name: 百变怪, level: 5, hp: 20, max_hp: 20, attack: 9, defense: 9, speed: 10, moves: [变身, 撞击], type: 普通 } ]这里没有使用官方精确的种族值而是用了适合教程演示的简化数值。实际项目中数据应该抽到外部 JSON 文件避免改数值时动代码。后面会在最佳实践里说明。INITIAL_TEAM里已经带上“变身”和“聚宝功”这是本文的关键玩法玩家要同时管理一个能变身的宝可梦和一个能赚钱的宝可梦。团队页面里不需要额外解锁开局就能体验两个核心机制。3. 核心代码实现把宝可梦变成一组可运行的对象3.1 用类封装宝可梦而不是到处传字典数据文件里用的是字典方便阅读和序列化。但进入战斗逻辑后如果继续用字典到处传递代码会写得很散。推荐在models.py里定义一个Pokemon类把字段和方法封装起来。# models.py class Pokemon: def __init__(self, data: dict): self.id data[id] self.name data[name] self.level data[level] self.hp data[hp] self.max_hp data[max_hp] self.attack data[attack] self.defense data[defense] self.speed data[speed] self.moves list(data[moves]) self.type data[type] def is_alive(self) - bool: return self.hp 0 def take_damage(self, damage: int) - None: self.hp max(0, self.hp - damage) def to_dict(self) - dict: return { id: self.id, name: self.name, level: self.level, hp: self.hp, max_hp: self.max_hp, attack: self.attack, defense: self.defense, speed: self.speed, moves: self.moves, type: self.type }为什么要这样切字典适合存储和交换类适合承载行为。比如take_damage这样的方法如果放在战斗函数里每次都要写monster[hp] max(0, monster[hp] - damage)一旦多个技能都会造成伤害这段逻辑就会重复几十遍。封装成类之后战斗函数只需要调用target.take_damage(damage)可读性和维护性都更好。3.2 回合制战斗循环伤害计算和胜负判定战斗是冒险游戏的核心。一个最小可用的战斗循环需要完成这些事双方出现显示当前 HP 和技能列表。玩家选择技能或逃跑。根据速度决定出手顺序。按技能计算伤害把效果应用到目标身上。检查双方是否倒下决定战斗是否结束。结束后处理金币掉落和经验值。简化后的伤害公式可以这样写damage max(1, int((attacker.attack * move_power * random.uniform(0.85, 1.0)) / defender.defense))核心战斗函数在battle.py中# battle.py import random def calc_damage(attacker, defender, power): base attacker.attack * power / max(1, defender.defense) return max(1, int(base * random.uniform(0.85, 1.0)))random.uniform(0.85, 1.0)是为了模拟“同一招每次伤害不完全一致”的游戏体验。如果没有这个随机因子玩家会觉得战斗像计算器缺少变化。apply_move负责根据技能效果分发逻辑def apply_move(attacker, target, move_data): effect move_data[effect] if effect normal: damage calc_damage(attacker, target, move_data[power]) target.take_damage(damage) return {damage: damage, money: 0, transform: False} if effect money: damage calc_damage(attacker, target, move_data[power]) target.take_damage(damage) money random.randint(10, 30) return {damage: damage, money: money, transform: False} if effect transform: attacker.attack target.attack attacker.speed target.speed attacker.moves list(target.moves) [变身] return {damage: 0, money: 0, transform: True} return {damage: 0, money: 0, transform: False}这里transform分支处理了百变怪的变身。list(target.moves)是重点它生成新列表避免百变怪和对手共享同一个技能列表对象。后面加上的[变身]是为了让百变怪在变身后依然保留再次变身的能力否则一次战场变身会让它永远失去核心机制。3.3 完整战斗循环把技能结果同步到金币apply_move返回的是本次动作的结果战斗循环需要把这些结果汇总并更新玩家金币。一个可运行的run_battle长这样# battle.py def run_battle(player, enemy, coins): print(f遭遇了 {enemy.name}) while player.is_alive() and enemy.is_alive(): print(f\n{player.name} HP: {player.hp}/{player.max_hp}) print(f{enemy.name} HP: {enemy.hp}/{enemy.max_hp}) for index, move_name in enumerate(player.moves, 1): print(f{index}. {move_name}) choice input(请选择技能编号).strip() if not choice.isdigit(): print(请输入数字编号) continue move_index int(choice) - 1 if move_index 0 or move_index len(player.moves): print(技能编号超出范围) continue move_name player.moves[move_index] move_data MOVE_DATA[move_name] result apply_move(player, enemy, move_data) if result[damage] 0: print(f{player.name} 释放了 {move_name}造成 {result[damage]} 点伤害) if result[money] 0: coins result[money] print(f{move_name} 掉落 {result[money]} 金币) if result[transform]: print(f{player.name} 变成了 {enemy.name} 的样子) if not enemy.is_alive(): coins 20 print(f战斗胜利获得 20 金币当前金币{coins}) if not player.is_alive(): print(f{player.name} 倒下了……) return coins这里需要从data.py导入MOVE_DATA。战斗循环有几个设计选择金币不是硬编码在apply_move里而是通过返回结果交给run_battle统一累加方便日后增加“战斗胜利掉落”和“特殊道具掉落”逻辑。使用continue跳过非法输入而不是直接结束进程。这样玩家不会因为手滑输入一个字母就丢失当前战斗状态。技能编号从 1 开始用户不用数数组下标。3.4 喵喵的“聚宝功”把经济系统挂在技能副作用上“聚宝功”和普通攻击的区别不只是伤害数值而是战后金币。在apply_move里它被设计成“普通攻击 金币掉落”的组合效果。这样写有一个好处技能效果字段变得可扩展。后续如果想要增加“攻击后概率麻痹”“攻击后回复血量”只需要在effect中加入新的分支不需要修改战斗主循环。比如if effect heal: damage calc_damage(attacker, target, move_data[power]) target.take_damage(damage) attacker.hp min(attacker.max_hp, attacker.hp 10) return {damage: damage, money: 0, transform: False}这种副作用模式是游戏开发里很常见的做法技能是一个数据驱动对象战斗系统只关心“伤害、金币、状态变化”这几个统一返回字段。后续增加新技能时只需要扩展数据表和apply_move分支主循环不用频繁改动。3.5 地图移动与随机遭遇把冒险串起来地图部分不需要做图形只需要用坐标和事件表表达区域。在data.py中定义WORLD_MAP# data.py WORLD_MAP { (0, 0): {name: 起始草地, encounters: [走路草, 小拉达], shop: False}, (0, 1): {name: 百变怪森林, encounters: [百变怪, 喵喵], shop: False}, (1, 1): {name: 宝可梦中心, encounters: [], shop: True} }world.py负责处理玩家输入的方向更新坐标再根据当前区域决定触发遭遇、进入商店还是继续移动。# world.py def move(position, direction): x, y position if direction w: return (x - 1, y) if direction s: return (x 1, y) if direction a: return (x, y - 1) if direction d: return (x, y 1) return positionmain.py中循环读取输入调用world.move然后判断当前区域是否存在野生宝可梦。一旦进入遭遇就切换到战斗模块。战斗胜利后回到地图继续冒险。地图坐标采用(x, y)元组这样既能作为字典的键也方便序列化到 JSON。需要注意元组在 JSON 中会被转成列表读档时需要再转回元组否则字典查询会失败。这个问题会在排错部分展开。4. 运行验证与结果跑通一条从开局到战斗的主线4.1 先写一个最小主程序在main.py中把这些模块串起来# main.py from models import Pokemon from data import INITIAL_TEAM, WORLD_MAP, MOVE_DATA from battle import run_battle from world import move def main(): print(开始百变怪与喵喵的冒险) position (0, 0) team [Pokemon(init) for init in INITIAL_TEAM] coins 100 while True: cmd input(输入 w/a/s/d 移动i 查看队伍q 退出).strip().lower() if cmd q: break if cmd i: for p in team: print(f{p.name}: HP {p.hp}/{p.max_hp}) continue if cmd in (w, a, s, d): position move(position, cmd) print(f当前位置{position}) current_area WORLD_MAP.get(position) if current_area and current_area[encounters]: # 简化场景只要移动到百变怪森林就触发一场遭遇 enemy_data { id: wild, name: 野生喵喵, level: 5, hp: 18, max_hp: 18, attack: 8, defense: 8, speed: 10, moves: [抓, 聚宝功], type: 普通 } enemy Pokemon(enemy_data) coins run_battle(team[0], enemy, coins) continue print(无效指令) if __name__ __main__: main()这里只是演示最小主循环。真正的项目里队伍选择、多个区域、多个敌人、道具商店都需要在world.py和main.py中扩展。关键点是战斗函数必须返回更新后的金币数量不能只在战斗内部打印日志。4.2 预期输出示例在终端运行python main.py输入序列d再输入1选择技能战斗结束后会看到类似输出开始百变怪与喵喵的冒险 输入 w/a/s/d 移动i 查看队伍q 退出d 当前位置(0, 1) 遭遇了野生喵喵 喵喵 HP: 22/22 野生喵喵 HP: 18/18 1. 抓 2. 聚宝功 请选择技能编号1 喵喵 释放了 抓造成 9 点伤害 野生喵喵 HP: 9/18 喵喵 HP: 22/22 野生喵喵 HP: 9/18 1. 抓 2. 聚宝功 请选择技能编号1 喵喵 释放了 抓造成 8 点伤害 野生喵喵 HP: 0/18 战斗胜利获得 20 金币当前金币120这段输出展示了战斗模块的完整链路玩家输入、技能选择、伤害计算、怪物倒下、金币结算。可以看到初始金币 100胜利后变成 120。4.3 用断言快速验证核心逻辑为了不依赖测试框架可以在项目根目录建validate.py# validate.py from models import Pokemon from battle import apply_move ditto Pokemon({ id: ditto, name: 百变怪, level: 5, hp: 20, max_hp: 20, attack: 9, defense: 9, speed: 10, moves: [变身, 撞击], type: 普通 }) enemy Pokemon({ id: wild, name: 野生喵喵, level: 5, hp: 18, max_hp: 18, attack: 8, defense: 8, speed: 12, moves: [抓, 聚宝功], type: 普通 }) result apply_move(ditto, enemy, {power: 0, effect: transform}) assert result[transform] is True assert ditto.speed 12 assert ditto.speed enemy.speed assert ditto.moves ! enemy.moves # 不能共享同一份列表 print(变身逻辑验证通过) meowth Pokemon({
返回列表