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

简介:五子棋是一款经典两人对弈游戏,规则简洁但富有策略性,是学习算法设计、图形界面开发与事件驱动编程的理想实践项目。本C#五子棋源程序涵盖Windows Forms界面构建、二维数组棋盘管理、棋子落子逻辑、连珠判断、胜负判定及基础AI对战功能,适合C#初学者深入理解面向对象编程与GUI应用开发。通过该项目的学习与实践,开发者可全面掌握桌面应用程序的开发流程与核心机制,提升综合编程能力。

五子棋开发全栈实践:从C#基础到AI智能对战

你有没有试过在某个深夜,打开电脑想下一盘五子棋,却发现找不到一个能陪你“斗智斗勇”的对手?或者自己写了个小游戏,结果逻辑混乱、界面卡顿、悔棋都像在拆炸弹?🤯 其实啊, 做一个看似简单的五子棋程序,背后藏着的可不只是“放个子、连五个”这么简单 。它是一扇门——通向现代软件工程核心思想的大门。

今天我们就来搞点“狠活”,不只教你画个棋盘、点个按钮,而是带你从零开始,用 C# + Windows Forms 搭建一套真正专业级的五子棋系统。你会看到:基础语法如何支撑复杂逻辑,面向对象怎样让代码优雅又健壮,图形界面为何需要双缓冲防闪烁,甚至还能给你的游戏塞进一个会“思考”的AI大脑🧠!

准备好了吗?Let’s go!🚀


🧱 基础语法:不是“Hello World”,而是游戏骨架

很多人学编程都卡在“学会了语法却不会写项目”这个坎上。其实关键在于—— 要把语言特性当成工具箱里的螺丝刀和扳手,而不是背诵说明书

比如我们做个15×15的标准五子棋盘,第一反应就是:

int[,] board = new int[15, 15];

这行代码看着平平无奇,但它干了三件大事:
- int[,] 是二维数组,模拟真实棋盘的坐标系;
- [15,15] 定义大小,避免后期频繁调整;
- 初始化为0,代表“空位”。

接下来判断能不能落子?一句话搞定:

if (board[x, y] == 0) { /* 可以下 */ }

别小看这个条件判断,它是整个游戏安全性的第一道防线。但如果你直接到处写 board[x,y]==0 ,等哪天你想加个“禁手规则”或“特殊地形”,改起来就得满世界找硬编码……😅 所以聪明的做法是把它封装成方法:

public bool IsEmpty(int row, int col) => grid[row, col] == 0;

你看,就这么一点点封装意识,就已经悄悄迈进了“好代码”的门槛。

至于胜利判定?总不能让用户自己数吧 😅 那就得靠循环遍历四个方向(横、竖、主对角线、副对角线):

for (int i = -4; i <= 4; i++)
{
    if (GetPiece(row + i, col) != piece) break;
    if (i == 4 && count == 5) return true;
}

这些看似简单的 if for ,组合起来就成了游戏运行的“神经系统”。它们不炫酷,但不可或缺——就像盖房子的地基,看不见却撑得起整栋楼。

💡 小贴士:初学者常犯的错误是把所有逻辑塞进一个类里,最后变成“上帝类”。记住一句话: 每一行代码都应该待在它最舒服的地方


🔗 面向对象设计:谁该干什么事?

现在问题来了:上面那些功能,到底放在哪儿合适?

假设你把棋盘状态、胜负判断、玩家信息全都扔进 Form1.cs ,那不出三天,这个文件就会膨胀到上千行,任何人想改个按钮颜色都得提心吊胆——生怕一动就崩 😰

这就是为什么我们需要 面向对象编程(OOP) 。它的本质不是什么高深理论,而是 给人类大脑减负的一种思维方式 :把复杂系统拆成几个各司其职的小部件,每个部件只关心自己的事。

🎯 核心三剑客:Board、Player、GameManager

想象一下真实的五子棋对局,有哪些“角色”参与?

  1. 棋盘(Board) :负责记录每一步落在哪;
  2. 玩家(Player) :决定什么时候轮到我下,我下哪儿;
  3. 裁判/主持人(GameManager) :控制流程、切换回合、宣布胜负。

这三个角色,正好对应三个类。

✅ Board 类:数据容器 + 行为封装

先来看棋盘类的设计。很多人一开始会这样写:

public class Board
{
    public int[,] Grid; // 错!太暴露了!
}

这样做等于把家门钥匙交给所有人,谁都能随便改你的棋盘状态。正确的做法是“藏起来”:

public class Board
{
    private const int Size = 15;
    private int[,] grid;

    public int Size => Size; // 只读属性暴露尺寸

    public Board() => grid = new int[Size, Size];

    public bool IsEmpty(int row, int col) => grid[row, col] == 0;

    public int GetCell(int row, int col) => grid[row, col];

    public bool PlacePiece(int row, int col, int piece)
    {
        if (row < 0 || row >= Size || col < 0 || col >= Size) return false;
        if (!IsEmpty(row, col)) return false;

        grid[row, col] = piece;
        return true;
    }
}

注意几个细节:
- private 保护内部数组;
- 构造函数确保每次新建棋盘都是干净的;
- PlacePiece 返回 bool ,方便调用者知道操作是否成功;
- 使用 => 简化单行方法,代码更清爽。

方法 功能 时间复杂度
IsEmpty() 判断位置是否为空 O(1)
GetCell() 获取某位置棋子类型 O(1)
PlacePiece() 落子并校验合法性 O(1)
Clear() 重置棋盘 O(n²)

这种设计下,外部代码永远无法直接修改 grid ,只能通过接口操作——这才是真正的“封装”。

✅ Player 类:不只是名字和颜色

玩家也不只是个标签。他要有身份、有行为、还能知道自己是不是该出招了:

public enum PlayerColor { Black = 1, White = 2 }

public class Player
{
    public string Name { get; }
    public PlayerColor Color { get; }
    public bool IsActive { get; set; } // 当前是否轮到该玩家

    public Player(string name, PlayerColor color)
    {
        Name = name;
        Color = color;
        IsActive = false;
    }

    public (int row, int col)? GetMove(Board board)
    {
        var emptyCells = new List<(int, int)>();
        for (int i = 0; i < board.Size; i++)
            for (int j = 0; j < board.Size; j++)
                if (board.IsEmpty(i, j))
                    emptyCells.Add((i, j));

        if (emptyCells.Count == 0) return null;

        var rnd = new Random();
        return emptyCells[rnd.Next(emptyCells.Count)];
    }
}

这里有几个值得玩味的设计点:
- 用了元组 (int row, int col) 返回坐标,语义清晰;
- GetMoveSafe 版本避免无限循环,在终局阶段更稳定;
- 返回 ? (可空值),明确表达“可能没地方下了”的意图;
- 把“随机落子”作为默认策略,未来可以轻松替换成 AI 算法。

🤔 思考题:为什么 Random 不做成静态字段?因为多线程环境下会有竞争问题!每次 new 一个虽然性能略低,但绝对安全。

✅ GameManager:游戏世界的“中央控制器”

有了棋盘和玩家,谁来指挥全局?

答案就是 GameManager 。它像是交响乐的指挥家,协调各个乐器奏出和谐旋律:

public class GameManager
{
    private Board board;
    private Player player1, player2;
    private Player currentPlayer;

    public event Action<GameResult> OnGameEnded;
    public event Action OnBoardUpdated;

    public GameManager()
    {
        board = new Board();
        player1 = new Player("人类", PlayerColor.Black);
        player2 = new Player("AI", PlayerColor.White);
        currentPlayer = player1;
    }

    public bool MakeMove(int row, int col)
    {
        if (!board.PlacePiece(row, col, (int)currentPlayer.Color))
            return false;

        OnBoardUpdated?.Invoke();

        if (CheckWin(row, col))
        {
            OnGameEnded?.Invoke(new GameResult(currentPlayer, WinReason.FiveInRow));
            return true;
        }

        SwitchTurn();
        return true;
    }

    private void SwitchTurn() =>
        currentPlayer = currentPlayer == player1 ? player2 : player1;
}

重点来了: 事件机制(Event) 的引入,彻底解耦了逻辑层和表现层!

什么意思呢?以前你可能在 MakeMove 里直接写 pictureBox1.Invalidate() ,但现在不需要了。UI 层只需要“订阅”这两个事件:

gameManager.OnBoardUpdated += () => pictureBox1.Invalidate();
gameManager.OnGameEnded += result => MessageBox.Show($"{result.Winner.Name} 赢了!");

这样一来, GameManager 根本不知道 UI 存不存在,它可以被用于命令行版、Web版、甚至服务器后台匹配系统 —— 高度复用性就这么来了

下面是类关系图,直观展示三者协作方式:

classDiagram
    class Board {
        -int[,] grid
        +bool PlacePiece()
        +bool IsEmpty()
    }
    class Player {
        +string Name
        +PlayerColor Color
        +bool IsActive
        +GetMove()
    }
    class GameManager {
        -Board board
        -Player player1, player2
        +MakeMove()
        +SwitchTurn()
        +event OnGameEnded
    }
    GameManager --> Board : uses
    GameManager --> Player : controls

看到没?没有双向依赖,没有全局变量,每个类职责分明。这才是工业级代码的样子!


🖼️ 图形界面:不只是好看,更要流畅

再厉害的底层逻辑,如果界面卡成PPT,用户也不会买账。Windows Forms 虽然是“老技术”,但它依然是快速构建桌面应用的利器。

🖼️ PictureBox:绘制棋盘的艺术

我们选 PictureBox 作为主画布,因为它支持自定义绘图。关键是要处理好 Paint 事件:

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    Graphics g = e.Graphics;
    int cellSize = 40;
    Pen gridPen = Pens.Black;

    for (int i = 0; i <= 15; i++)
    {
        g.DrawLine(gridPen, 0, i * cellSize, 600, i * cellSize); // 横线
        g.DrawLine(gridPen, i * cellSize, 0, i * cellSize, 600); // 竖线
    }

    // 天元点(中心黑点)
    g.FillEllipse(Brushes.Black, 297, 297, 6, 6);
}

但这里有坑⚠️:每次重绘都会创建新 Graphics ,如果你在这里不断 new Pen(...) ,内存迟早爆炸!

✅ 正确姿势:把 Pen Brush 提升为窗体字段,并在窗体关闭时释放资源:

private Pen gridPen = new Pen(Color.Black, 1);
private Brush blackBrush = Brushes.Black;

// 最后别忘了 Dispose
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        gridPen.Dispose();
        blackBrush.Dispose();
    }
    base.Dispose(disposing);
}

⚙️ 双缓冲:告别屏幕闪烁

当你频繁调用 Invalidate() (比如鼠标悬停预览落子),画面会疯狂闪动。这是因为 Windows 默认采用“直接绘制”模式,一边擦旧图一边画新图,肉眼就能看出撕裂感。

解决方案:开启双缓冲!

this.SetStyle(
    ControlStyles.AllPaintingInWmPaint |
    ControlStyles.UserPaint |
    ControlStyles.DoubleBuffer,
    true);

原理很简单:先在内存中画一张完整的图,画好了再一次性“贴”到屏幕上。虽然多占点内存,但换来丝般顺滑的体验,值!

控制样式 作用
AllPaintingInWmPaint 禁止自动擦背景
UserPaint 允许手动绘图
DoubleBuffer 启用离屏缓冲

📏 坐标转换:从像素到格子

用户点击的是屏幕坐标(X=312, Y=405),但我们需要的是棋盘索引(row=10, col=7)。怎么转?

private Point GetBoardPointFromScreen(Point screenPoint)
{
    int row = screenPoint.Y / cellSize;
    int col = screenPoint.X / cellSize;

    if (row >= 0 && row < 15 && col >= 0 && col < 15)
        return new Point(row, col);
    else
        return new Point(-1, -1); // 无效位置
}

注意这里用了整除 / ,自动向下取整,刚好对应网格定位。再加上边界检查,防止越界访问数组。

流程如下:

graph LR
    A[鼠标点击] --> B{获取Screen Point}
    B --> C[调用GetBoardPointFromScreen]
    C --> D[计算Row/Col]
    D --> E[越界检测]
    E -->|有效| F[提交落子请求]
    E -->|无效| G[忽略操作]

🧩 布局管理:适应各种分辨率

别让你的游戏只能在一个固定窗口运行!使用 TableLayoutPanel FlowLayoutPanel 让控件自动排列:

flowPanel.Dock = DockStyle.Bottom;
flowPanel.Controls.Add(btnRestart);
flowPanel.Controls.Add(btnExit);

配合 Anchor Dock 属性,实现响应式布局:

属性 效果
Dock=Fill 填充父容器
Anchor=All 四边随窗口缩放
AutoSize=true 自动匹配内容大小

还有 DPI 缩放问题也不能忽视,在 Main() 中加上:

Application.SetHighDpiMode(HighDpiMode.SystemAware);

否则在高分屏上文字糊成一片,用户体验直接归零。


🐭 事件驱动:让程序“活”起来

GUI 应用的本质是“等待用户动作”。这就引出了 事件驱动模型 —— 整个程序围绕事件展开,而不是顺序执行。

🖱️ 鼠标点击处理全流程

当玩家点击棋盘时,发生了什么?

chessBoardPanel.MouseClick += ChessBoardPanel_MouseClick;

private void ChessBoardPanel_MouseClick(object sender, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;

    var (row, col) = ConvertToBoardIndex(e.X, e.Y);

    if (!IsValidMove(row, col)) return;

    gameManager.MakeMove(row, col);
}

其中 IsValidMove 包含三重防护:

private bool IsValidMove(int row, int col)
{
    if (row < 0 || row >= 15 || col < 0 || col >= 15) return false;
    if (_gameManager.IsGameOver) return false;
    if (!_board.IsEmpty(row, col)) return false;
    return true;
}

层层过滤,确保只有合法操作才能进入核心逻辑。

完整流程如下:

flowchart TD
    A[MouseClick Event Triggered] --> B{Is Left Button?}
    B -- No --> Z[Ignore]
    B -- Yes --> C[Extract X,Y Coordinates]
    C --> D[Convert to Row,Col Index]
    D --> E{Within Board Bounds?}
    E -- No --> Z
    E -- Yes --> F{Game Over?}
    F -- Yes --> Z
    F -- No --> G{Position Empty?}
    G -- No --> Z
    G -- Yes --> H[Call MakeMove]
    H --> I[Update Board State]
    I --> J[Refresh UI]

每一个判断都是系统的“守门员”,守住稳定性底线。


🤖 AI 对战:给游戏一颗会思考的大脑

没有 AI 的五子棋,就像没有对手的拳击手。我们来加个智能引擎,让它不仅能下,还能“算计”。

🌲 Minimax 算法:AI 的决策树

Minimax 是博弈类 AI 的经典算法。它的思路很朴素: 我走一步,假设对手走最优,然后我再走最优……一直推演若干步,选最终评分最高的那个开局走法

简化实现如下:

public (int r, int c) GetBestMove(Board board, Player ai)
{
    int bestScore = int.MinValue;
    (int r, int c) bestMove = (-1, -1);

    foreach (var move in board.GetAvailableMoves())
    {
        board.MakeMove(move.r, move.c, ai);
        int score = Minimax(board, 4, false, ai, int.MinValue, int.MaxValue);
        board.UndoMove(move.r, move.c);

        if (score > bestScore)
        {
            bestScore = score;
            bestMove = move;
        }
    }

    return bestMove;
}

递归部分包含 Alpha-Beta 剪枝,大幅提升效率:

private int Minimax(Board board, int depth, bool isMax, Player ai, int alpha, int beta)
{
    if (depth == 0 || board.IsGameOver()) 
        return Evaluate(board, ai);

    if (isMax)
    {
        int maxEval = int.MinValue;
        foreach (var m in board.GetMoves())
        {
            board.MakeMove(m, ai);
            int eval = Minimax(board, depth-1, false, ai, alpha, beta);
            board.UndoMove(m);

            maxEval = Math.Max(maxEval, eval);
            alpha = Math.Max(alpha, eval);
            if (beta <= alpha) break; // 剪枝
        }
        return maxEval;
    }
    else
    {
        // 对手回合,取最小值
    }
}

💡 提示:搜索深度 DEPTH=4 是平衡点,更深虽更强但更慢。

🧮 评估函数:教AI“看局势”

光会算步不行,还得懂“形势”。我们定义几种关键棋型并打分:

棋型 分数 示例
五连珠 100000 🟡🟡🟡🟡🟡
活四 50000 ○🟡🟡🟡○
冲四 10000 ●🟡🟡🟡○
活三 5000 ○🟡🟡○
眠三 800 ●🟡🟡○

可以用位运算加速检测,比如将一行转为64位整数,用掩码匹配:

long LIVE_FOUR_MASK = 0b_011110L;
bool isLiveFour = (line & LIVE_FOUR_MASK) == LIVE_FOUR_MASK;

效率比逐个循环判断高出一个数量级!


🧩 完整项目结构:模块化才是王道

最后来看看整个项目的组织方式:

GomokuGame/
├── Program.cs                  
├── Models/
│   ├── Board.cs                
│   ├── Player.cs               
│   └── MoveRecord.cs           
├── AI/
│   ├── AIPlayer.cs             
│   └── EvaluationEngine.cs     
├── Forms/
│   ├── MainForm.cs             
│   └── GamePanel.cs            
├── Managers/
│   └── GameManager.cs          
└── Utilities/
    ├── Serializer.cs           
    └── Logger.cs               

每一层各司其职,新人接手也能快速定位代码。配合单元测试,维护成本大大降低。


🔚 结语:从小游戏到大思维

你以为我们只是做了一个五子棋?No no no 🙅‍♂️

我们实际上演练了一整套 现代软件开发的核心范式

  • 基础语法 → 构建最小可用单元
  • 面向对象 → 实现高内聚低耦合
  • 事件驱动 → 解耦逻辑与界面
  • 设计模式 → 提升可扩展性
  • AI算法 → 探索智能化可能

这套思维不仅能用来做游戏,还能迁移到 Web 开发、移动应用、后台服务……任何复杂的系统设计中。

所以啊,下次有人说“我会写 HelloWorld”,你可以微微一笑:“那你会写一盘五子棋吗?” 😉

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

简介:五子棋是一款经典两人对弈游戏,规则简洁但富有策略性,是学习算法设计、图形界面开发与事件驱动编程的理想实践项目。本C#五子棋源程序涵盖Windows Forms界面构建、二维数组棋盘管理、棋子落子逻辑、连珠判断、胜负判定及基础AI对战功能,适合C#初学者深入理解面向对象编程与GUI应用开发。通过该项目的学习与实践,开发者可全面掌握桌面应用程序的开发流程与核心机制,提升综合编程能力。


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

Logo

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

更多推荐