C#实现石头剪刀布游戏:从基础语法到设计模式实战

发布时间:2026/7/24 4:37:08
C#实现石头剪刀布游戏:从基础语法到设计模式实战 1. 项目概述与核心价值最近在带新人或者自己回顾基础的时候发现“石头剪刀布”这个小游戏是个绝佳的练手项目。别看它规则简单但用C#完整实现一遍尤其是要处理好玩家与电脑的对战逻辑能串联起从基础语法到面向对象设计再到随机数生成和状态判断的多个核心知识点。很多面试里所谓的“设计模式”考察比如状态机其最直观的入门理解就可以从这个游戏开始。我这次就带大家从头到尾用控制台应用的形式实现一个结构清晰、可扩展性强的石头剪刀布游戏。我们不止于实现功能更会探讨如何写出易于维护和扩展的代码比如未来如果想增加“蜥蜴、史波克”这种扩展规则或者改成图形界面现在的结构是否能轻松应对。这个项目非常适合C#初学者巩固基础也适合有一定经验的开发者思考如何将简单需求设计得更加优雅。我们会从最直白的“if-else”堆叠开始然后逐步重构引入枚举、类设计甚至触摸一下策略模式的思想让你看到代码是如何一步步“进化”的。整个过程你会清晰地看到变量定义、方法封装、随机数应用、条件判断以及控制台输入输出这些基础技能是如何被有机组合起来的。2. 游戏核心逻辑与初始设计2.1 规则定义与状态枚举任何游戏开始前明确定义规则是第一步。石头剪刀布的规则很简单石头赢剪刀剪刀赢布布赢石头相同则为平局。在代码里我们首先需要定义出“手势”这个核心概念。用整数0、1、2来代表虽然直接但代码可读性极差。这时候C#的枚举enum就派上用场了。public enum HandGesture { Rock, // 石头 Scissors, // 剪刀 Paper // 布 }使用枚举后代码里到处都是HandGesture.Rock、HandGesture.Scissors这样的字眼意图一目了然完全避免了“魔法数字”的出现。接下来我们需要一个裁判它的职责是根据两个手势判断胜负。最直接的实现就是一个方法里面用switch或if-else穷举所有情况。public static class GameRule { public static GameResult Judge(HandGesture player, HandGesture computer) { if (player computer) return GameResult.Draw; if ((player HandGesture.Rock computer HandGesture.Scissors) || (player HandGesture.Scissors computer HandGesture.Paper) || (player HandGesture.Paper computer HandGesture.Rock)) { return GameResult.PlayerWin; } return GameResult.ComputerWin; } } public enum GameResult { PlayerWin, ComputerWin, Draw }注意这里将胜负判断逻辑单独封装在GameRule静态类中是一个很好的实践。它遵循了单一职责原则未来如果规则变化比如加入新的手势我们只需要修改这一个类而不会影响到玩家或电脑类的代码。2.2 玩家与电脑的抽象游戏中有两个参与者玩家和电脑。他们有一些共同行为比如“出拳”GetGesture但具体实现方式截然不同。玩家需要通过控制台输入而电脑则需要随机生成。这立刻让我们想到面向对象中的继承与多态。我们可以设计一个基类Player。public abstract class Player { public string Name { get; protected set; } public abstract HandGesture GetGesture(); }基类Player包含一个名字属性和一个抽象方法GetGesture。这样具体的玩家类和电脑类就可以继承它并实现各自获取手势的逻辑。public class HumanPlayer : Player { public HumanPlayer(string name) { Name name; } public override HandGesture GetGesture() { while (true) { Console.WriteLine(${Name}请出拳 (0-石头, 1-剪刀, 2-布)); string input Console.ReadLine(); if (int.TryParse(input, out int choice) choice 0 choice 2) { return (HandGesture)choice; // 利用枚举和整数的转换 } Console.WriteLine(输入无效请输入0、1或2); } } } public class ComputerPlayer : Player { private readonly Random _random new Random(); public ComputerPlayer() { Name 电脑; } public override HandGesture GetGesture() { // Random.Next(3) 生成 0, 1, 2 之间的随机数 int choice _random.Next(Enum.GetValues(typeof(HandGesture)).Length); return (HandGesture)choice; } }这里有几个关键点输入验证在HumanPlayer.GetGesture()中我们用一个while循环来确保玩家输入的是有效数字并给出了清晰的提示。这是提升用户体验和程序健壮性的基本操作。随机数生成ComputerPlayer使用Random类来生成随机手势。这里_random被定义为只读字段并在构造函数中初始化这是一个好习惯。注意Random的实例化不宜在方法内频繁进行否则在极短时间内连续调用可能导致生成的随机数不够“随机”。枚举转换我们巧妙利用了枚举底层是整型的特性通过(HandGesture)choice进行转换代码非常简洁。3. 游戏主流程与循环结构实现3.1 游戏引擎类的设计有了玩家和规则我们需要一个“导演”来串联整个流程这就是游戏引擎或主控类GameEngine。它的核心是一个游戏循环。public class GameEngine { private readonly HumanPlayer _human; private readonly ComputerPlayer _computer; private int _playerScore; private int _computerScore; private int _drawCount; public GameEngine(string playerName) { _human new HumanPlayer(playerName); _computer new ComputerPlayer(); _playerScore 0; _computerScore 0; _drawCount 0; } public void Start() { Console.WriteLine($ 石头剪刀布游戏开始 ); Console.WriteLine($玩家{_human.Name} vs {_computer.Name}); bool continuePlaying true; while (continuePlaying) { PlayOneRound(); Console.WriteLine($当前比分{_human.Name} {_playerScore} : {_computerScore} {_computer.Name} 平局 {_drawCount}); Console.WriteLine(是否继续(y/n)); continuePlaying Console.ReadLine().ToLower() y; } PrintFinalResult(); } private void PlayOneRound() { // 获取手势 HandGesture playerGesture _human.GetGesture(); HandGesture computerGesture _computer.GetGesture(); Console.WriteLine(${_human.Name} 出了 {playerGesture}); Console.WriteLine(${_computer.Name} 出了 {computerGesture}); // 判定胜负 GameResult result GameRule.Judge(playerGesture, computerGesture); // 更新比分并显示结果 switch (result) { case GameResult.PlayerWin: _playerScore; Console.WriteLine(${_human.Name} 赢了); break; case GameResult.ComputerWin: _computerScore; Console.WriteLine(${_computer.Name} 赢了); break; case GameResult.Draw: _drawCount; Console.WriteLine(平局); break; } Console.WriteLine(); // 空行分隔 } private void PrintFinalResult() { Console.WriteLine(\n 游戏结束 ); Console.WriteLine($最终比分{_human.Name} {_playerScore} : {_computerScore} {_computer.Name}); if (_playerScore _computerScore) Console.WriteLine($恭喜 {_human.Name} 获得最终胜利); else if (_playerScore _computerScore) Console.WriteLine($很遗憾{_computer.Name} 获得了胜利。); else Console.WriteLine(最终双方战平); } }GameEngine类封装了游戏的所有状态比分和行为开始、进行一轮、结束。Start方法中的while循环构成了游戏的主循环每轮结束后询问玩家是否继续。这种结构清晰地将初始化、游戏循环和结束处理分开。3.2 程序入口与启动最后在控制台应用的Main方法中我们只需要创建游戏引擎并启动它。class Program { static void Main(string[] args) { Console.WriteLine(欢迎来到石头剪刀布游戏); Console.Write(请输入你的名字); string playerName Console.ReadLine(); GameEngine game new GameEngine(playerName); game.Start(); Console.WriteLine(按任意键退出...); Console.ReadKey(); } }至此一个功能完整、结构清晰的石头剪刀布游戏就完成了。你可以编译并运行它体验与电脑对战的乐趣。4. 代码优化与设计模式初探上面的实现已经是一个合格的作品。但如果我们从软件工程和可维护性的角度审视还有不少可以优化和深入思考的地方。这部分内容对于希望提升代码质量的开发者尤为重要。4.1 引入“策略模式”优化电脑AI目前的电脑出拳是完全随机的这很公平但也很“笨”。如果我们想让电脑有点“智能”比如根据历史出拳记录来猜测玩家的习惯该怎么办直接修改ComputerPlayer类的GetGesture方法会违反“开闭原则”对扩展开放对修改关闭。这时策略模式Strategy Pattern就非常适合。策略模式定义了一系列算法并将每个算法封装起来使它们可以相互替换。对于电脑玩家来说“出拳策略”就是一个可以替换的算法。我们首先定义一个策略接口public interface IGestureStrategy { HandGesture GetNextGesture(GameHistory history); }GameHistory是一个新类用于记录历史对战数据比如最近10轮双方的手势。然后我们可以实现不同的策略// 随机策略原来的逻辑 public class RandomStrategy : IGestureStrategy { private static readonly Random _rand new Random(); public HandGesture GetNextGesture(GameHistory history) { return (HandGesture)_rand.Next(3); } } // 简单模式策略总是出石头 public class RockOnlyStrategy : IGestureStrategy { public HandGesture GetNextGesture(GameHistory history) HandGesture.Rock; } // 基于历史的策略分析玩家最常出的手势并出能克制它的手势 public class HistoryBasedStrategy : IGestureStrategy { private static readonly Random _rand new Random(); public HandGesture GetNextGesture(GameHistory history) { if (history.Rounds.Count 0) return (HandGesture)_rand.Next(3); // 简化逻辑找出玩家最频繁出的手势 var playerGestures history.Rounds.Select(r r.PlayerGesture); var mostFrequent playerGestures.GroupBy(g g) .OrderByDescending(g g.Count()) .First().Key; // 假设玩家会继续出这个手势我们出能赢它的手势 // 这是一个非常简单的反制逻辑 return mostFrequent switch { HandGesture.Rock HandGesture.Paper, HandGesture.Scissors HandGesture.Rock, HandGesture.Paper HandGesture.Scissors, _ (HandGesture)_rand.Next(3) }; } }然后修改ComputerPlayer类使其接受一个策略接口public class ComputerPlayer : Player { private readonly IGestureStrategy _strategy; private readonly GameHistory _history; public ComputerPlayer(IGestureStrategy strategy, GameHistory history) { Name 电脑; _strategy strategy; _history history; } public override HandGesture GetGesture() { HandGesture gesture _strategy.GetNextGesture(_history); // 电脑出手后也可以选择是否记录到历史这里电脑是后手记录由引擎统一处理更合适 return gesture; } }最后在GameEngine中创建电脑玩家时注入具体的策略// 在GameEngine构造函数中 GameHistory history new GameHistory(); _computer new ComputerPlayer(new HistoryBasedStrategy(), history); // 或者 new RandomStrategy()这样我们无需改动ComputerPlayer的核心代码只需创建不同的策略对象就能让电脑拥有截然不同的行为模式。这极大地增强了程序的扩展性。4.2 游戏历史的记录与持久化为了支持基于历史的策略以及让玩家可以回顾战绩我们需要记录游戏历史。创建一个GameRound记录单轮信息和一个GameHistory管理所有记录。public class GameRound { public HandGesture PlayerGesture { get; set; } public HandGesture ComputerGesture { get; set; } public GameResult Result { get; set; } public DateTime PlayTime { get; set; } DateTime.Now; } public class GameHistory { public ListGameRound Rounds { get; private set; } new ListGameRound(); public void AddRound(HandGesture player, HandGesture computer, GameResult result) { Rounds.Add(new GameRound { PlayerGesture player, ComputerGesture computer, Result result }); // 可选只保留最近N条记录防止内存无限增长 if (Rounds.Count 100) Rounds.RemoveAt(0); } public void PrintHistory() { Console.WriteLine(\n 对战历史 ); foreach (var round in Rounds) { Console.WriteLine(${round.PlayTime:HH:mm:ss} - 玩家:{round.PlayerGesture} vs 电脑:{round.ComputerGesture} - {round.Result}); } } }在GameEngine.PlayOneRound方法中在判定胜负后调用_history.AddRound(...)将本轮记录保存下来。你还可以在游戏结束时或者在游戏中增加一个命令如输入“history”来调用PrintHistory方法展示所有记录。实操心得历史记录功能看似简单但引入了状态管理。在设计时要考虑清楚GameHistory的生命周期和归属。是作为GameEngine的成员还是通过依赖注入传递给需要它的对象如策略类这里我们将其作为GameEngine的成员并传递给电脑策略是一个比较清晰的设计。4.3 配置化与扩展性思考一个健壮的程序应该易于配置。例如游戏总局数、是否允许平局、不同的计分规则等都可以从硬编码中提取出来放到配置文件如appsettings.json或通过启动参数设置。我们可以创建一个GameSettings类public class GameSettings { public int MaxRounds { get; set; } 10; // 默认最多10局 public bool EnableExtendedRules { get; set; } false; // 是否启用“蜥蜴史波克”扩展规则 public string ComputerStrategy { get; set; } Random; // 电脑策略配置 }在GameEngine中读取这个配置。当EnableExtendedRules为true时我们需要动态切换GameRule和HandGesture枚举。这提示我们最初的GameRule.Judge方法写死了三种手势的判断扩展性不足。更好的做法是定义一个规则接口IGameRule并为基本规则和扩展规则分别实现。这又一次体现了面向接口编程和策略模式的威力。虽然由于篇幅我们不会在此完全实现扩展规则但这个思考过程至关重要。它迫使你思考当需求变化时你的代码哪些地方需要大动干戈哪些地方可以像换零件一样轻松适配提前进行这种抽象是区分普通代码和优秀设计的关键。5. 常见问题、调试技巧与进阶方向5.1 开发中常见问题与解决输入处理异常玩家可能输入非数字字符或者超出范围。我们的HumanPlayer类中已经通过int.TryParse和范围检查做了处理。这是一个必须有的防御性编程步骤。随机数“不随机”在快速循环中创建new Random()由于系统时钟作为种子变化不大可能导致生成的随机数序列高度相关。解决方案是像我们之前做的那样将Random实例作为类的静态或实例字段只初始化一次。枚举转换的陷阱我们使用(HandGesture)choice将整数转为枚举。但如果choice的值不在枚举定义的范围内比如-1或3转换仍然会成功得到一个无效的枚举值这可能在后续比较中引发难以察觉的错误。更安全的方法是使用Enum.IsDefined方法进行检查。int choice ...; if (Enum.IsDefined(typeof(HandGesture), choice)) { return (HandGesture)choice; } else { // 处理无效输入例如返回一个默认值或抛出异常 return HandGesture.Rock; }对象引用与状态管理确保在正确的时机初始化对象。例如GameHistory应该在GameEngine开始前创建并贯穿整个游戏生命周期传递给需要它的对象如策略类而不是在每轮游戏中新建。5.2 调试与测试技巧单元测试这个项目是练习单元测试的绝佳对象。你可以为GameRule.Judge方法编写测试用例覆盖所有9种3x3手势组合确保胜负判断逻辑百分百正确。也可以为不同的IGestureStrategy实现编写测试验证其输出是否符合预期。控制台输出调试在策略类中添加详细的日志输出例如Console.WriteLine($[策略] 分析历史玩家常出{mostFrequent}因此我出{gesture})这能帮助你直观理解AI的“思考”过程。使用断点在GetGesture、Judge等关键方法内部设置断点逐步执行观察变量的值是理解程序流程和排查逻辑错误最有效的方法。5.3 项目进阶方向如果你已经完成了基础版本并尝试了上述优化还可以挑战以下方向让这个项目成为你简历上的一个亮点图形界面WPF/WinForms将控制台应用迁移到图形界面。玩家通过点击按钮出拳电脑的手势和结果以图片形式展示。这涉及到事件处理、UI线程与后台逻辑的分离。网络对战将项目改造成一个简单的客户端-服务器应用支持两个真人玩家通过网络对战。这会引入Socket编程、异步通信、数据序列化等网络编程核心知识。数据持久化将玩家的历史战绩、胜率等信息保存到数据库如SQLite或文件中。每次游戏后更新数据并可以提供一个排行榜功能。更复杂的AI实现更强大的电脑AI例如使用马尔可夫链预测玩家的出拳序列或者使用简单的机器学习模型虽然有点杀鸡用牛刀但作为学习很有趣。依赖注入使用像Microsoft.Extensions.DependencyInjection这样的库来管理GameEngine、Player、IGestureStrategy等对象的创建和生命周期使代码耦合度更低更易于测试。从一个小小的石头剪刀布游戏出发竟然可以深入到设计模式、架构设计、网络通信、数据持久化等多个层面。这正是编程的魅力所在从一个简单的需求开始不断挖掘、重构和扩展你的代码和设计能力就在这个过程中得到了实实在在的锻炼。