ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#语言连连看游戏源代码:棋盘算法、消除判定与WinForms绘制技巧

C#语言连连看游戏源代码:棋盘算法、消除判定与WinForms绘制技巧 简介《C#语言连连看》源代码包以RAR压缩包形式提供压缩包大小约941KB文件目录及类型明细未单独标注解压后可直接查看源码与配套资源。这份资源面向正在学习C#、Windows Forms界面开发或入门游戏编程的人群适合用作课程设计、毕业设计或自学实战案例。源码配有详细注释在实现连连看对局时涉及GUI布局与PictureBox/Button控件交互、Click事件驱动机制、System.Drawing图像加载与绘制、二维数组/List 棋盘状态存储以及基于DFS/BFS的路径查找和图片配对消除逻辑同时包含棋盘初始化、胜负判定、洗牌重排等完整流程。通过逐行阅读注释初学者能快速理解图形界面程序从界面搭建到算法落地的全过程有经验的开发者也能从中借鉴游戏逻辑拆分与代码组织方式。该资源目前已获得1108人次浏览学习轻量体积与细致注释可作为C#游戏编程的练手素材。1. C#语言连连看游戏源代码一份带注释的 WinForms 项目到底值不值得读很多人搜“C#语言连连看”的动机非常具体课程设计要交一个能跑的 WinForms 小程序或者面试前想找个像样的桌面项目撑场面。标题里的五个字——C#语言连连看——背后其实是一套很标准的桌面小游戏写法用二维数组当棋盘、用路径搜索做消除判定、用 GDI 画图块、用鼠标事件驱动游戏逻辑。它能解决的问题也不止“交作业”这么简单而是让你一口气摸清 C# 桌面开发里最常被问到的四个知识点。这篇文章按这类源码的常见结构把棋盘建模、消除算法、绘制交互和排错经验拆开讲末尾再给你一个能立刻加进去的提示与测试方案。读完你不仅能看懂注释还能自己动手改出一版能玩、能测、能拿得出手的小游戏。2. 棋盘数据结构与初始化用 C# 二维数组把一局棋盘装进内存2.1 为什么棋盘选 int[,] 而不是 ListList 翻开这类源码最先出现的不是绘图代码而是棋盘数据结构。绝大多数有效的 C# 连连看实现都会选int[,]或int[][]而不是ListListint。原因是棋盘永远是矩形、尺寸固定二维数组按下标访问时没有列表那层间接寻址开销代码读起来也最贴近你脑子里那张网格。每个格子存 0 或一个正整数类型编号0 代表已消除的空格1 到 N 对应不同图块。// GameBoard.cs —— 棋盘核心类 public class GameBoard { private readonly int[,] _grid; private readonly int _rows; private readonly int _cols; public const int EMPTY 0; // 0 永远代表空格禁止其他含义 public GameBoard(int rows, int cols) { _rows rows; _cols cols; _grid new int[rows, cols]; // new 出来默认全是 0也就是全空 } public int Rows _rows; public int Cols _cols; // 用索引器暴露只读访问调用方拿值容易改值只能通过专门方法 public int this[int r, int c] { get { return _grid[r, c]; } } public bool IsEmpty(int r, int c) { if (r 0 || r _rows || c 0 || c _cols) throw new ArgumentOutOfRangeException(棋盘坐标越界); return _grid[r, c] EMPTY; } }逻辑说明构造参数rows和cols决定棋盘有效区域索引器this[r, c]让调用方可以直接取图块值却没法随意写入非法值EMPTY统一管理“空”的语义。以后如果要做“冰块块”“锁块”这类扩展只需要约定新的占位值不用到处改魔法数字。参数说明rows和cols一般取 10 和 12也就是 10 行 12 列共 120 个格子太小了对路径判定不友好太大了首屏装不下EMPTY0是约定所有新增逻辑都必须遵守这个约定否则后面LineClear里判断! EMPTY就会把不该算障碍的格子算进去。这里还有一个值得注意的工程习惯把棋盘类拆成独立文件不要写到Form1.cs里。带详细注释的源码通常都会强调这一点因为逻辑层与界面层分离后后面用控制台、单元测试去调引擎会非常顺手。做 C# 上位机或者别的 WinForms 项目时也该守住这条边界——事件处理器只做转发不做业务判断。2.2 “先组牌再洗牌”的生成逻辑保证成对且开局不卡死棋盘初始化最容易踩两个坑一是总块数不是偶数消到最后会剩单块二是虽然成对但开局就找不到任何可消除的一对。常见的解法是“先组牌、再洗牌”的两段式做法。// 初始化一局把牌组打散后填入棋盘 public void ResetBoard(int rows, int cols, int typeCount) { int totalCells rows * cols; if (totalCells % 2 ! 0) throw new InvalidOperationException(棋盘格子数必须是偶数); Listint pieces new Listint(totalCells); // 第一步按类型造牌保证每种类型出现偶数次 for (int type 1; type typeCount; type) { int count totalCells / typeCount; if (count % 2 ! 0) count--; // 奇数就砍掉一个保持偶数 for (int i 0; i count; i) pieces.Add(type); } // 除不尽时剩下的格子用已有的牌补上保证总牌数 总格子数 while (pieces.Count totalCells) pieces.Add(pieces[0]); // 第二步费雪-耶茨洗牌O(n) 且分布均匀 Random rng new Random(); for (int i pieces.Count - 1; i 0; i--) { int j rng.Next(i 1); (pieces[i], pieces[j]) (pieces[j], pieces[i]); } // 第三步按行优先填入二维数组 int idx 0; for (int r 0; r rows; r) { for (int c 0; c cols; c) _grid[r, c] pieces[idx]; } // 第四步死局检测无解就重新洗牌 while (!HasAnyValidMove()) ShuffleInPlace(); }逻辑说明先拼一个 List每种类型放totalCells / typeCount个偶数配平后再洗牌。费雪-耶茨洗牌用rng.Next(i 1)保证均匀分布注意千万别写OrderBy(x rng.Next())那种写法既不均匀又会为整个序列创建大量临时对象在 10×12 这种小棋盘上其实无所谓但坏习惯一旦带到上位机项目里就是性能隐患。参数说明typeCount决定游戏难度。总数 120 格、类型数 12 时每种类型 10 块类型数越多同型块分布越散可消除对越少游戏越难。末尾那个while (!HasAnyValidMove()) ShuffleInPlace();是保底逻辑下章讲完CanConnect后再回头理解会非常顺。2.3 类型数与棋盘尺寸如何搭配这里的参数调整最直接地影响手感也是这类源码里注释最多的部分。常见搭配如下棋盘尺寸格子总数推荐类型数同型块均值实际节奏8×8648~106.4~8偏快新手容易上手10×1212012~167.5~10经典配置难度均衡12×1416816~208.4~10.5偏难死局出现频率高必须做好重洗实际源码里如果看到const int ROWS 10; const int COLS 12;那配套的typeCount大概率落在 12 到 16 之间。如果类型数调到 20 以上你很快会发现一局游戏频繁触发“无解重洗”玩家体验会很差调低到 8 以下又太简单开局随便点都能消。所以表格里的推荐值是按“每种类型大概 8~10 块”反推的这也是这个品类的通用设计经验。要注意ShuffleInPlace不能在死局后无限重试。更稳的写法是限制重洗次数比如 50 次后仍然无解就调用ResetBoard(rows, cols, typeCount 2)把类型数调高一点重新生成避免极端情况下Random连续给出恶意分布导致死循环。这个细节在代码里不显眼但实际运行久了一定会碰到。3. 消除判定核心算法两个拐点以内的路径搜索怎么写3.1 直线检查 LineClear边界与端点的三个坑连连看的规则一句话能说清两个图块可消除当且仅当存在一条不穿越其他图块的路径连接它们且路径至多拐两次弯。拆开就是直线0 拐、一次折角1 拐、两次折角2 拐三种形态。路径永远不能穿过非空格子但可以贴着棋盘外沿走——这一点在实现里经常被漏掉。先写最底层的直线检查// 判断 a、b 之间的直线上是否有障碍 // 前置条件a、b 必须同行或同列端点本身不算障碍 private bool LineClear(Point a, Point b) { if (a.X b.X) // 垂直方向行相同检查列之间 { int low Math.Min(a.Y, b.Y); int high Math.Max(a.Y, b.Y); for (int y low 1; y high; y) { if (_grid[a.X, y] ! EMPTY) return false; } return true; } if (a.Y b.Y) // 水平方向列相同检查行之间 { int low Math.Min(a.X, b.X); int high Math.Max(a.X, b.X); for (int x low 1; x high; x) { if (_grid[x, a.Y] ! EMPTY) return false; } return true; } return false; // 不同行也不同列直线检查直接不通过 }逻辑说明LineClear只处理同行或同列的a、b。循环从low 1到high - 1恰好避开两个端点本身——端点属于正在比较的两个图块当然不算障碍。如果把范围写成low到high两个紧挨着的同款块会永远判定为“中间有障碍”这是这个函数里最常见的翻车点。参数说明a、b是System.Drawing.PointX存行号、Y存列号这只是个约定你完全可以改成两个整数参数。注意Point是System.Drawing下的值类型绘图代码共用确实方便但如果想把这个核心类拆成不依赖 WinForms 的类库做单元测试就需要改成自建结构体或者直接传四个int避免测试工程被迫引用System.Drawing。3.2 0拐、1拐、2拐的完整连接判定CanConnect 的实现直线之外一次拐弯等价于“两个块位于一个矩形的对角位置找一个邻角点若它为空且两端到它的连线都畅通就能连”。两次拐弯则要麻烦一点穷举所有可能的中介行或中介列// 核心判定两个格子能否通过不超过两次拐弯的路径连接 public bool CanConnect(Point a, Point b) { if (a b) return false; if (_grid[a.X, a.Y] ! _grid[b.X, b.Y]) return false; // 图块类型不同 if (_grid[a.X, a.Y] EMPTY) return false; // 空格不能消 // 0 拐直线 if ((a.X b.X || a.Y b.Y) LineClear(a, b)) return true; // 1 拐矩形对角上的两个角点 Point corner1 new Point(a.X, b.Y); Point corner2 new Point(b.X, a.Y); if (IsVirtualEmpty(corner1) LineClear(a, corner1) LineClear(corner1, b)) return true; if (IsVirtualEmpty(corner2) LineClear(a, corner2) LineClear(corner2, b)) return true; // 2 拐枚举所有中介行注意 -1 和 _rows 是外沿虚拟行 for (int r -1; r _rows; r) { Point mid1 new Point(r, a.Y); Point mid2 new Point(r, b.Y); if (IsVirtualEmpty(mid1) IsVirtualEmpty(mid2) LineClear(a, mid1) LineClear(mid1, mid2) LineClear(mid2, b)) return true; } // 2 拐枚举所有中介列同样包含外沿虚拟列 for (int c -1; c _cols; c) { Point mid1 new Point(a.X, c); Point mid2 new Point(b.X, c); if (IsVirtualEmpty(mid1) IsVirtualEmpty(mid2) LineClear(a, mid1) LineClear(mid1, mid2) LineClear(mid2, b)) return true; } return false; }逻辑说明r从 -1 扫到_rowsc从 -1 扫到_cols就是为了包含棋盘外沿的虚拟空位——连连看允许路径贴着棋盘外沿走拐点落在棋盘外面完全合法。IsVirtualEmpty的判断很简单坐标越界视为空坐标在界内则看_grid里对应位置是否为EMPTY。中间的LineClear(a, mid1)检查起点到拐点的路径LineClear(mid1, mid2)检查拐点之间的长距离三步缺一不可顺序也不能换否则会漏掉路径。性能说明10×12 的棋盘一共 120 格最坏情况做一次全盘可消性扫描也就是约 7000 对组合每对组合里CanConnect最坏循环几十次总共几十万次整数比较在 C# 里是毫秒级的事。完全不需要上 BFS 或者 A*用 BFS 去解这个“最多两拐弯”的约束反而容易得出让人看不懂的绕路路径跟玩家预期不一致。3.3 死局检测与重洗别让一局游戏卡在无解状态ResetBoard末尾那句HasAnyValidMove现在能实现了// 全局可消检测任意两个非空且类型相同的块能连上就说明没有死局 public bool HasAnyValidMove() { for (int r1 0; r1 _rows; r1) { for (int c1 0; c1 _cols; c1) { int type _grid[r1, c1]; if (type EMPTY) continue; for (int r2 r1; r2 _rows; r2) { for (int c2 (r2 r1) ? c1 1 : 0; c2 _cols; c2) { if (_grid[r2, c2] type CanConnect(new Point(r1, c1), new Point(r2, c2))) return true; } } } } return false; }逻辑说明外层两个循环固定“第一块”内层两个循环遍历它后面所有格子避免同一对检查两遍。只要类型相同且CanConnect返回 true就说明当前棋盘还有解。每次玩家成功消除一对后也应该调用它做一次终局判定如果返回 false 就该弹出提示并提供“重洗”按钮。参数说明这里的_rows、_cols是棋盘有效行列数不含外沿虚拟区域。遍历范围必须是有效区域因为虚拟区域没有真实图块参与配对只会制造假的可消除对。重洗按钮的逻辑可以直接复用ShuffleInPlace()但注意洗牌后仍然要再做一次HasAnyValidMove()否则可能洗出一个新的死局棋盘玩家会以为程序卡死了。4. GDI 渲染与鼠标交互从贴图到高亮反馈的完整绘制流程4.1 双缓冲绘制三步让界面告别闪烁WinForms 里画棋盘最常见的问题是闪烁。闪烁的根源是默认情况下控件收到WM_PAINT后是边擦背景边画内容每一步都显示在屏幕上只要绘图内容多就会看到明显的刷屏抖动。解法是启用双缓冲// GamePanel.cs —— 一个自绘面板 public class GamePanel : Panel { public GamePanel() { // 双缓冲三件套先画到后台缓冲再一次性拷贝到屏幕 this.SetStyle( ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); this.ResizeRedraw true; // 尺寸变化时自动重绘 } private Image[] _tiles; // 每种图块的纹理构造时加载一次 private int _cellSize 48; // 每个格子的边长像素 private GameBoard _board; protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g e.Graphics; g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.Clear(Color.White); for (int r 0; r _board.Rows; r) { for (int c 0; c _board.Cols; c) { Rectangle rect new Rectangle( c * _cellSize, r * _cellSize, _cellSize, _cellSize); g.DrawRectangle(Pens.LightGray, rect); int val _board[r, c]; if (val ! GameBoard.EMPTY _tiles[val] ! null) { g.DrawImage(_tiles[val], rect); // 把图块贴进格子 } } } } }逻辑说明SetStyle三件套中OptimizedDoubleBuffer是关键它让控件先把所有绘图内容画到内存缓冲再一次性复制到屏幕AllPaintingInWmPaint避免背景擦除造成白闪UserPaint则让控件完全由自绘逻辑负责。ResizeRedraw true使得窗口拉伸时立即触发重绘而不是残留旧像素。参数说明_cellSize 48是个视觉与点击精度都折中的值再小到 32 以下图块看不清大到 64 以上同等棋盘会超出屏幕。图片纹理务必在构造函数里一次加载好不要放进OnPaint里否则每一次重绘都会去读磁盘或反复解析图片帧率一掉玩家第一反应就是“这个 C# 程序好卡”。4.2 鼠标坐标映射与选中高亮鼠标事件在 WinForms 里通常注册在Panel.MouseDown上核心工作是像素坐标换算成棋盘行列// 鼠标按下换算棋盘坐标并处理两次点选逻辑 private void GamePanel_MouseDown(object sender, MouseEventArgs e) { int col e.X / _cellSize; // 像素坐标 → 列号 int row e.Y / _cellSize; // 像素坐标 → 行号 if (row 0 || row _board.Rows || col 0 || col _board.Cols) return; // 点在棋盘外忽略 if (_board[row, col] GameBoard.EMPTY) return; // 点在空格上同样忽略 if (!_selected.HasValue) { _selected new Point(row, col); // 第一次点选记录并高亮 Invalidate(); return; } // 第二次点选尝试消除 Point second new Point(row, col); if (_board.CanConnect(_selected.Value, second)) { _board.Eliminate(_selected.Value, second); // 将两个格子置空 _selected null; Invalidate(); } else { // 消除失败把选中状态切到当前点击的块上常见交互习惯 _selected second; Invalidate(); } }逻辑说明e.X / _cellSize用的是整数除法48 像素格子里 x 从 0 到 47 都返回列 0正好匹配绘制时的矩形边界。_selected采用Point?可空类型第一次点击记录位置第二次点击尝试连接失败则把选中切到新块——这是主流连连看交互比“失败后清空选中”更有连续性。参数说明e.Location是相对于触发事件的 Panel 的坐标不是窗体坐标。千万不要用Cursor.Position或PointToClient去二次换算否则面板一旦有边框或偏移高亮就会整体偏一格。还有一个隐藏细节第二次点击如果点到了第一次相同的块必须忽略否则玩家快速双击时程序会尝试“自己连自己”导致逻辑错乱。选中高亮的绘制放在OnPaint末尾if (_selected.HasValue) { int r _selected.Value.X; int c _selected.Value.Y; using (Pen pen new Pen(Color.OrangeRed, 3f)) { Rectangle selRect new Rectangle( c * _cellSize, r * _cellSize, _cellSize, _cellSize); g.DrawRectangle(pen, selRect); } }注意using包裹PenGDI 对象用完要释放。WinForms 的Pen、Brush都是 IDisposable频繁new不释放会积累 GDI 句柄程序跑久了画图会突然变慢甚至抛异常——这种问题在任务管理器里很难看出来属于典型的“看起来没内存泄漏但句柄数一直在涨”。4.3 可选增强临时路径连线与提示动画做到这里核心逻辑已经完整但想让游戏更直观可以加一条“连接预览线”当第二次点击的目标和当前选中块满足CanConnect时在消除前先画出路径。这需要把CanConnect的判定结果和路径一起返回// 试着找一条 2 拐的路径找不到返回 null public ListPoint TryFindPath(Point a, Point b) { if (a b) return null; if (_grid[a.X, a.Y] ! _grid[b.X, b.Y]) return null; if (_grid[a.X, a.Y] EMPTY) return null; // 直线路径 if ((a.X b.X || a.Y b.Y) LineClear(a, b)) return new ListPoint { a, b }; // 1 拐路径 Point corner1 new Point(a.X, b.Y); Point corner2 new Point(b.X, a.Y); if (IsVirtualEmpty(corner1) LineClear(a, corner1) LineClear(corner1, b)) return new ListPoint { a, corner1, b }; if (IsVirtualEmpty(corner2) LineClear(a, corner2) LineClear(corner2, b)) return new ListPoint { a, corner2, b }; // 2 拐路径这里保留中介点绘制时可以直接连成折线 for (int r -1; r _rows; r) { Point mid1 new Point(r, a.Y); Point mid2 new Point(r, b.Y); if (IsVirtualEmpty(mid1) IsVirtualEmpty(mid2) LineClear(a, mid1) LineClear(mid1, mid2) LineClear(mid2, b)) return new ListPoint { a, mid1, mid2, b }; } // 列方向同理略 return null; }逻辑说明这份代码和CanConnect几乎同构区别只是把判定成功的路径点收集起来。绘图时拿到ListPoint后用Graphics.DrawLines依次连线即可。注意IsVirtualEmpty返回 true 的虚拟拐点坐标可能在棋盘外绘制时要先 clip 到 Panel 区域否则线条会画出控件边界。5. 避坑指南连连看开发里最容易翻车的5个细节5.1 现象两个相邻图块却永远无法消除开始时我遇到过这种怪事两个同款块并肩挨着点击后程序却提示无法连接。检查半天发现LineClear的循环写成了for (int y low; y high; y)这里把端点也包含进循环了。两个相邻块low和high只差 1循环体执行一次检查的正好是起点位置的格子它当然不等于EMPTY于是直线永远“被阻挡”。原因总结循环边界没有排除端点自身。解决统一改成low 1到high - 1并写一个最小用例验证——用 2×4 棋盘放两对相邻同款块跑一次CanConnecttrue 就对了。5.2 现象快速双击时同一个块被连续吞掉或状态错乱连续快速点击时第一次点选和第二次点选之间鼠标事件已经触发了多次导致程序把同一个点既当作“第一次选中”又当作“第二次消除目标”最后同一块被连消两次棋盘上出现一个莫名其妙的空格。原因总结交互状态机没做防抖两次点击间隔内没有检查目标块是否还是原来那个非空块。解决在消除分支里强制判断_selected.Value ! second同时用Eliminate方法内部再检查一次两个格子是否都非空。也就是说不能只依赖界面层的判断核心逻辑层也要做防御双保险才不会被快速点击钻空子。5.3 现象拖动窗体时画面闪烁得像幻灯片WinForms 自绘控件默认是不开双缓冲的。如果你直接在Form1的Paint事件里画棋盘并且每次画之前又从磁盘加载图片拖动窗口时会一帧一帧地擦背景、读文件、画图块看起来就像幻灯片。原因总结没启用双缓冲且OnPaint里做了耗时的图片加载。解决把自绘逻辑放到自定义 Panel 里启用OptimizedDoubleBuffer | AllPaintingInWmPaint | UserPaint图片资源放到构造函数里加载。这个坑在更换到高 DPI 屏幕或 4K 显示器时更容易暴露因为每次重绘的像素量成倍增长。5.4 现象棋盘后半程找不到可消对整局卡死一局玩到只剩十几块时突然怎么点都消不了而且程序也没有任何提示。这是因为开发时只做了开局死局检测没做“每消一对之后”的再次检测。原因总结消除后棋盘状态变化原先有解的布局可能变成死局如果代码只在ResetBoard时检测一次后半程就没人管了。解决每次Eliminate成功之后调用一次HasAnyValidMove()返回 false 时就触发“无解重洗”弹出一个提示框并调用ShuffleInPlace()。重洗后必须再做一次可消检测这个逻辑写成循环最多重试 50 次再不行就加类型数重新生成。这属于典型的运行时防护别指望开局检测能覆盖后面所有局面。5.5 现象点击格子高亮位置和鼠标相差一格看起来像“鼠标点到左边格高亮却出现在右边格”。问题通常出在坐标换算上常见两种写法int col (e.X - panel.Left) / _cellSize; // panel.Left 是相对窗体的坐标 int col e.X / (_cellSize 1); // 多加了边框宽度第一种错在e.X本身已经是相对 Panel 的坐标再减panel.Left就把坐标往左推了第二种错在把 1 像素边框也当成格子边长的一部分导致边界区域永远映射到前一格。原因总结坐标空间混用、边框宽度参与计算。解决统一用e.X / _cellSize和e.Y / _cellSize绝对不要再用 Panel 的Left、Top或Location做二次偏移。面板如果有边框要把边框宽度单独考虑把绘图区域和点击区域共用同一个_cellSize常量一处定义所有地方都引用它就不会出现绘制和交互两套标准。6. 进阶技巧提示、计分与核心算法的自动化验证6.1 提示功能怎么实现扫描第一对可消对提示按钮的逻辑本质就是调一次HasAnyValidMove的变体并且把找到的那一对格子返回出来// 返回当前棋盘任意一对可消块找不到返回 null public (Point first, Point second)? FindHint() { for (int r1 0; r1 _rows; r1) { for (int c1 0; c1 _cols; c1) { if (_grid[r1, c1] EMPTY) continue; for (int r2 r1; r2 _rows; r2) { for (int c2 (r2 r1) ? c1 1 : 0; c2 _cols; c2) { if (_grid[r2, c2] _grid[r1, c1] CanConnect(new Point(r1, c1), new Point(r2, c2))) { return (new Point(r1, c1), new Point(r2, c2)); } } } } } return null; }拿到返回值后把两个坐标高亮两秒再自动清除高亮。计时计分就更直接了在Eliminate调用处加一个计数器每消一对加 10 分连续消三对以上给连击加成计时用System.Windows.Forms.Timer设置Interval 1000每秒触发一次把剩余时间减一归零弹窗结束游戏。这一套加完项目就具备了完整游戏闭环拿去面试演示完全够用。6.2 用单元测试锁死 CanConnect 的正确性开发这类逻辑时我最大的教训是把判定代码直接写在窗体事件里结果没法单独验证出了问题只能靠肉眼盯着屏幕猜。后来把GameBoard拆成不依赖System.Drawing的纯逻辑类用int行号列号参数替代Point事情就简单了。你完全可以建一个控制台项目引用同一个GameBoard.cs写几个断言跑一遍// 核心算法验证直接调用不涉及任何窗体绘制 public void TestLineClear() { GameBoard gb new GameBoard(4, 4); gb.SetCell(0, 0, 1); gb.SetCell(0, 2, 1); bool ok1 gb.CanConnect(0, 0, 0, 2); // 中间无阻挡应为 true gb.SetCell(0, 1, 2); // 在中间放一个障碍块 bool ok2 gb.CanConnect(0, 0, 0, 2); // 中间有阻挡应为 false Console.WriteLine($直连无阻挡{ok1}, 中间挡块{ok2}); if (ok1 ! true || ok2 ! false) throw new Exception(LineClear 逻辑异常); }逻辑说明CanConnect(0,0,0,2)这种重载直接吃行列号避免测试工程引用System.Drawing。给GameBoard补一个SetCell方法用于测试数据准备就能把所有边界情况都打出来。我把LineClear端点排除、1 拐角点为空、2 拐虚拟外沿这三类最容易写错的分支各测一遍只要这些用例通过游戏核心逻辑基本就稳了剩下的事情交给界面层。这也是我想强调的收尾习惯源码带详细注释是好事但只有当你把核心算法从窗体里拆出来、能独立测试、能重构、能加新功能时这份源码才真正变成你自己的能力。我一开始把全部判断都塞进MouseDown事件里代码写了三百行没法测后来痛定思痛拆成引擎类和界面类整个项目的可维护性完全不一样。这个教训让我后来做 C# 上位机项目时也一直坚持逻辑层与界面层分离。希望帮到你。本文还有配套的精品资源点击获取
返回列表