C++实现碰手指游戏:揭秘后手必胜策略与博弈算法

发布时间:2026/7/31 17:21:11
C++实现碰手指游戏:揭秘后手必胜策略与博弈算法 1. 项目概述一个看似简单却暗藏玄机的博弈游戏最近在整理一些经典的小游戏项目时翻到了一个非常有意思的“碰手指游戏”。这个游戏的规则简单到令人发指两个人轮流报数每次可以报1到3个数谁先报到10谁就输。听起来是不是有点像简化版的“抢30”但它的策略核心却截然不同而且有一个反直觉的结论——在这个“到10即消”的规则下实际上是后手玩家拥有必胜策略。这个结论让很多第一次接触的朋友都感到惊讶明明感觉先手更有主动权怎么就后手必胜了呢为了把这个有趣的数学博弈和编程实践结合起来我决定用C完整地实现一遍这个游戏并且把必胜策略的逻辑也内置到AI对手中。这样无论是想理解背后的数学原理还是想获得一个可运行、可学习的C小游戏源码都能得到满足。这个小项目非常适合C初学者用来练手它涵盖了基本的输入输出、控制流、函数封装以及简单的算法逻辑代码清晰没有复杂的第三方依赖在任何一个C开发环境里都能轻松跑起来。2. 游戏规则与必胜策略的数学原理拆解2.1 “到10即消”的核心规则解析我们先把这个游戏的规则说得再明确一些这是所有分析的起点。参与方两名玩家可以是人vs人人vs电脑或者电脑vs电脑。数字序列一个从1开始按顺序递增的整数序列。我们关注的是报到数字“10”的那一刻。行动规则双方轮流进行。在自己的回合必须且只能选择报出1个、2个或3个连续的数字。例如当前数字报到了5那么当前玩家可以报“6”、或“6,7”、或“6,7,8”。胜负判定谁报出数字“10”谁就立即输掉游戏。注意是“报出”数字10而不是“累计报到”10。比如当前数字是9轮到玩家A如果他选择报“10”那么他直接告负如果他选择报“9,10”同样也是输因为10包含在其中。这个规则的关键在于“到10即输”它把10变成了一个“必败点”。我们的目标就是避免自己踏入这个必败点同时想方设法把对手逼进去。2.2 逆向推导为什么后手必胜很多博弈问题都可以通过“逆向归纳法”来寻找必胜策略。我们从终点输的点开始倒着推哪些点是“安全”的。确定必败点N点首先数字10是一个绝对的必败点记作 N-Position。因为谁走到10轮到他报数他就输了。寻找必胜点P点如果一个点可以通过一次合法操作报1-3个数将对手送入一个必败点N点那么这个点就是必胜点P-Position。逆向推导过程数字10是N点必败。看数字9从9出发可以报“10”1个数直接将对手送入必败点10。所以9是P点必胜。谁走到9谁就赢了。看数字8从8出发可以报“9”1个数将对手送入必胜点9这对我们不利。可以报“8,9”2个数或“8,9,10”3个数都会包含9或10。我们发现无论怎么走都会把对手送到9P点或者自己直接报10N点。所以8是N点必败。看数字7从7出发可以报“8”1个数将对手送入必败点8。所以7是P点必胜。看数字6从6出发可以报“7”1个数送对手到P点报“6,7”2个数送对手到P点报“6,7,8”3个数送对手到N点8。存在一种选择报3个数可以将对手送入N点所以6是P点。看数字5从5出发报“6”送P点“5,6”送P点“5,6,7”送P点。所有选择都指向对手的P点所以5是N点。依次类推4可以报“5”送N点所以4是P点。3报“4”送P点“3,4”送P点“3,4,5”送N点。存在送N点的选择所以3是P点。2报“3”送P点“2,3”送P点“2,3,4”送P点。全是P点所以2是N点。1报“2”送N点所以1是P点。0游戏起点可以看作报“1”送P点“1,2”送N点“1,2,3”送P点。存在送N点的选择所以起点0是P点。注意这里的“点”指的是游戏开始前或某一轮开始前下一个将要报的数字的前一个数字。例如当数字7刚被报出下一个该从8开始报我们说当前“站在”点7上。推导结论表格数字点类型说明0P点 (必胜)游戏起点1P点2N点 (必败)3P点4P点5N点6P点7P点8N点9P点10N点 (终局必败)这个表格揭示了规律N点必败点是2、5、8、10。它们之间相差3。而10是最终的N点。后手必胜策略游戏从0开始0是P点必胜点。先手玩家从0开始行动他一定会将局面从一个P点变为一个N点因为从P点出发所有选择都包含走向N点的可能但先手可能不知道策略而走错。而后手玩家的任务就是无论先手怎么走都通过自己的操作将局面重新拉回到下一个N点2,5,8上直到最后将先手逼到10这个终极N点。例如先手报“1”走到点1N点不点1是P点他离开了起点0这个P点但没走到N点。后手应该报“2,3”2个数走到点3不对这样局面是点3P点策略不对。正确后手策略是先手从0报“1”后当前数字是1下一个该报2。后手应该报“2”1个数这样报出的数字是2局面走到点2N点。完美。先手报“1,2”走到点2N点。后手发现局面已经是N点对自己不利此时他无法一步将局面扭转到下一个N点因为从N点出发所有走法都会送对手到P点。所以如果先手第一步就走到了N点如2后手就失去了必胜保障先手反而有机会赢。但先手如果不懂策略很难第一步就精准走到N点。因此在双方都完全理性、知晓必胜策略的情况下游戏起点0是必胜点所以先手玩家是必胜的。但是我们通常假设先手玩家不了解这个策略而后手玩家AI知道。那么后手AI就可以通过策略保持不败并且一旦先手犯错后手就能必胜。这就是“后手必胜”说法的常见语境面对一个不知道策略的玩家后手的AI是必胜的。2.3 策略算法化如何让AI“聪明”起来根据上面的推导AI后手的核心策略可以总结为目标总是试图在自己行动后使累计报出的最后一个数字是“必败点N”即2,5,8,10。因为10是终局所以前期目标是2,5,8。方法计算当前距离下一个必败点还有多少个数。设当前最后一个已报数字为current下一个必败点序列为[2,5,8,10]。找到第一个大于current的必败点target。计算差值diff target - current。如果diff的值在1到3之间那么AI就报diff个数正好走到target。如果diff大于3说明当前离下一个必败点还远AI可以随机报1-3个数或者采用一种保守策略比如总是报1个数等待对手犯错。因为只要对手不走到必败点局面就还在AI的控制节奏之外但AI也没犯错误。如果diff等于0这不可能因为如果current已经是必败点那应该是对手的回合AI不会在必败点上行动。随机性当diff大于3时AI的随机行动可以增加游戏的可玩性避免模式固定。同时如果玩家也懂得策略那么游戏就会进入“拼失误”的环节谁先偏离必败点序列谁就输。3. C游戏实现详解与源码剖析接下来我们把这个策略用C实现出来。项目结构清晰主要包含游戏状态管理、玩家回合处理、AI逻辑和主循环。3.1 游戏核心类的设计我们将游戏逻辑封装在一个类中这样状态清晰易于管理。// FingerTouchGame.h #ifndef FINGERTOUCHGAME_H #define FINGERTOUCHGAME_H #include vector #include string class FingerTouchGame { public: // 游戏模式枚举 enum class GameMode { PvP, // 玩家对玩家 PvC, // 玩家对电脑 (玩家先手) CvC // 电脑对电脑 (演示用) }; // 构造函数初始化游戏 explicit FingerTouchGame(GameMode mode GameMode::PvC); // 开始游戏主循环 void StartGame(); // 获取当前最后一个数字 int GetCurrentNumber() const; // 获取当前玩家 (1 或 2) int GetCurrentPlayer() const; // 判断游戏是否结束 bool IsGameOver() const; // 获取赢家 (0: 未结束, 1: 玩家1, 2: 玩家2) int GetWinner() const; private: GameMode mode_; // 游戏模式 int currentNumber_; // 当前最后一个已报出的数字初始为0 int currentPlayer_; // 当前行动玩家 (1 或 2) bool gameOver_; // 游戏结束标志 int winner_; // 获胜者 // 必败点序列 const std::vectorint losingPositions_ {2, 5, 8, 10}; // 处理玩家回合 (人类) void ProcessHumanTurn(); // 处理电脑回合 (AI) void ProcessComputerTurn(); // 执行报数操作并更新状态 // 参数count: 本次要报的数字个数 (1,2,3) // 返回值: 是否操作成功 (例如报数后是否导致游戏结束) bool PerformMove(int count); // AI决策根据策略返回应该报的数字个数 (1,2,3) int MakeAIDecision() const; // 显示当前游戏状态 void DisplayGameState() const; // 显示获胜信息 void DisplayWinner() const; // 验证输入是否合法 (1,2,3 且不超过10) bool IsValidMove(int count) const; }; #endif // FINGERTOUCHGAME_H设计思路解析状态集中管理所有游戏状态当前数字、当前玩家、是否结束等都作为类的成员变量避免使用全局变量提高了代码的封装性和可维护性。模式支持通过GameMode枚举支持三种模式方便测试和演示。PvC模式是我们重点其中电脑作为后手应用必胜策略。核心数据losingPositions_存储了计算好的必败点序列AI决策函数会查询这个序列。操作与显示分离PerformMove只负责更新内部状态DisplayGameState负责显示符合单一职责原则。3.2 核心逻辑的实现让我们看看关键函数的实现特别是AI决策和游戏主循环。// FingerTouchGame.cpp #include “FingerTouchGame.h” #include iostream #include cstdlib #include ctime #include limits // 初始化随机种子 namespace { bool srandCalled false; void InitRandom() { if (!srandCalled) { std::srand(static_castunsigned int(std::time(nullptr))); srandCalled true; } } } FingerTouchGame::FingerTouchGame(GameMode mode) : mode_(mode), currentNumber_(0), currentPlayer_(1), gameOver_(false), winner_(0) { InitRandom(); // 确保随机数生成器已初始化 } void FingerTouchGame::StartGame() { std::cout “\n 碰手指游戏 (到10即输) \n”; std::cout “规则两人轮流报数每次可报1-3个连续数字。谁报到数字10谁输\n”; while (!IsGameOver()) { DisplayGameState(); switch (mode_) { case GameMode::PvP: ProcessHumanTurn(); // 总是人类回合 break; case GameMode::PvC: if (currentPlayer_ 1) { ProcessHumanTurn(); // 玩家1是人类 } else { ProcessComputerTurn(); // 玩家2是电脑 } break; case GameMode::CvC: ProcessComputerTurn(); // 两个都是电脑 break; } // 切换玩家 if (!gameOver_) { currentPlayer_ (currentPlayer_ 1) ? 2 : 1; } } DisplayWinner(); } // AI决策的核心函数 int FingerTouchGame::MakeAIDecision() const { // 找到下一个必败点 int target -1; for (int pos : losingPositions_) { if (pos currentNumber_) { target pos; break; } } // 如果找到了下一个必败点计算差值 if (target ! -1) { int diff target - currentNumber_; if (diff 1 diff 3) { // 完美可以一步走到必败点 return diff; } } // 如果无法一步走到必败点或者已经过了所有必败点理论上不会因为到10游戏结束则随机走 // 这里可以加入一些简单策略比如避免直接报出9送对手到必胜点 // 简单实现随机1-3 return (std::rand() % 3) 1; // 生成1,2,3的随机数 } void FingerTouchGame::ProcessComputerTurn() { std::cout “\n- 玩家” currentPlayer_ “ (电脑) 正在思考...\n”; // 可以加一个短暂延迟模拟思考过程 // std::this_thread::sleep_for(std::chrono::milliseconds(800)); int move MakeAIDecision(); std::cout “电脑决定报 ” move “ 个数。” std::endl; PerformMove(move); } void FingerTouchGame::ProcessHumanTurn() { int move 0; bool validInput false; while (!validInput) { std::cout “\n- 玩家” currentPlayer_ “请输入要报的数字个数 (1, 2, 3): “; std::cin move; // 处理非数字输入导致的cin错误状态 if (std::cin.fail()) { std::cin.clear(); // 清除错误状态 std::cin.ignore(std::numeric_limitsstd::streamsize::max(), ‘\n’); // 忽略错误输入行 std::cout “输入错误请输入一个数字。\n”; continue; } std::cin.ignore(std::numeric_limitsstd::streamsize::max(), ‘\n’); // 忽略换行符 if (IsValidMove(move)) { validInput true; } else { std::cout “无效的输入请输入1, 2 或 3并且报数后不能超过10。\n”; } } PerformMove(move); } bool FingerTouchGame::PerformMove(int count) { // 报数 std::cout “报数: “; for (int i 1; i count; i) { std::cout (currentNumber_ i); if (i count) std::cout “, “; } std::cout std::endl; currentNumber_ count; // 检查游戏是否结束 if (currentNumber_ 10) { gameOver_ true; winner_ (currentPlayer_ 1) ? 2 : 1; // 当前报出10的玩家输所以赢家是另一方 std::cout “\n玩家” currentPlayer_ “ 报出了数字10\n”; return true; } return false; } // 其他辅助函数实现... bool FingerTouchGame::IsValidMove(int count) const { if (count 1 || count 3) return false; if (currentNumber_ count 10) return false; // 报数后不能超过10 return true; } void FingerTouchGame::DisplayGameState() const { std::cout “\n——————————\n”; std::cout “当前数字: “ currentNumber_ std::endl; std::cout “下一个数字从 ” (currentNumber_ 1) “ 开始\n”; std::cout “轮到玩家 ” currentPlayer_; if (mode_ GameMode::PvC currentPlayer_ 2) { std::cout “ (电脑)”; } std::cout “ 行动\n”; }代码要点解析AI决策 (MakeAIDecision)这是实现必胜策略的核心。它遍历预设的必败点序列找到第一个比当前数字大的点作为目标。计算差值diff如果在1-3之间就返回diff实现最优走法。否则随机返回1-3。这个简单的逻辑使得后手AI在面对不懂策略的玩家时几乎立于不败之地。输入验证与鲁棒性在ProcessHumanTurn中我们不仅检查输入数字是否在1-3且不超过10还处理了std::cin.fail()的情况比如用户输入了字母。这是编写交互式控制台程序时必须注意的细节能防止程序因意外输入而崩溃。游戏状态更新 (PerformMove)这个函数负责核心的状态转移。它输出报数过程更新currentNumber_并检查是否触发游戏结束条件currentNumber_ 10。注意这里用的是10而不是10是一种防御性编程防止意外逻辑错误导致数字跳过10。主循环 (StartGame)清晰地根据游戏模式分发回合处理逻辑并在每轮结束后切换当前玩家直到游戏结束。3.3 主函数与编译运行一个简单的main.cpp来启动游戏// main.cpp #include “FingerTouchGame.h” #include iostream int main() { int choice 0; std::cout “选择游戏模式:\n”; std::cout “1. 玩家 vs 电脑 (推荐体验后手AI策略)\n”; std::cout “2. 玩家 vs 玩家\n”; std::cout “3. 电脑 vs 电脑 (演示)\n”; std::cout “请输入选择 (1-3): “; std::cin choice; FingerTouchGame::GameMode mode; switch (choice) { case 1: mode FingerTouchGame::GameMode::PvC; break; case 2: mode FingerTouchGame::GameMode::PvP; break; case 3: mode FingerTouchGame::GameMode::CvC; break; default: std::cout “无效选择默认使用玩家vs电脑模式。\n”; mode FingerTouchGame::GameMode::PvC; break; } FingerTouchGame game(mode); game.StartGame(); std::cout “\n游戏结束谢谢游玩\n”; return 0; }编译与运行 这是一个标准的C控制台项目。你可以使用任何喜欢的IDE或命令行工具编译。GCC/Clang命令行g -stdc11 main.cpp FingerTouchGame.cpp -o finger_gameVisual Studio创建一个空项目添加这三个.cpp文件编译运行即可。运行后选择模式1玩家vs电脑你就可以作为先手挑战那个拥有“后手必胜”策略的AI了。看看你能坚持多久4. 项目扩展思路与编程技巧探讨实现基础版本后我们可以从多个角度对这个项目进行扩展这不仅能增加趣味性也能深化对C和博弈论的理解。4.1 游戏规则变体与策略泛化“到10即消”只是这类“取物游戏”或“减法游戏”的一个特例。我们可以抽象出一个更通用的模型总数字N不是10可以是任意正整数N比如21点游戏。每次可取范围不是1-3可以是1到kk为任意正整数。胜负目标可以是“取到最后一个输”Misère规则如本项目也可以是“取到最后一个赢”Normal规则。对于通用模型总数为N每次取1~k个取最后一个输其必胜策略的规律是第一个必败点是 (k1) 的倍数吗不一定。对于“取最后一个输”其必败点序列的通项公式会更复杂一些但依然可以通过动态规划逆向推导出来。我们可以修改程序让用户输入N和k然后程序自动计算并应用必胜策略。这涉及到算法从“硬编码”到“动态计算”的升级。4.2 AI难度分级与机器学习初探目前的AI是“完美”的在它知道策略的情况下。我们可以引入难度分级简单AI完全随机走法。中等AI在基础策略上加入一定概率的“失误”比如有20%的概率不执行最优走法而是随机走这样人类玩家有机会赢。困难AI即当前的完美策略AI。学习型AI挑战可以尝试用简单的强化学习如Q-learning来训练AI。状态是当前数字动作是报1/2/3个数奖励在输的时候为负赢的时候为正。让AI自我对弈成千上万局最终它也能学习到类似的必胜策略。这对于C初学者来说是个不小的挑战但可以作为了解AI入门的一个有趣方向。4.3 代码优化与工程化实践对于这个小项目我们还可以在代码质量上做些文章使用智能指针管理资源虽然当前项目没有动态资源但可以设想如果游戏状态更复杂引入std::unique_ptr来管理某些对象是良好的习惯。将策略算法抽象为接口定义一个IGameStrategy纯虚类然后派生出RandomStrategy、PerfectStrategy、LearningStrategy等。游戏主类持有一个策略接口的指针通过注入不同的策略实现来改变AI行为。这大大增强了代码的扩展性和可测试性。加入单元测试使用Google Test等框架为IsValidMove、PerformMove、MakeAIDecision等核心函数编写测试用例确保逻辑正确。特别是AI决策函数可以测试其在各种局面下当前数字为1,4,7等是否做出了预期中的最优选择。图形化界面用Qt、SFML或甚至控制台图形库如ncurses为游戏制作一个简单的图形界面用进度条或高亮数字显示当前状态提升用户体验。4.4 常见的编译与运行问题排查对于初学者在尝试编译运行此代码时可能会遇到一些问题这里集中列举一下头文件找不到确保FingerTouchGame.h和FingerTouchGame.cpp、main.cpp在同一个目录下或者正确配置了项目的包含路径。std::this_thread::sleep_for未定义如果取消注释AI思考延迟的那行代码需要包含thread和chrono头文件并且编译器需要支持C11及以上标准。编译时加上-stdc11参数。随机数每次一样我们使用了std::srand(std::time(nullptr))来初始化随机种子。如果在极短时间内多次运行程序time(nullptr)可能返回相同的值导致随机序列相同。这在演示中问题不大。对于要求更高的场景可以考虑使用C11的random库。输入缓冲问题我们在ProcessHumanTurn中使用了std::cin.ignore(...)来清除输入缓冲区。这是为了避免之前输入残留的换行符影响后续读取。如果发现输入行为异常可以检查这里的逻辑。游戏逻辑感觉不对请再次核对规则“谁报到10谁输”。有时容易和“谁报到10谁赢”混淆。必胜策略是完全相反的。确保你的理解和代码中的判定条件currentNumber_ 10匹配的是“输”的规则。这个“碰手指游戏”项目虽小却串联起了数学博弈、算法策略和C面向对象编程。通过动手实现它你不仅能获得一个有趣的游戏更能深入理解“必胜策略”背后的逆向推导思维以及如何将数学模型转化为可靠的代码。希望这份详细的源码和解析能给你带来启发。