
1. 项目概述为什么选择C重写三国杀作为一个玩了十几年三国杀也写了十几年C的老程序员我一直有个执念能不能用C从零开始把三国杀这个经典卡牌游戏的核心逻辑完整地实现一遍并且给它加上一个能打的AI对手这个想法源于两个痛点。第一市面上很多三国杀的开源实现比如我之前研究过的Java版本虽然功能完整但代码结构庞大动辄上万个类对于想深入理解游戏状态机、卡牌效果结算和AI决策逻辑的开发者来说入门门槛太高像走进了一个迷宫。第二用C来实现是对自己基本功的一次“大考”。它不像Java或Python有丰富的现成容器和GC垃圾回收你需要自己管理内存、设计高效的数据结构来处理复杂的游戏状态还要构建一个清晰的、可扩展的面向对象框架来应对上百个武将技能和卡牌效果——这本身就是极具挑战性和成就感的事情。这个项目的目的绝不仅仅是“能运行”而已。我希望通过它深入剖析几个核心问题一个非对称、多角色、带技能和装备的卡牌游戏其底层状态机如何设计才能既清晰又高效卡牌效果特别是像【无中生有】、【南蛮入侵】这种影响多个目标的锦囊的结算顺序和连锁反应在代码层面如何优雅地实现更重要的是如何为这个高度不确定性的游戏设计一个AI这个AI不仅要懂得基本的出杀、闪避还要能权衡手牌资源、预判对手意图、甚至进行简单的“伪装”和“欺诈”比如手握【桃】却故意不出引诱对手集火。最终产出的会是一个完整的、可编译运行的控制台CLI程序支持多名玩家与AI同台竞技并且代码结构清晰注释完整可以作为学习C面向对象设计、状态模式和简单AI算法的绝佳案例。2. 核心架构设计如何用C搭建游戏骨架用C从零开始意味着一切都要自己搭建。一个好的架构是项目成功的基石它能确保你在添加第100个武将技能时不会因为之前的代码耦合度过高而崩溃。2.1 核心类的职责划分整个游戏世界由几个核心类构成它们各司其职通过清晰的接口进行通信。Game (游戏引擎)这是总指挥单例模式是最佳选择。它持有游戏的所有全局状态当前回合玩家、游戏阶段准备、进行中、结束、胜利条件等。它的核心是一个驱动游戏循环的run()方法按照“回合开始 - 判定 - 摸牌 - 出牌 - 弃牌 - 回合结束”的顺序推进。同时它也是事件总线Event Bus的中心任何游戏内事件如“一名角色即将受到伤害”、“一张牌即将被使用”都从这里发布供其他模块监听和响应。Player (玩家/角色基类)这是所有场上角色的抽象。它包含一些基本属性体力值、体力上限、手牌列表、装备区武器、防具、坐骑、判定区牌、角色状态是否横置、是否翻面。更重要的是它定义了一系列“时机”Phase的虚函数如onTurnStart(),onUseCard(),onDamaged()等。人类玩家和AI玩家都将继承自这个类人类玩家重写这些函数以提供UI交互AI玩家则重写以实现自动决策。Card (卡牌基类)所有卡牌基本牌、锦囊牌、装备牌的父类。它至少包含花色、点数、名称、卡牌类型等属性。最关键的是它有一个纯虚函数bool effect(Game* game, Player* user, const std::vectorPlayer* targets)。这个函数定义了这张牌的效果如何生效。例如【杀】的effect会检查距离、是否被【闪】响应然后造成伤害【桃】的effect会为目标回复1点体力。Skill (技能接口)为了应对武将技能的多样性我们需要将技能也对象化。定义一个Skill基类包含技能名称、发动时机如“当你使用【杀】指定目标后”、“当你成为【杀】的目标时”、发动条件等。每个武将持有一个Skill对象的列表。在Game引擎推进到特定时机时会遍历所有角色的技能列表检查条件并触发相应的效果。这比用巨大的switch-case或if-else链来处理技能要优雅和可扩展得多。AIController (AI控制器)这是AI的大脑它持有一个Player对象的引用。AI的所有决策包括出牌、选择目标、发动技能都由这个控制器根据当前的游戏状态来计算。我们可以设计不同难度的AI比如“简单AI”只会出杀和闪“中级AI”会计算手牌价值和使用锦囊的收益“高级AI”甚至会尝试推测对手的手牌并制定策略。注意在C中由于没有原生的反射机制技能和卡牌的效果函数通常需要通过一个全局的注册表Registry或工厂模式Factory Pattern来动态创建。例如我们可以用一个std::mapstd::string, std::functionCard*()来映射卡牌名和其构造函数这样在从配置文件中读取“杀”时就能动态创建出ShaCard对象。2.2 游戏状态与事件驱动模型三国杀是一个状态复杂的游戏。一个【乐不思蜀】的判定可能触发司马懿的“反馈”、张角的“鬼道”、诸葛亮的“观星”这些技能又会改变判定牌进而影响最终结果。如果用线性的、过程式的代码来写很快就会变成一堆难以维护的“面条代码”。我的解决方案是采用事件驱动模型。定义一套完整的事件枚举例如Event::CardUsed: 卡牌被使用。Event::CardTargeted: 卡牌指定了目标。Event::DamageCaused: 伤害即将造成。Event::DamageInflicted: 伤害即将承受。Event::JudgeStarted: 判定开始。Event::JudgeFinished: 判定结束。Game类维护一个std::vectorstd::pairEvent, std::functionvoid(EventData)的事件监听器列表。任何对象如一个技能都可以在初始化时向游戏引擎注册“当Event::DamageCaused事件发生时请调用我的这个回调函数”。当游戏流程进行到相应节点时Game引擎就创建一个EventData结构体里面包含了事件相关的所有上下文信息如来源、目标、卡牌指针等然后遍历监听器依次调用回调。这样司马懿的技能只需要注册对Event::JudgeStarted的监听在回调里检查自己是否可以改判然后修改EventData中的判定牌即可。各个模块之间解耦得非常彻底新增技能就像插拔插件一样方便。3. 核心逻辑实现卡牌、技能与结算链架构搭好了接下来就是填充血肉实现那些让三国杀充满魅力的核心规则。3.1 卡牌系统的面向对象设计卡牌继承体系的设计至关重要。我采用了多层继承来平衡灵活性和代码复用。class Card { public: enum class Suit { SPADE, HEART, CLUB, DIAMOND }; // 花色 enum class Type { BASIC, TRICK, EQUIP }; // 类型 virtual ~Card() default; virtual Type getType() const 0; virtual bool isAvailable(const Game game, const Player user) const; // 检查使用合法性 virtual bool effect(Game game, Player user, const std::vectorPlayer* targets) 0; // 核心效果 protected: Suit suit_; int number_; std::string name_; }; class BasicCard : public Card { public: Type getType() const override { return Type::BASIC; } }; class ShaCard : public BasicCard { public: bool isAvailable(const Game game, const Player user) const override { // 检查距离、出杀次数、是否被“断肠”等 return /* ... */; } bool effect(Game game, Player user, const std::vectorPlayer* targets) override { // 1. 触发“使用杀时”技能事件 // 2. 请求目标打出【闪】 // 3. 若未打出触发“造成伤害时”事件然后结算伤害 return /* ... */; } }; class TrickCard : public Card { public: Type getType() const override { return Type::TRICK; } virtual bool isGlobal() const { return false; } // 是否是群体锦囊如南蛮、万箭 }; class NanmanCard : public TrickCard { public: bool isGlobal() const override { return true; } bool effect(Game game, Player user, const std::vectorPlayer* targets) override { // 遍历所有其他角色要求他们打出【杀】否则受到伤害 return /* ... */; } }; class EquipCard : public Card { public: Type getType() const override { return Type::EQUIP; } enum class SubType { WEAPON, ARMOR, HORSE_MINUS, HORSE_PLUS }; virtual SubType getSubType() const 0; virtual void onEquip(Player owner) 0; // 装备时效果 virtual void onUnequip(Player owner) 0; // 卸下时效果 };关键点effect函数是卡牌逻辑的核心。它需要接收Game和Player的引用因为卡牌效果几乎总是需要与游戏全局状态和其他玩家交互。返回值bool可以表示此牌的使用是否成功例如【顺手牵羊】被【无懈可击】响应了就可以返回false。3.2 技能系统的响应式实现技能作为独立的对象其核心是“在正确的时机做正确的事”。我们通过重写Player基类中的那些时机虚函数或者向游戏事件总线注册监听器来实现。以标准版张飞的技能“咆哮”为例锁定技你使用【杀】无次数限制class ZhangFeiSkill : public Skill { public: ZhangFeiSkill() : Skill(咆哮, SkillTiming::BEFORE_CARD_USE) {} void onBeforeUseCard(Game game, Player self, Card card) override { if (card.getName() 杀) { // 这是一个标记技能可以通过修改游戏规则或玩家状态来实现 // 更优雅的方式是在Game引擎检查出杀次数时如果玩家是张飞则跳过次数检查 // 我们可以在Player类中添加一个查询函数 } } };而以标准版司马懿的技能“反馈”为例当你受到伤害后你可以获得伤害来源的一张牌class SimaYiSkill : public Skill { public: SimaYiSkill() : Skill(反馈, SkillTiming::AFTER_DAMAGED) {} void onAfterDamaged(Game game, Player self, Player source, int damage) override { if (self.isAlive() source.isAlive() self.askForSkill(反馈)) { // 弹出选择界面AI或人类让司马懿选择获得伤害来源的手牌、装备区或判定区的一张牌 Card* cardToObtain game.requestCardFromPlayer(source, self); if (cardToObtain) { self.obtainCard(cardToObtain); game.broadcast(self.getName() 发动了【反馈】获得了 source.getName() 的 cardToObtain-getName()); } } } };设计心得将技能写成独立的类虽然会增加类的数量但好处是巨大的。第一可读性极强每个技能的代码都集中在一处。第二易于测试你可以单独为某个技能写单元测试。第三支持动态加载未来如果想做成支持MOD的游戏只需要将技能类编译成动态库在运行时加载即可。3.3 复杂的结算顺序与堆栈三国杀最复杂的部分莫过于结算顺序。多个技能、多个效果同时被触发时谁先谁后这里需要引入一个“结算堆栈”或“队列”的概念。例如A对B使用【杀】B使用【闪】响应。此时A装备了【青龙偃月刀】当你的【杀】被【闪】抵消时你可以对相同的目标再使用一张【杀】而B是陆逊拥有技能“谦逊”你不能成为【顺手牵羊】和【乐不思蜀】的目标但这里不触发。同时旁观者C是郭嘉有技能“遗计”每当受到1点伤害后你可以摸两张牌。这个简单的过程在代码中的结算链可能是这样的A声明使用【杀】指定B为目标。触发事件Event::CardUsed。B响应打出【闪】。触发事件Event::CardResponsed。【杀】被抵消。触发事件Event::CardNullified。此时检查A的装备和技能。发现【青龙偃月刀】的效果被触发其监听器在Event::CardNullified时检查条件如果满足则让A选择是否发动刀的效果。如果发动则流程回到第1步发起一次新的【杀】。如果A不发动刀或者新的【杀】也被闪则结算结束。如果B没有【闪】则进入伤害结算。触发Event::DamageCaused(A对B造成伤害)。此时B的防具如【八卦阵】、技能如曹操的“奸雄”可能会监听此事件。触发Event::DamageInflicted(B受到伤害)。此时B的技能如郭嘉的“遗计”、司马懿的“反馈”以及A的技能如典韦的“强袭”如果造成伤害会监听此事件。注意这些监听器的执行顺序需要规则定义通常是按当前回合角色逆时针顺序依次询问。伤害值最终扣除B的体力。如果B体力为0则进入濒死结算这是一个更复杂的子状态机。伤害结算完毕触发Event::DamageFinished。实现这个结算链我的做法是在Game类中维护一个std::stackstd::functionvoid()称为“结算堆栈”。当需要处理嵌套结算时例如在伤害结算中又触发了新的卡牌使用就把当前上下文压栈先处理新的事件处理完后再出栈继续。这能很好地模拟“后发先至”的结算规则类似于函数调用栈。4. AI对战模块的设计与实现有了一个能运行的游戏接下来就是赋予它灵魂——一个聪明的AI对手。三国杀的AI设计是典型的不完全信息博弈AI看不到对手的手牌需要根据游戏历史、角色技能、牌堆剩余牌型来进行概率推断和决策。4.1 AI决策框架状态评估与动作选择一个基本的AI决策循环可以概括为观察状态 - 评估价值 - 生成动作 - 选择最优。状态观察 (State Observation)AI需要从Game对象中提取所有它能看到的公开信息以及根据规则它能推断出的信息。这包括所有玩家的体力、体力上限、已装备的牌、判定区的牌、明牌如被【观星】或【神吕蒙】涉猎看到的牌。自己的手牌、技能。全局信息牌堆剩余数量、弃牌堆、当前回合阶段、游戏历史谁对谁出了什么牌。动作生成 (Action Generation)在当前状态下AI有哪些合法的操作这需要调用游戏规则引擎。例如在出牌阶段AI需要枚举所有可使用的卡牌考虑距离、次数限制。每张卡牌所有可能的目标组合例如【过河拆桥】可以选择场上任意一名其他角色的一张牌。所有可发动的技能考虑发动时机和条件。 这个枚举空间可能很大需要高效过滤。例如如果AI没有【杀】那么“使用杀”这个动作就直接跳过。状态评估 (State Evaluation)这是AI的大脑。我们需要一个评估函数float evaluate(const GameState state, int player_id)它从player_id的角度给当前游戏状态打一个分。分数越高表示这个状态对AI越有利。这个函数的设计是AI强弱的关键。一个简单的评估函数可以考虑以下因素生存能力自己的体力值越高越好对手的体力值越低越好。体力值为0是负无穷。资源价值手牌数量、手牌质量给每张牌一个基础价值分如【桃】5分【无中生有】4分【杀】1分。场面控制装备区的牌价值【诸葛连弩】在杀多时价值极高判定区的控制给对手贴【乐不思蜀】是正收益。角色技能潜力估算自己技能在未来回合的潜在收益。 这些因素需要赋予不同的权重并通过大量对局测试来调整。动作选择 (Action Selection)有了评估函数AI就可以用一些搜索算法来选择动作。对于三国杀这样分支因子大、回合数可能较长的游戏穷举搜索不现实。常用方法有贪婪算法 (Greedy)只考虑当前动作的即时收益。例如使用【桃园结义】如果能回复自己体力就立刻用。这是最简单、最快的AI。蒙特卡洛树搜索 (MCTS)这是目前在不完全信息博弈中比较先进的方法。通过随机模拟大量对局来评估某个动作的长期胜率。但实现复杂且需要大量的模拟次数才能保证效果。规则脚本 (Rule-based Script)为常见场景编写“if-then”规则。例如“如果我的体力低于2且手中有【桃】则使用【桃】”“如果对手没有手牌则优先使用【决斗】”。这种方法可解释性强容易调试是中级AI的实用选择。在我的实现中我采用了混合策略。底层是一个基于规则的脚本引擎处理大多数常规决策保证AI行为的合理性和可读性。在此基础上为关键决策如是否使用关键的【无懈可击】、如何分配【南蛮入侵】后的出杀目标引入一个轻量级的一步前瞻搜索模拟执行每个候选动作然后用评估函数给模拟后的状态打分选择分数最高的动作。4.2 具体AI行为实现示例让我们以“出牌阶段AI如何使用一张【过河拆桥】”为例拆解其决策过程动作生成AI遍历所有其他玩家对于每个玩家遍历其区域手牌、装备区、判定区。由于手牌是未知的AI只能以“未知牌”作为目标。所以生成的候选动作是[拆玩家B的手牌1, 拆玩家B的装备武器, 拆玩家C的判定区乐不思蜀, ...]。规则过滤首先应用一些硬性规则。例如如果对手有【八卦阵】且AI没有【顺手牵羊】那么拆掉【八卦阵】的优先级可能很高。如果对手被【乐不思蜀】且判定牌未知拆掉【乐不思蜀】可以防止其生效。这些规则可以直接排除或提升某些选项的优先级。价值评估对于剩余候选对于“拆玩家B的手牌”这种未知选项AI需要进行概率推断。它知道牌堆的剩余构成因为牌堆是全局对象AI可以查询。假设牌堆还剩30张牌其中【桃】有2张【无懈可击】有1张。那么从概率上讲B的手牌中有一张【桃】的概率并不高。但是AI可以结合游戏历史B刚才在濒死时被救活很可能有队友给了他【桃】或者他之前故意留牌。AI可以给每个玩家维护一个“手牌价值期望”根据其角色、玩法和历史行为动态调整。最终给“拆B手牌”这个动作赋予一个期望价值V P(关键牌) * Value(关键牌)。搜索与选择AI对几个高价值候选如拆B的武器拆C的乐不思蜀进行一步前瞻模拟。模拟“拆掉B的武器”后评估接下来的局面B的攻击距离缩短对我方威胁减小状态评分5。模拟“拆掉C的乐不思蜀”后C下回合可以出牌可能对我方造成威胁状态评分-2。于是AI选择“拆B的武器”。执行与学习AI执行动作。在游戏结束后如果这个对局被保存我们可以用更复杂的离线分析来评估这个决策的好坏从而微调规则权重或概率模型这就是简单的机器学习。避坑指南AI最容易犯的蠢事是“自杀式攻击”。比如自己只有1点体力却去决斗一个满血对手。在评估函数中必须将生存放在绝对最高的权重。可以给“自身体力值”一个指数增长的评分例如health_score pow(2, my_health)这样AI在低血量时会极端保守。另一个常见问题是AI不会“蓄爆”总是有多少牌出多少牌。可以在评估函数中加入“手牌数量”的评分并给一些高价值锦囊牌如【南蛮入侵】、【万箭齐发】设置一个“最佳使用时机”的阈值比如当对手数量大于2且自己血量安全时才考虑使用。5. 开发环境搭建、调试与性能优化用C做项目环境配置和性能调优是绕不开的坎。一个好的开发环境能极大提升效率而性能优化则决定了你的游戏是否能流畅运行复杂的AI计算。5.1 开发环境与工具链选择我强烈推荐使用Visual Studio Code (VSCode)配合CMake作为开发环境而不是庞大的Visual Studio IDE。VSCode轻量、跨平台通过插件可以获得媲美IDE的体验。编译器在Windows上安装MinGW-w64或MSVC。我更推荐MinGW-w64因为它更接近Linux环境避免了一些MSVC特有的怪异问题。确保将g.exe的路径添加到系统环境变量PATH中。VSCode插件C/C (Microsoft)提供智能提示、代码跳转、调试支持。CMake Tools管理CMake项目的配置、构建和调试。Code Runner一键运行单个文件方便测试小模块。CMake配置在项目根目录创建CMakeLists.txt。这是现代C项目的标配它管理编译选项、依赖库并生成你本地IDE如VSCode或编译器的项目文件。cmake_minimum_required(VERSION 3.10) project(SanguoshaCPP) set(CMAKE_CXX_STANDARD 17) # 使用C17标准 set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置编译选项调试信息、警告全开、优化等级 if(CMAKE_BUILD_TYPE STREQUAL Debug) add_compile_options(-g -Wall -Wextra -Werror) else() add_compile_options(-O2) endif() # 将源代码添加到可执行目标 add_executable(sanguosha src/main.cpp src/game.cpp src/player.cpp # ... 所有源文件 )构建与调试在VSCode中按F1输入CMake: Configure来配置项目然后CMake: Build进行编译。使用CMake: Debug可以启动调试会话你可以设置断点、查看变量、单步执行这对于调试复杂的游戏状态机和AI决策逻辑至关重要。5.2 常见编译与运行时问题排查即使环境配好了在开发过程中也一定会遇到各种“坑”。问题1undefined reference to ...链接错误这是最常见的问题意味着编译器找到了函数声明在.h头文件里但没找到函数定义在.cpp文件里。检查确保所有非模板的函数和类成员函数都在某个.cpp文件中有了实现。特别注意如果你在头文件中写了函数的默认实现在类定义内部那没问题。但如果你在头文件中只声明在类外定义比如inline void Foo::bar() {}那么每个包含此头文件的.cpp文件都会生成一份该函数的副本可能导致重复定义错误。最好将实现放在.cpp里。问题2复杂的模板导致的编译错误如果你使用了模板元编程或复杂的STL容器嵌套错误信息可能长达几百行。策略从错误信息的第一行和最后几行看起。第一行通常指出根本原因如“没有匹配的函数调用”最后几行指出错误发生的具体位置。简化将复杂的模板表达式拆分成多行使用using别名来简化类型名例如using CardVec std::vectorstd::shared_ptrCard;。问题3内存错误访问越界、使用空指针、内存泄漏C没有垃圾回收内存管理必须谨慎。智能指针对于有明确所有权的对象如Game拥有所有PlayerPlayer拥有其手牌使用std::unique_ptr。对于需要共享所有权的对象如一张牌可能被多个对象引用查看但只有牌堆真正拥有它使用std::shared_ptr。尽量避免使用裸指针 (Card*)。容器下标的检查在使用vector[index]或map[key]前养成检查index size()或find(key) ! end()的习惯。工具在Linux/macOS下使用valgrind检查内存泄漏。在Windows下可以使用Visual Studio自带的内存诊断工具或者Dr. Memory。问题4游戏逻辑Bug结算顺序错误、AI死循环这类Bug最难查因为可能涉及复杂的交互。日志系统实现一个简单的日志宏将游戏的关键步骤如“玩家A对B使用杀”、“触发技能反馈”输出到文件或控制台。通过日志可以清晰地复盘对局过程。#define GAME_LOG(...) \ do { \ std::ofstream logfile(game.log, std::ios::app); \ logfile __VA_ARGS__ std::endl; \ } while(0)单元测试对核心的、独立的模块写单元测试。例如测试Card的effect函数测试Game的回合推进逻辑。使用像Google Test这样的框架可以事半功倍。断言 (Assert)在代码中假设必须成立的地方使用assert例如assert(player ! nullptr Player pointer is null!);。在Debug模式下断言失败会立刻终止程序并指出位置能快速定位问题。5.3 性能优化要点一个回合制卡牌游戏对性能要求不高但如果你实现了复杂的AI搜索如MCTS性能就可能成为瓶颈。避免不必要的拷贝大量使用const T传递参数使用std::move转移所有权。特别是在评估函数中频繁拷贝GameState是性能杀手。可以考虑为GameState实现写时复制Copy-on-Write或者为AI搜索专门设计一个轻量级的、只读的GameStateView。数据结构的选择需要快速随机访问和遍历用std::vector。需要快速插入删除中间元素用std::list但实际中vector在元素不多时可能更快因为缓存友好。需要快速查找按武将名找技能对象用std::unordered_map(哈希表)。关键点std::vector在内存中是连续的CPU缓存命中率高遍历速度极快。在性能热点处如遍历所有玩家检查技能触发优先考虑std::vector。预计算与缓存有些信息是重复计算的。例如两个玩家之间的“距离”每次都用公式计算会很慢。可以在Game中维护一个距离矩阵当有玩家死亡或坐骑变化时才更新这个矩阵其他时候直接查表。AI搜索的剪枝这是提升AI速度最有效的方法。在生成动作时果断抛弃那些明显很差的选项例如对满血队友使用【桃】。在搜索树中如果当前分支的评估分数已经低于已知的最好分支就停止搜索这个分支Alpha-Beta剪枝的思想。6. 项目扩展与未来展望实现基础版本只是一个开始。这个项目就像一个乐高底座有无数有趣的扩展方向。图形界面 (GUI)控制台版本适合学习和测试核心逻辑但终究不够直观。你可以使用Qt或SFML这类跨平台的C图形库来构建一个图形界面。将Player、Card等核心类作为数据模型GUI只负责渲染和接收用户输入。这是一个将业务逻辑与表现层分离的绝佳练习。网络对战将游戏引擎改造成客户端-服务器架构。服务器 (GameServer) 运行核心逻辑处理所有规则判定。客户端 (GameClient) 只负责显示界面和发送用户操作。你需要设计一套网络协议来同步游戏状态如“玩家A出杀目标B”。这会涉及到网络编程、序列化用Protobuf或JSON、状态同步和延迟处理等更高级的主题。更强大的AI你可以尝试集成机器学习库如libtorchPyTorch的C前端让AI通过自我对弈Self-play来学习。为游戏状态设计一个神经网络特征提取器输出对每个合法动作的评估。这需要大量的数据和计算资源但做出来会非常有成就感。MOD支持与数据驱动将武将技能、卡牌效果从硬编码中解放出来用配置文件如YAML、JSON或脚本如Lua来定义。这样你甚至不需要重新编译程序就能添加新的扩展包。这要求你的核心引擎有一个非常灵活和强大的脚本接口。回顾整个项目用C实现三国杀是一次对编程基本功和软件设计能力的全面锻炼。你不仅是在实现游戏规则更是在设计一个易于维护、易于扩展的软件系统。从面向对象的设计模式到内存管理再到算法和AI每一个环节都充满了挑战和乐趣。当你最终看到自己写的AI在屏幕上做出一个精妙的操作时那种成就感是无与伦比的。这个项目的代码将会是你技术 portfolio 中一个非常亮眼的部分。