本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文详细介绍如何使用C#编程语言实现经典划拳游戏“石头剪子布”,涵盖从基础规则建模到图形化界面开发的全过程。通过面向对象设计,利用枚举类型表示手势选择,构建游戏逻辑判断胜负,并结合控制台与GUI两种模式提升交互体验。项目包含用户输入处理、随机算法生成电脑决策、实时得分统计及游戏流程控制,帮助开发者掌握C#核心语法和Windows Forms/WPF界面编程技巧,是初学者实践C#应用开发的理想案例。

石头剪子布游戏的工程化实现:从逻辑建模到交互体验

你有没有想过,一个看似简单的“石头剪子布”游戏,背后其实藏着一整套现代软件开发的核心思想?🤯 别笑,这可不是小孩子过家家——它完全可以成为一个 展示C#编程精髓、面向对象设计原则和用户体验思维的绝佳载体

想象一下:你写了一个控制台程序,用户输入数字选择出拳,电脑随机回应,最后统计胜率。一切正常运行……但当你回头再看代码时,却发现满屏都是 if-else 、全局变量满天飞、状态混乱得像一团毛线🧶。这时候你就知道: 能跑 ≠ 好代码

今天,我们就来彻底重构这个“小游戏”,把它变成一个结构清晰、扩展性强、甚至还能加音效和表情包的完整项目!🎉 不仅要让它“能玩”,更要让它“值得维护”。


用状态机理清思路:别让流程失控

先问个问题:你觉得“石头剪子布”有几个关键阶段?

很多人脱口而出:“出拳、判断胜负、显示结果。”
没错,但这只是表面。如果我们把整个交互过程拆解成机器可理解的状态流,就会发现更清晰的脉络:

stateDiagram-v2
    [*] --> Idle
    Idle --> PlayerTurn : 开始游戏
    PlayerTurn --> Judge : 双方出拳
    Judge --> Result : 判定胜负
    Result --> ScoreUpdate : 更新计分
    ScoreUpdate --> ConfirmRestart
    ConfirmRestart --> Idle : 重新开始
    ConfirmRestart --> [*] : 退出

看到了吗?这已经不是一个线性脚本了,而是一个 有限状态机(Finite State Machine) 。每一步都明确知道当前处于什么状态,下一步该往哪走。

这种设计的好处是啥?三个字: 可控性

比如未来你想加入“三局两胜自动结束”的规则,只需要在 ScoreUpdate 后判断是否达到条件即可,完全不影响其他模块。而如果所有逻辑挤在一个 Main() 函数里,改起来就是噩梦 😵‍💫。

所以,别急着写代码,先把这张图刻进脑子里。我们接下来的所有工作,都是为了让这个状态流转得更加优雅、稳定、可扩展。


枚举不只是命名常量:它是类型安全的守护神

在写代码的时候,你会怎么表示“石头”、“剪刀”、“布”?

有人可能会这样写:

string playerChoice = "rock";

或者用整数:

int choice = 1; // 0=石头, 1=剪刀, 2=布?

听着好像没问题,但实际开发中你会发现各种低级错误频发:拼错 "roock" 、传入 -1 5 这种非法值……编译器还不会报错!

怎么办?用 枚举(enum)

public enum Gesture
{
    Rock = 0,
    Paper = 1,
    Scissors = 2
}

现在你的方法签名可以长这样:

public void MakeMove(Gesture playerGesture)
{
    // ...
}

这时候如果你传个 "rockk" ,直接编译失败 ❌;IDE还会自动提示你能选哪些选项 ✅。这就是所谓的“ 编译时检查 + 智能感知 ”,大大降低出错概率。

而且更重要的是—— 语义清晰 。看到 Gesture.Rock ,谁都知道这是“出石头”,而不是某个神秘的 1 "rck"

💡 小贴士:虽然枚举默认从0开始递增,但我建议显式赋值。一是增强可读性,二是方便后续做数学运算(后面会讲到)。


胜负判定还能这么玩?一行公式搞定循环克制

传统的胜负判断通常是这样的:

if (player == Gesture.Rock && computer == Gesture.Scissors)
    return "Player wins!";
else if (player == Gesture.Scissors && computer == Gesture.Paper)
    return "Player wins!";
// ... 啊啊啊写吐了

太啰嗦了吧!能不能更简洁一点?

当然可以!利用枚举背后的整数值,我们可以把胜负关系抽象成一个数学模型。

观察一下这个循环克制链:
- Rock (0) → beats → Scissors (2)
- Scissors (2) → beats → Paper (1)
- Paper (1) → beats → Rock (0)

你会发现:只要 (player - computer + 3) % 3 == 1 ,玩家就赢了!🤯

为什么加3?因为防止负数取模出问题(虽然C#支持负数模,但为了逻辑统一还是加上保险)。

于是,我们的胜负判断变成了这样:

public static GameResult DetermineWinner(Gesture player, Gesture computer)
{
    int result = ((int)player - (int)computer + 3) % 3;
    return result switch
    {
        1 => GameResult.Win,
        2 => GameResult.Lose,
        _ => GameResult.Draw
    };
}
差值 含义 示例
0 平局 Rock vs Rock (0-0=0)
1 玩家胜 Scissors vs Paper (2-1=1)
2 电脑胜 Paper vs Rock (1-0=1 → %3=1? No! Wait…)

等等……这里好像有点不对劲?

⚠️ 注意!上面的例子暴露了一个常见陷阱: 顺序依赖

因为我们定义的是 Rock=0, Paper=1, Scissors=2 ,但克制关系并不是按数值顺序排列的。所以我们需要确保枚举定义与公式匹配。

正确的映射应该是:

Rock = 0,
Paper = 1,
Scissors = 2

然后记住:
- (差值 % 3) == 1 → 玩家胜
- (差值 % 3) == 2 → 电脑胜
- 否则平局

这个技巧不仅节省了大量 if-else ,还体现了“ 数据驱动逻辑 ”的设计哲学——把复杂的业务规则压缩成一个简单公式,是不是很酷?


随机不是随便:小心 Random 的坑

为了让游戏公平,计算机必须“随机”出拳。C# 提供了 System.Random 类,听起来很简单对吧?

但新手最容易犯的一个错误就是: 频繁创建新的 Random 实例

// 错误示范 ⚠️
public Gesture GetComputerChoice_Bad()
{
    var random = new Random(); // 每次都新建
    return (Gesture)random.Next(0, 3);
}

你以为每次都是新种子?实际上,在短时间内多次调用,系统时间戳可能相同,导致多个实例使用相同的种子,从而产生相同的“随机”序列!

那怎么办?答案是: 单例 + 静态字段

private static readonly Random _rng = new Random();

public Gesture Choose()
{
    lock (_rng) // 多线程安全
    {
        int index = _rng.Next(0, 3);
        return (Gesture)index;
    }
}

为什么要加 lock ?因为 Random 不是线程安全的!多个线程同时调用 .Next() 可能会导致内部状态损坏或返回异常值。

当然,如果你追求更高安全性,也可以考虑使用加密级随机数生成器:

using System.Security.Cryptography;

public static int GetSecureRandom(int min, int max)
{
    byte[] bytes = new byte[4];
    using (var rng = RandomNumberGenerator.Create())
    {
        rng.GetBytes(bytes);
    }
    uint randomValue = BitConverter.ToUInt32(bytes, 0);
    return (int)(min + (randomValue % (max - min)));
}

不过对于小游戏来说,性能开销太大,没必要搞这么复杂 😅。


Round 类:每一回合都是一段不可篡改的历史

当游戏变成多轮制后,你不仅要关心“这一局谁赢了”,还得知道“总共打了几局”、“我最喜欢出什么”等问题。

这就引出了一个核心概念: 回合(Round)应该被封装为一个独立的对象

public class Round
{
    public Gesture PlayerChoice { get; }
    public Gesture ComputerChoice { get; }
    public GameResult Result { get; }

    public Round(Gesture playerChoice, Gesture computerChoice, GameResult result)
    {
        PlayerChoice = playerChoice;
        ComputerChoice = computerChoice;
        Result = result;
    }

    public override string ToString()
    {
        return $"[Round] You: {PlayerChoice}, CPU: {ComputerChoice} => {Result}";
    }
}

看到没?所有属性都是只读的,构造函数一次性初始化。这意味着一旦创建,这段历史就不能被修改 —— 完美符合“事实记录”的本质。

而且重写了 ToString() 方法,打印日志或调试时一眼就能看懂发生了什么:

[Round] You: Rock, CPU: Scissors => Win
[Round] You: Paper, CPU: Rock => Win
[Round] You: Scissors, CPU: Scissors => Draw

是不是比一堆 { player=0, cpu=2, res=1 } 清晰多了?

此外,这类对象非常适合用于未来的功能拓展,比如:
- 存入数据库做数据分析 📊
- 支持回放/录像功能 ▶️
- 实现AI学习玩家习惯 🤖


得分系统不能裸奔:封装才是王道

得分怎么记?有人可能直接写:

int playerWins = 0;
int computerWins = 0;

看起来没问题,但如果别人不小心写了个 playerWins-- 怎么办?或者在某处误设为 -1 ?整个逻辑就崩了。

我们应该怎么做?封装!

public class ScoreTracker
{
    private int _playerWins;
    private int _computerWins;
    private int _draws;

    public void RecordWin() => _playerWins++;
    public void RecordLoss() => _computerWins++;
    public void RecordDraw() => _draws++;

    public int PlayerWins => _playerWins;
    public int ComputerWins => _computerWins;
    public int Draws => _draws;

    public void Reset()
    {
        _playerWins = 0;
        _computerWins = 0;
        _draws = 0;
    }

    public string GetSummary()
    {
        int total = _playerWins + _computerWins + _draws;
        return $"总场次: {total} | 胜: {_playerWins} | 负: {_computerWins} | 平: {_draws}";
    }
}

这样做的好处是什么?

  1. 数据保护 :外部无法直接修改 _playerWins
  2. 可扩展性 :以后想加“连胜计数”、“触发成就”等功能,只需在 RecordWin() 里加逻辑;
  3. 一致性保障 :所有状态变更都经过统一入口,便于调试和审计。

比如将来你想限制最多只能玩100局:

public void RecordWin()
{
    if (_playerWins >= 100)
        throw new InvalidOperationException("今日挑战已达上限!");
    _playerWins++;
}

一句话的事儿,不用到处去查哪里改了分数。


历史记录不止是列表:它是分析的起点

有了 Round ScoreTracker ,我们就可以构建完整的多轮游戏系统了。

主控制器里维护一个历史记录列表:

private readonly List<Round> _history = new();

每打完一局就存进去:

var round = new Round(playerChoice, computerChoice, result);
_history.Add(round);

然后你可以轻松实现“战绩回放”功能:

public void ShowHistory()
{
    Console.WriteLine("\n--- 对战记录 ---");
    for (int i = 0; i < _history.Count; i++)
    {
        Console.WriteLine($"#{i+1}: {_history[i]}");
    }
}

输出效果杠杠的:

--- 对战记录 ---
#1: [Round] You: Rock, CPU: Scissors => Win
#2: [Round] You: Paper, CPU: Rock => Win
#3: [Round] You: Scissors, CPU: Scissors => Draw

但这还不是最厉害的!借助 LINQ,你能瞬间完成高级分析:

🔍 查看自己的出拳偏好

var frequency = _history
    .GroupBy(r => r.PlayerChoice)
    .Select(g => new { 手势 = g.Key, 次数 = g.Count() })
    .OrderByDescending(x => x.次数);

foreach (var item in frequency)
{
    Console.WriteLine($"{item.手势}: {item.次数} 次");
}

说不定你会发现:“哎呀,我竟然70%的时间都在出石头!” 😂

📈 计算胜率

double winRate = (double)_history.Count(r => r.Result == GameResult.Win) / _history.Count;
Console.WriteLine($"当前胜率: {winRate:P2}");

🕵️‍♂️ 分析电脑是否真的“随机”

var aiChoices = _history.GroupBy(r => r.ComputerChoice);
foreach (var g in aiChoices)
{
    Console.WriteLine($"电脑出 {g.Key}: {g.Count()} 次");
}

如果发现电脑连续五次出“布”,那你就有理由怀疑它的随机性啦~


主循环怎么写才不翻车?

控制台游戏的核心是主循环。常见的写法是:

while (true)
{
    var input = GetUserInput();
    if (!input.HasValue) continue;

    var computer = _ai.Choose();
    var result = Judge.DetermineWinner(input.Value, computer);

    var round = new Round(input.Value, computer, result);
    _score.RecordResult(result);
    _history.Add(round);

    DisplayResult(round);

    if (!AskToContinue()) break;
}

这里面有两个关键词特别重要:

  • continue :遇到非法输入时跳过本次循环,让用户重试;
  • break :当用户选择退出时跳出无限循环。

其中 AskToContinue() 是提升体验的关键细节:

static bool AskToContinue()
{
    Console.Write("再来一局?(y/n): ");
    return Console.ReadLine()?.Trim().ToLower() == "y";
}

别小看这一句,它决定了用户是不是愿意继续玩下去。毕竟没人喜欢玩完一局就被强制退出的游戏吧?


控制台也能有颜值:颜色+边框+菜单全安排

很多人觉得控制台程序“土”,其实那是因为没好好打扮它!

🎨 彩色输出

static void PrintResult(string message, ConsoleColor color)
{
    Console.ForegroundColor = color;
    Console.WriteLine($"【结果】{message}");
    Console.ResetColor();
}

// 使用示例
PrintResult("你赢了!", ConsoleColor.Green);
PrintResult("电脑赢了!", ConsoleColor.Red);

🖼️ 加个装饰边框

static void DrawBorder(string title)
{
    string line = new string('═', 50);
    Console.WriteLine("╔" + line + "╗");
    Console.WriteLine($"║ {title,-46} ║");
    Console.WriteLine("╚" + line + "╝");
}

// 调用
DrawBorder("欢迎来到石头剪子布大战!");

效果立马不一样了有没有!

🧭 结构化菜单系统

public static class ConsoleUI
{
    public static int ShowMenu()
    {
        Console.WriteLine("\n请选择操作:");
        Console.WriteLine("1. 开始新回合");
        Console.WriteLine("2. 游戏规则说明");
        Console.WriteLine("3. 查看当前比分");
        Console.WriteLine("4. 重置所有记录");
        Console.WriteLine("0. 退出游戏");
        Console.Write("请输入选项:");

        if (int.TryParse(Console.ReadLine(), out int choice) && choice >= 0 && choice <= 4)
            return choice;
        else
        {
            PrintResult("无效输入,请选择 0-4 的数字。", ConsoleColor.Red);
            return -1;
        }
    }
}

这样一来,整个程序就有了“正规软件”的感觉,不再是乱糟糟的一堆指令。


GUI来了!Windows Forms带你起飞

当你掌握了控制台版本,下一步自然是要做个图形界面,体验真正的事件驱动编程!

创建一个 GameForm.cs ,布局几个按钮和图片框:

private Button btnRock;
private Button btnPaper;
private Button btnScissors;
private PictureBox picPlayerChoice;
private PictureBox picComputerChoice;
private Label lblResult;
private Label lblScore;

然后绑定事件:

btnRock.Click += (s, e) => PlayRound(Gesture.Rock);
btnPaper.Click += (s, e) => PlayRound(Gesture.Paper);
btnScissors.Click += (s, e) => PlayRound(Gesture.Scissors);

PlayRound 方法负责协调逻辑层和视图层:

private void PlayRound(Gesture playerChoice)
{
    var computerChoice = _ai.Choose();
    var result = _judge.DetermineWinner(playerChoice, computerChoice);

    // 更新图像
    picPlayerChoice.Image = Image.FromFile($"Images/{playerChoice}.png");
    picComputerChoice.Image = Image.FromFile($"Images/{computerChoice}.png");

    // 更新结果显示
    lblResult.Text = FormatResultText(result);
    UpdateScoreDisplay();

    // 播放音效
    PlaySoundEffect(result);
}

看看这个流程:

graph TD
    A[用户点击出拳按钮] --> B{触发Click事件}
    B --> C[调用PlayRound方法]
    C --> D[生成电脑选择]
    D --> E[执行胜负判断]
    E --> F[更新PictureBox图像]
    F --> G[播放对应音效]
    G --> H[刷新Label比分]
    H --> I[切换表情图标]
    I --> J[等待下一次交互]

整个过程就像一场精心编排的交响乐,每个组件各司其职,互不干扰。


图片+音效=沉浸感爆棚!

光有界面还不够,我们要让游戏“活”起来!

🖼️ 图像资源加载

Rock.png , Paper.png , Scissors.png 放进 Images/ 文件夹,设置为“嵌入资源”或“复制到输出目录”。

picPlayerChoice.Image = Image.FromFile($"Images/{playerChoice}.png");

还可以加个动画效果,比如晃动一下图片表示“出拳动作”。

🔊 音效加持

private SoundPlayer winSound = new SoundPlayer(Properties.Resources.WinSound);
private SoundPlayer loseSound = new SoundPlayer(Properties.Resources.LoseSound);

private void PlaySoundEffect(GameResult result)
{
    switch (result)
    {
        case GameResult.Win:
            winSound.Play();
            break;
        case GameResult.Lose:
            loseSound.Play();
            break;
        default:
            SystemSounds.Beep.Play(); // 平局响一声
            break;
    }
}

再配合一个表情包图标切换:

picEmotion.Image = result switch
{
    GameResult.Win => Properties.Resources.HappyFace,
    GameResult.Lose => Properties.Resources.SadFace,
    _ => Properties.Resources.NeutralFace
};

这一刻,你的小游戏已经拥有了90年代街机的感觉!🎮💥


综合能力拉满:这才是真正的编程成长

回顾整个项目,我们走了这样一条路:

  1. 逻辑建模 :用状态机梳理流程;
  2. 语言基础 :善用 enum Random switch expression 等特性;
  3. 面向对象设计 Round ScoreTracker 封装职责;
  4. 用户体验优化 :控制台美化、GUI开发、多媒体集成;
  5. 可扩展架构 :各层解耦,未来可迁移到Web或移动端。

这些技能单独看都不难,但能把它们组合起来做成一个完整产品,才是工程师的核心竞争力。

更妙的是,这套架构天生支持扩展。比如你想加入“蜥蜴-斯波克”版(《生活大爆炸》同款),只需要:

public enum Gesture
{
    Rock,
    Paper,
    Scissors,
    Lizard,
    Spock  // 新增!
}

然后调整判定公式或矩阵即可,其他代码几乎不用动!

甚至你可以定义接口,为未来接入网络对战做准备:

public interface IGameService
{
    Round PlayTurn(Gesture gesture);
    ScoreSummary GetScore();
    void Reset();
}

底层逻辑不变,上层通信换掉就行。这才是“高内聚、低耦合”的真实体现!


写在最后:小项目也有大智慧

很多人总觉得只有做大型系统才算练技术,其实不然。

正所谓:“ 一沙一世界,一花一天堂 ”。一个小小的“石头剪子布”,完全可以成为你通往优秀程序员之路的跳板。

下次当你接到一个“简单需求”时,别急着 rush 上去 coding。停下来想想:

  • 它的状态有哪些?
  • 数据如何建模?
  • 行为如何封装?
  • 用户体验如何提升?
  • 将来怎么扩展?

这些问题的答案,才是真正区分“码农”和“工程师”的地方。

所以,别再小看任何一个小项目。只要你用心去做,它都能教会你很多东西。🌟

最后送大家一句我常说的话: 代码即思想,结构见格局 。愿你在每一次敲击键盘中,都能写出更有灵魂的作品。💻❤️

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本文详细介绍如何使用C#编程语言实现经典划拳游戏“石头剪子布”,涵盖从基础规则建模到图形化界面开发的全过程。通过面向对象设计,利用枚举类型表示手势选择,构建游戏逻辑判断胜负,并结合控制台与GUI两种模式提升交互体验。项目包含用户输入处理、随机算法生成电脑决策、实时得分统计及游戏流程控制,帮助开发者掌握C#核心语法和Windows Forms/WPF界面编程技巧,是初学者实践C#应用开发的理想案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐