公司动态

Python回合制对战游戏Demo:类设计与完整实现

📅 2026/9/1 11:39:19
Python回合制对战游戏Demo:类设计与完整实现
用 Python 写一个“雾之湖对打比赛”回合制对战 Demo从类设计到完整可运行最近在整理 Python 面向对象编程的练手项目时突然想到一个很有意思的场景如果让“雾之湖”的两位选手来一场回合制对打比赛该怎么用代码实现这个 idea 用来做 Python 入门到进阶的小项目特别合适。一方面它不依赖任何第三方库纯 Python 标准库就能写完另一方面它把类、对象、继承、枚举、异常处理、随机数、状态管理等 Python 核心知识点全部串了起来。本文就带着大家从零开始实现一个完整的“雾之湖对打比赛”回合制对战程序。不管你是刚学完 Python 基础语法还是想找一个能写进简历的小项目这篇文章都值得读完。先说一下这篇文章要覆盖的内容回合制对战游戏的经典结构如何用类设计角色、技能和战斗系统完整的可运行代码分模块讲解运行效果演示常见报错和排错思路工程化改进建议1. 背景与核心概念1.1 什么是回合制对战程序回合制对战简单说就是两位选手轮流行动你打一下、我打一下直到一方生命值归零。它和即时战斗比如动作游戏最大的区别在于时间被拆分成离散的“回合”每个回合内玩家只能执行有限的操作。从程序设计角度看回合制对战是一个非常典型的“状态机”场景。程序的运行过程可以抽象成战斗开始 - 选手A行动 - 检查是否结束 - 选手B行动 - 检查是否结束 - 循环这个流程看起来简单但真正写代码时会牵扯出不少值得深入思考的问题选手的属性生命值、攻击力、防御力、速度用什么数据结构保存技能效果伤害、治疗、防御增益如何统一描述战斗流程如何控制才不会让逻辑混乱程序如何判断胜负如果以后要扩展更多角色、更多技能代码能不能做到“改一处不影响其他地方”这些问题的本质就是面向对象设计和程序的可扩展性问题。所以我选择了用类的思路来实现这个项目。1.2 项目设定既然标题叫“雾之湖的对打比赛”我们就以雾之湖为背景设计两位参赛选手选手简要设定战斗特点琪露诺⑨雾之湖的冰之妖精自称最强擅长冰系技能攻击力高但防御偏弱大妖精雾之湖的温和妖精速度较快技能全面注意这里我们只是借用了一个趣味命名目的是让项目更有代入感。代码本身不依赖任何外部资源你完全可以把角色名改成自己喜欢的名字。1.3 技术选型实现这个项目我推荐用 Python 3.8 的语法特性。为什么选 PythonPython 的类、继承、枚举机制非常适合描述角色和技能。标准库random可以轻松实现暴击、闪避、随机伤害等效果。不需要安装第三方库环境配置成本为零。语法简单能让初学者把注意力集中在“程序设计”上而不是被复杂的框架绑定。如果你还没安装 Python请先到 Python 官网下载 3.8 以上版本。本文示例以 3.10 版本作为演示环境但代码兼容 3.8 及以上所有版本。2. 环境准备与版本说明在开始写代码之前先明确环境要求。2.1 开发环境建议使用以下任一环境组合环境项推荐方案操作系统Windows 10/11、Ubuntu 20.04、macOSPython 版本3.8推荐 3.10IDEPyCharm、VS Code、或者任意文本编辑器依赖包无仅标准库2.2 验证 Python 环境打开终端Windows 下按Win R输入cmdLinux/macOS 下打开 Terminal输入python --version如果能输出版本号比如Python 3.10.0说明环境正常。如果你电脑上同时装了 Python 2 和 Python 3可能需要使用python3命令python3 --version2.3 项目结构为了方便管理我们按模块化方式组织代码。项目结构如下misty-lake-battle/ ├── main.py # 程序入口负责启动战斗 ├── character.py # 角色类定义 ├── skill.py # 技能类定义 ├── battle.py # 战斗系统核心逻辑 └── README.md # 项目说明可选后面的实战环节我们会按这个结构逐个创建文件。你也可以把所有代码写在一个.py文件里但那样不利于后期扩展。从工程化角度我更推荐分成多个模块。3. 核心设计先想清楚再写代码写代码最忌讳的就是“拿到需求就开干”。我们先花几分钟把程序的结构设计好后面写起来会顺畅很多。3.1 角色属性设计一个回合制游戏中的角色通常包含以下核心属性名称name用来显示。生命值hp / max_hp当前生命值和最大生命值。生命值为 0 时角色倒下。攻击力attack决定基础伤害值。防御力defense减少受到的伤害。速度speed决定先手顺序。在 Python 中最自然的表达方式就是定义一个Character类class Character: def __init__(self, name, max_hp, attack, defense, speed): self.name name self.max_hp max_hp self.hp max_hp self.attack attack self.defense defense self.speed speed这里有一个设计细节hp初始等于max_hp因为角色入场时是满血状态。max_hp单独保存是为了后续回血、显示血条、判断百分比时使用。3.2 技能设计如果角色只会“普通攻击”这个对战系统就显得很单调。所以我们需要抽象出“技能”这个概念。一个技能通常包含技能名称伤害系数普通攻击的倍数。命中率决定这个技能有多大概率打中对方。特殊效果比如治疗、增加防御、必定暴击。为了描述这些信息我们可以定义一个Skill类class Skill: def __init__(self, name, multiplier1.0, hit_rate1.0, kinddamage): self.name name self.multiplier multiplier self.hit_rate hit_rate self.kind kind其中kind的类型可以扩展为damage伤害技能。heal治疗技能。buff增益技能比如提升防御。这样设计的好处是以后要加新技能不需要改战斗系统逻辑只需要创建新的Skill实例即可。3.3 战斗流程设计战斗核心是一个循环。伪代码如下while 双方都活着: 按照速度决定先手 先手方选择技能并执行 检查对方是否倒下 如果没倒下后手方执行 检查是否有一方倒下为了代码的清晰性我们把“执行一次行动”的逻辑封装成一个函数避免主循环过于臃肿。3.4 为什么会用到枚举Python 中有一个很实用的模块叫enum。它适合表示“固定的有限选项”比如技能类型就非常适合用枚举表示from enum import Enum class SkillType(Enum): DAMAGE damage HEAL heal BUFF buff使用枚举比直接用字符串有很多优势拼写错误在运行时不容易发现但枚举会在赋值阶段就能抛出更明确的错误。枚举自带可读性代码里写SkillType.DAMAGE比写damage更清晰。IDE 能自动补全减少低级错误。4. 完整实战实现“雾之湖对打比赛”设计思路确定后下面我们动手写完整代码。4.1 创建技能模块skill.py先创建skill.py定义技能类和枚举类型。代码如下# 文件路径skill.py from enum import Enum class SkillType(Enum): 技能类型枚举 DAMAGE damage # 伤害 HEAL heal # 治疗 BUFF buff # 增益 class Skill: 技能类 属性说明 name 技能名称 skill_type 技能类型SkillType 枚举 multiplier 伤害倍率或治疗倍率 hit_rate 命中率范围 0~1 description 技能描述 def __init__(self, name, skill_typeSkillType.DAMAGE, multiplier1.0, hit_rate1.0, description): self.name name self.skill_type skill_type self.multiplier multiplier self.hit_rate hit_rate self.description description def __str__(self): return f{self.name}{self.description}4.2 创建角色模块character.py接下来定义character.py实现角色类。角色类需要提供几个关键方法is_alive()判断角色是否存活。take_damage(amount)受到伤害。heal(amount)接受治疗。show_info()显示角色状态。同时为了让角色拥有“技能”我们把角色和技能组合起来——每个角色有一组技能列表。为了简化操作我们采用“普通攻击 一个特殊技能”的模式。# 文件路径character.py import random from skill import Skill, SkillType class Character: 对战角色类 属性说明 name 角色名 max_hp 最大生命值 hp 当前生命值 attack 攻击力 defense 防御力 speed 速度 skills 技能列表 def __init__(self, name, max_hp, attack, defense, speed, skillsNone): self.name name self.max_hp max_hp self.hp max_hp self.attack attack self.defense defense self.speed speed self.skills skills if skills else [] def is_alive(self): 判断角色是否存活 return self.hp 0 def take_damage(self, amount): 受到伤害 计算方式实际伤害 攻击力 - 防御力最低不低于 1 damage max(1, amount) self.hp - damage if self.hp 0: self.hp 0 return damage def heal(self, amount): 治疗不超过最大生命值 self.hp amount if self.hp self.max_hp: self.hp self.max_hp def show_info(self): 显示角色状态 bar_length 20 filled int(bar_length * self.hp / self.max_hp) bar # * filled - * (bar_length - filled) print(f{self.name}: HP {self.hp}/{self.max_hp} [{bar}]) def add_skill(self, skill): 给角色添加技能 self.skills.append(skill)这里take_damage的设计需要解释一下。很多人会这样写self.hp - amount但这样有个问题如果攻击力低于防御力伤害可能是负数角色反而回血了。所以我们在角色内部做一个保护无论外部传入的伤害是多少最终结算时最低伤害为 1。同时heal里也做了上限限制防止治疗溢出回满之后还超过最大血量。4.3 创建战斗系统模块battle.py这是整个项目的核心模块。战斗系统负责按速度排序决定出手顺序。处理技能释放。打印战斗日志。判定胜负。# 文件路径battle.py import random import time from character import Character from skill import SkillType class Battle: 战斗系统类 主要负责两个角色之间的回合制对战流程。 def __init__(self, character1, character2): self.character1 character1 self.character2 character2 self.round_number 0 def get_attack_order(self): 根据速度决定先手顺序速度相同时随机 if self.character1.speed self.character2.speed: return [self.character1, self.character2] elif self.character1.speed self.character2.speed: return [self.character2, self.character1] else: # 速度相同随机顺序 characters [self.character1, self.character2] random.shuffle(characters) return characters def calculate_damage(self, attacker, defender, multiplier): 计算伤害值 基础伤害 攻击力 * 倍率 实际伤害 基础伤害 - 防御力 最终伤害 max(实际伤害, 1) base_damage attacker.attack * multiplier damage base_damage - defender.defense return max(1, damage) def use_skill(self, attacker, defender, skill): 执行一个技能 返回技能是否命中 # 判断命中 if random.random() skill.hit_rate: print(f {attacker.name} 使用 {skill.name}但是被躲开了) return False if skill.skill_type SkillType.DAMAGE: damage self.calculate_damage(attacker, defender, skill.multiplier) actual_damage defender.take_damage(damage) print(f {attacker.name} 使用 {skill.name}对 {defender.name} 造成 {actual_damage} 点伤害) elif skill.skill_type SkillType.HEAL: heal_amount int(attacker.max_hp * skill.multiplier) attacker.heal(heal_amount) print(f {attacker.name} 使用 {skill.name}恢复了 {heal_amount} 点生命值) elif skill.skill_type SkillType.BUFF: attacker.attack int(attacker.attack * skill.multiplier) print(f {attacker.name} 使用 {skill.name}攻击力提升了) return True def take_turn(self, attacker, defender): 执行一个回合中的一次行动 # 默认使用第一个技能普通攻击 # 实际项目里可以做成玩家选择技能这里为了简化随机选择一个技能 skill random.choice(attacker.skills) print(f {attacker.name} 选择了技能{skill.name}) self.use_skill(attacker, defender, skill) time.sleep(1) def is_finished(self): 判断战斗是否结束 return not self.character1.is_alive() or not self.character2.is_alive() def get_winner(self): 获取胜者 if not self.character1.is_alive(): return self.character2 elif not self.character2.is_alive(): return self.character1 return None def start(self): 开始战斗 print( 战斗开始) print(f{self.character1.name} VS {self.character2.name}) print() # 显示初始状态 self.character1.show_info() self.character2.show_info() print() # 确定先手顺序 order self.get_attack_order() first, second order print(f速度决定先手{first.name} 先行动) print() # 主循环 while not self.is_finished(): self.round_number 1 print(f--- 第 {self.round_number} 回合 ---) self.take_turn(first, second) if self.is_finished(): break self.take_turn(second, first) if self.is_finished(): break # 每个回合结束显示双方状态 print() self.character1.show_info() self.character2.show_info() print() # 战斗结束 print() winner self.get_winner() if winner: print(f {winner.name} 获胜) else: print( 平局)这个类的设计有几个值得注意的点get_attack_order把“出手顺序”单独抽出来这样以后如果加入“先手技能”“偷袭”等玩法只需要修改这一个方法不需要动主循环。take_turn负责“一次行动”的完整流程。这里为了演示简单让角色随机选择技能。实际开发中可以改成读取玩家输入或者用 AI 策略选择这部分我们放在后面扩展。use_skill使用枚举判断技能类型而不是用 if-else 判断字符串可读性更好。time.sleep(1)增加 1 秒延迟让战斗过程不至于一瞬间结束观感更接近真正的回合制游戏。4.4 创建入口模块main.py最后创建main.py把角色和技能配置起来然后启动战斗。# 文件路径main.py from skill import Skill, SkillType from character import Character from battle import Battle def create_cirno(): 创建角色琪露诺 skills [ Skill(普通攻击, SkillType.DAMAGE, multiplier1.0, hit_rate1.0), Skill(冰符「Icicle Fall」, SkillType.DAMAGE, multiplier1.8, hit_rate0.85), Skill(寒冰治愈, SkillType.HEAL, multiplier0.3, hit_rate1.0), Skill(冻符「Perfect Freeze」, SkillType.BUFF, multiplier1.2, hit_rate1.0), ] return Character( name琪露诺, max_hp180, attack28, defense10, speed22, skillsskills ) def create_daiyousei(): 创建角色大妖精 skills [ Skill(普通攻击, SkillType.DAMAGE, multiplier1.0, hit_rate1.0), Skill(风符「Wind Blast」, SkillType.DAMAGE, multiplier1.5, hit_rate0.9), Skill(自然的恩惠, SkillType.HEAL, multiplier0.35, hit_rate1.0), ] return Character( name大妖精, max_hp160, attack25, defense12, speed25, skillsskills ) def main(): cirno create_cirno() daiyousei create_daiyousei() battle Battle(cirno, daiyousei) battle.start() if __name__ __main__: main()4.5 运行程序在项目目录下运行python main.py如果你的系统用的是 Python 3 命令请运行python3 main.py预期输出效果类似下面这样每次运行因为随机数不同结果可能不一样 战斗开始 琪露诺 VS 大妖精 琪露诺: HP 180/180 [####################] 大妖精: HP 160/160 [####################] 速度决定先手大妖精 先行动 --- 第 1 回合 --- 大妖精 选择了技能普通攻击 大妖精 使用 普通攻击对 琪露诺 造成 15 点伤害 琪露诺 选择了技能冰符「Icicle Fall」 琪露诺 使用 冰符「Icicle Fall」对 大妖精 造成 38 点伤害 琪露诺: HP 165/180 [####################] 大妖精: HP 122/160 [#################---] --- 第 2 回合 --- ...当一方 HP 降到 0 时战斗结束并输出获胜者。5. 程序运行流程分析这一节我们用文字和图的方式拆解战斗系统的运行流程方便你彻底理解整个程序。5.1 启动阶段程序入口是main()函数它会调用create_cirno()创建角色“琪露诺”。调用create_daiyousei()创建角色“大妖精”。创建一个Battle实例传入两个角色。调用battle.start()启动战斗。这里要注意create_cirno()返回的是一个Character对象对象内部包含了名字、属性、技能列表。这些数据全部封装在对象内部外部调用者不需要关心这些属性是怎么存储的。5.2 战斗主循环start()方法内部有一个while循环循环条件判断is_finished()是否为 False。每一轮的流程如下回合数round_number加 1。先手方行动。判断战斗是否结束如果结束就 break。后手方行动。判断战斗是否结束如果结束就 break。这里有一个细节先手方行动后如果直接打倒了后手方就不需要再执行后手方的回合了。所以每次行动后都调用is_finished()检查。5.3 伤害计算流程伤害计算在calculate_damage方法中完成基础伤害 攻击力 * 技能倍率 实际伤害 基础伤害 - 防御力 最终伤害 max(实际伤害, 1)举一个具体例子琪露诺攻击力 28大妖精防御力 12。使用普通攻击倍率 1.0时基础伤害为 28。实际伤害为 28 - 12 16。如果使用“冰符「Icicle Fall」”倍率 1.8基础伤害为 28 * 1.8 50.4。实际伤害为 50.4 - 12 38.4取整后最终伤害为 38。这个公式虽然简单但已经能体现出攻击力、防御力、技能倍率三个要素之间的联动关系。如果要增加暴击机制、属性克制可以在这个方法里继续扩展。6. 常见问题与排查思路在写这个项目的过程中新手可能会遇到各种问题。我整理了一份常见的报错排查清单。问题现象常见原因解决思路ModuleNotFoundError: No module named skill运行入口文件的位置不对Python 找不到模块确认main.py、skill.py、character.py、battle.py在同一个目录下AttributeError: Character object has no attribute name类初始化方法里属性名写错或者没有创建对象直接用了空对象检查__init__中是否定义了self.name调用前确认对象初始化成功角色血量变负数伤害结算没有做下限保护检查take_damage是否存在max(1, amount)或其他保护逻辑治疗超过最大生命值回血公式没有限制在heal方法中增加if self.hp self.max_hp: self.hp self.max_hp速度相同但先手顺序不稳定没有处理速度相同的分支在get_attack_order中增加随机打乱逻辑技能命中率为 1.0 时依然打空随机数判断写反了检查if random.random() skill.hit_rate这段逻辑命中率 1.0 时random.random()不会大于 1运行后秒退看不到结果IDE 或终端窗口自动关闭在main函数末尾增加input()等待用户输入或者直接在终端中运行修改了角色属性但战斗结果没变化新角色对象没有传给Battle检查Battle(cirno, daiyousei)传入的变量是否为最新创建的对象6.1 模块导入失败的完整排查流程这是最常见的一类问题。假设你运行python main.py时报错ModuleNotFoundError: No module named skill按以下步骤排查第一步确认当前目录下有哪些文件。在终端运行ls -l第二步确认文件确实是.py后缀并且名字正确。第三步确认main.py中导入语句是否写对from skill import Skill, SkillType第四步确认终端当前目录和项目目录一致。如果不一致使用cd命令切换目录。第五步检查是否有大小写问题。Python 对文件名的大小写敏感Skill.py和skill.py是两个不同的模块。6.2 为什么角色属性修改后没有生效这个问题通常在调试过程中出现。比如你把create_cirno中的attack改成 50重新运行后却发现伤害没有提升。最常见的原因是你启动运行的还是旧的.pyc缓存文件。Python 会在__pycache__目录下缓存编译后的字节码。如果代码修改后没有重新编译可能加载到旧缓存。解决方案也很简单删除项目目录下的__pycache__文件夹然后重新运行rm -rf __pycache__ python main.py或者直接在 IDE 里执行Clean操作。7. 进阶扩展方向这个版本的程序已经能完成一场完整的对战但它仍然是一个“能跑的最小原型”。要让项目更完善、更有学习价值可以从下面几个方向继续扩展。7.1 添加玩家交互目前的版本中角色是随机选择技能的。实际的对战游戏应该让玩家自己选择技能。改造方式非常简单。在take_turn方法中把随机选择改成控制台输入def choose_skill(self, character): 让玩家通过控制台选择技能 print(f {character.name} 的回合请选择技能) for i, skill in enumerate(character.skills): print(f {i 1}. {skill.name}) while True: try: choice int(input( 请输入技能编号)) if 1 choice len(character.skills): return character.skills[choice - 1] else: print( 输入无效请重新输入。) except ValueError: print( 请输入数字编号。)7.2 增加暴击和闪避在calculate_damage中加入暴击逻辑def calculate_damage(self, attacker, defender, multiplier, crit_rate0.1): base_damage attacker.attack * multiplier damage base_damage - defender.defense if random.random() crit_rate: damage * 1.5 print( 暴击) return max(1, int(damage))闪避逻辑可以在use_skill的命中判断中结合速度属性。7.3 增加多角色和队伍系统现在的代码只支持 1v1。如果要支持 2v2 或者角色组队可以抽象出Team类管理一个角色列表。战斗逻辑就需要从“两个角色轮流行动”改为“每轮按速度从两个队伍中各选一个角色行动”。7.4 增加存档和日志系统用json模块把每场战斗的数据保存下来赛后可以复盘。这也是提升工程能力的一个好练习。import json def save_battle_log(rounds, winner): log { rounds: rounds, winner: winner, } with open(battle_log.json, w, encodingutf-8) as f: json.dump(log, f, ensure_asciiFalse, indent2)8. 最佳实践与工程建议项目虽然不大但写作过程中有不少值得固化为习惯的工程经验这里分享给你。8.1 模块职责分离现在我打开很多初学者项目最常见的问题是一个.py文件写了 500 行里面既有角色初始化又有战斗逻辑还有打印信息最后 main 函数里还夹着一堆 if-else。这个项目虽然规模小但从一开始就保持模块分离的习惯skill.py只负责技能的定义。character.py只负责角色的状态和属性。battle.py只负责战斗流程。main.py只负责组装和启动。这样在后续扩展时你可以只改battle.py增加新的战斗机制而不必碰character.py。8.2 使用枚举替代魔法字符串代码里使用SkillType.DAMAGE而不是damage是一个很重要的细节。魔法字符串直接写在代码里的字符串常量在大型项目中很容易引发低级错误比如大小写写错、中英文引号混用等。枚举的好处是集中定义常量编译器/解释器能在运行前提高发现问题的概率。即使不是这个项目以后写任何 Python 代码都建议用enum.Enum管理一组固定选项。8.3 防御性编程我在take_damage里加了最小伤害保护在heal里加了最大生命限制这两个看似“多余”的检查正是防御性编程思想的体现。实际项目中外部传入的参数不一定可控。一个防御力大于攻击力的对手、一个溢出治疗的技能都可能让程序出现“负数伤害”“超上限血量”这种诡异状态。好的代码应该在任何情况下都保持状态的一致性。8.4 函数命名要表达意图命名是编程中最重要的可读性工程之一。is_alive()直接表达了“是否存活”。take_damage(amount)表达了“受到伤害”。get_attack_order()表达了“获取攻击顺序”。is_finished()表达了“战斗是否结束”。如果一个函数名需要看函数体才能猜到它的作用说明命名还有优化空间。8.5 善用__str__和打印格式化在Skill类中我们实现了__str__方法def __str__(self): return f{self.name}{self.description}这个方法的优势在于当你在日志里打印skill对象时输出的不再是一段看不懂的skill.Skill object at 0x7f...而是技能的可读名称。这个习惯在调试和日志记录中特别有价值。8.6 生产环境的版本依赖管理虽然这个项目不依赖第三方库但一旦以后引入外部依赖就应该在项目根目录创建requirements.txt# 示例 requirements.txt后续有第三方库时使用 # requests2.31.0实际项目中锁死依赖版本是避免环境不一致的通用做法。不同机器上如果依赖版本不同经常会出现“在我电脑上能跑在你电脑上报错”的尴尬情况。9. 总结这篇文章从一个趣味项目“在雾之湖举行对打比赛的⑨”出发把它落地成了 Python 面向对象编程的完整实战案例。我们一步步实现了技能类Skill用枚举区分伤害、治疗、增益三种技能类型。角色类Character封装血量、攻击、防御、速度等属性并提供受伤、治疗、状态显示方法。战斗类Battle包含出手顺序、回合循环、技能释放、胜负判定。入口模块main.py创建两位选手并启动战斗。整个项目全部使用 Python 标准库实现不需要安装任何第三方包复制代码后就能直接运行。如果你正在学习 Python建议不要只是“看”完这篇文章而是亲手把代码敲一遍然后尝试做以下练习把角色改成你自己设定的两个角色调整属性数值。给角色增加第三个技能并观察战斗结果的变化。修改take_turn方法让玩家能手动选择技能。增加一个“防御”指令让角色在本回合减少受到的伤害。把整场战斗写入日志文件复盘战斗结果。动手修改这些代码比阅读任何教程都更能加深理解。如果过程中遇到问题欢迎对照本文的排错章节逐步排查。希望这个“雾之湖对打比赛”项目能帮你真正掌握 Python 面向对象编程的核心思路。