☰
C#五子棋课程设计实战:WinForms界面+SQLite数据库完整实现
2026/10/4 2:17:20 网站建设 项目流程

简介:这是一份面向计算机专业学生与C#初学者的五子棋课程设计完整项目,涵盖人机对战与局域网联机两大模式,源码附带数据库文件,可直接用于课程答辩、毕业设计或就业作品集。项目实现中涉及Minimax与Alpha-Beta剪枝AI算法、TCP/IP网络通信、数据库持久化、Windows Forms界面布局以及悔棋与背景音乐等交互细节,能够帮助读者串联面向对象编程、多线程和数据库操作等核心知识点。压缩包共200个文件,主体为53个C#源码文件,搭配47个图片资源、30个resources资源、4个MP3音频、1个MDF数据库文件,另含3个exe可执行程序,整体约53MB。文件划分清晰,csproj与sln工程文件可直接用Visual Studio加载运行,数据库文件和程序集DLL便于理解玩家记录与对战数据的存取流程。目前已有615人学习下载,适合需要参考完整项目结构、调试联机逻辑或替换界面素材的开发者。

1. 为什么 C# 五子棋是大学项目里的“高性价比”选择:源码与数据库都要交什么

C# 五子棋是很多学校课程设计和实训项目里的常客,搜索这个标题的读者,多半是计算机相关专业、需要独立交付一个“源码+数据库”完整项目的初学者。选它的理由很实际:界面复杂度可控,玩法逻辑没有大量数学推导,AI 部分又能做到“有深度但不用读懂论文”,加上数据库模块后,项目从“一个小游戏”变成了“一个带数据闭环的软件系统”,答辩时有得讲。

我以前带过不少学生做这个题目,发现一个规律:花时间最多的地方往往不是算法,而是棋盘画出来点不准、五连判断在边界漏判、数据库装好了不知道怎么把棋局存进去。这些坑在需求文档里根本不会写。如果你正在找现成源码参考,或者在纠结数据库到底存什么表,这篇笔记会从工程搭建到落地细节把整个方案拆开讲。最后你能得到一个既能下棋、能复盘、又能查历史棋局的完整项目,而不是一个只画了几个圆圈的窗口。

2. 选型与工程结构:WinForms 做五子棋,三层划分和坐标模型一次说清

2.1 三个 UI 方案对比:WinForms、WPF、Unity 谁更适合课程设计答辩

同样是 C# 做五子棋,界面层有几种常见路线。WinForms 是最“稳”的选择,它的绘图模型简单,一个 OnPaint 事件就能画完整个棋盘,双击事件直接处理落子,不需要额外学习数据绑定和依赖属性。WPF 的界面更现代,适合后期美化,但写棋盘要接触 DrawingVisual 或者 Canvas 控件,绑定和样式会在项目里占掉很大篇幅,新手容易在“让棋子显示出来”这一步就消耗掉大量时间。Unity 做五子棋要处理场景、相机和输入系统,从课程设计角度来讲,本质是杀鸡用了牛刀。

如果目标是“交一个能跑、能讲、不过度依赖奇技淫巧”的项目,WinForms 优先。我一般给自己的学生定这样一个选型规则:答辩时间只有五到十分钟,界面方案越通用,老师越容易理解;换成 WPF 或者 Unity,反而会被追问很多与五子棋逻辑无关的框架问题,答不上来就很被动。另外“C# 五子棋 源码”这个话题下,能直接参考并跑起来的示例大多也是 WinForms,对新手更友好。

选型学习成本界面效果答辩风险适合场景
WinForms低,绘图事件直观中规中矩低,逻辑容易讲清课程设计、实训项目
WPF中高,需懂绑定和模板较好,可定制中,会被追问框架细节有半年以上 C# 基础的同学
Unity高,涉及场景与渲染管线最好,可做特效高,游戏工程复杂度大想做独立游戏方向展示

2.2 工程目录怎么摆:界面、业务逻辑、数据访问三层的边界

很多大学项目源码的问题不是功能缺失,而是所有代码都堆在 MainForm.cs 里,一个文件几千行,连自己都找不到落子逻辑在哪。我把工程结构固定成四个目录,界面层只负责“显示和接收鼠标事件”,业务层只负责“棋盘状态和判定”,数据层只负责“读写数据库”。

Gobang/ ├─ Gobang.sln ├─ Gobang/ │ ├─ Program.cs // 应用程序入口 │ ├─ Forms/ │ │ └─ MainForm.cs // 棋盘界面、鼠标事件、回放定时器 │ ├─ Models/ │ │ ├─ Board.cs // 15x15 棋盘状态、落子、胜负判定 │ │ └─ Move.cs // 一步棋的模型:行列坐标、执棋方 │ ├─ AI/ │ │ └─ Evaluator.cs // 局面评分、AI 落子逻辑 │ ├─ Data/ │ │ ├─ DbHelper.cs // 连接管理、表初始化 │ │ └─ GameRepository.cs // 棋局与落子步的增删改查 │ └─ gobang.db // SQLite 数据库文件,运行时生成

这样拆分之后,答辩时可以直接说“我把界面、逻辑和数据访问分层了”,这句话在老师眼里比多写两百行代码有价值得多。写代码时的约束也很简单:Forms 里不出现 SQL 字符串,Board 里不知道数据库存在,GameRepository 不关心棋子长什么样。如果后面有人机对战、悔棋、回放这些功能,都只在对应层里加方法,不会牵一发动全身。

2.3 棋盘坐标模型:数组下标与像素点的换算,别把逻辑写死在 Paint 里

五子棋的棋盘逻辑坐标是二维数组下标,物理坐标是屏幕上的像素点。两者的换算看起来简单,但很多人的第一版代码是把坐标算在 MouseDown 事件里、再把算好的像素点存进数组,结果窗口一缩放就全错。正确做法是单独封装两个方法:一个把棋子坐标转像素点,一个把鼠标点击位置反向解析成棋子坐标。

private const int BoardSize = 15; // 15路棋盘 private const int CellSize = 34; // 格子边长,单位像素 private const int Margin = 24; // 棋盘边界留白 // 棋盘坐标 (row, col) 转屏幕像素点 private Point ToPixel(int row, int col) { int x = Margin + col * CellSize; int y = Margin + row * CellSize; return new Point(x, y); } // 屏幕像素点反解成棋盘坐标,超出命中范围返回 null private (int row, int col)? ToBoard(Point p) { for (int row = 0; row < BoardSize; row++) { for (int col = 0; col < BoardSize; col++) { Point pt = ToPixel(row, col); bool hitX = Math.Abs(p.X - pt.X) <= CellSize / 2; bool hitY = Math.Abs(p.Y - pt.Y) <= CellSize / 2; if (hitX && hitY) { return (row, col); } } } return null; }

这段代码的关键在于:ToPixel 直接根据行号和列号计算像素点,逻辑集中在一处;ToBoard 用格子边长的一半作为命中半径,鼠标不必精确点在交叉点上,落子手感会好很多。CellSize 取 34 像素是为了让整个棋盘尺寸在 15 路乘 34 像素再加两侧留白后,窗口在 1080p 屏幕上不显得局促。如果你喜欢更紧凑的布局,可以改成 30,但要注意 ToBoard 的命中半径会跟着变小,点歪的概率会略微增加。这个换算模型是整个项目的地基,后面所有绘制、回放、AI 判定都要调用它。

3. 把游戏逻辑跑通:落子、胜负判定和简单 AI 的实现顺序

3.1 15x15 棋盘绘制与落子:双缓冲和命中检测

界面层要做的事情只有画网格、画棋子、处理点击。画棋盘用 GDI+ 的 DrawLine 画十五条横线和十五条竖线,棋子用 FillEllipse 画实心圆。这里有一个新手容易忽略的问题:WinForms 的控件默认在重绘时会闪烁,因为背景擦除和前景绘制是分开的。处理方式是在窗体构造函数里打开双缓冲。

public MainForm() { InitializeComponent(); // 开启双缓冲,减少棋盘重绘时的闪烁 SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); _board = new Board(); DoubleBuffered = true; }

有人说“五子棋闪烁是玄学,忍一忍就过去了”,其实不是。拖动窗口边缘、切换最小化再恢复时,如果没有双缓冲,棋盘会残影一片。上面这段配置把背景擦除和绘制合并到一次操作里,是标准的 GDI+ 优化方式。落子的点击事件里,先调用 ToBoard 判断是否点中了某个交叉点,再检查该位置是否为空,然后才调用 Board 层的方法更新状态,最后触发 Refresh 重绘。

每次落子之后的流程都是同样的三步:修改数据模型、调用 Invalidate 请求重绘、检查游戏是否结束。不要自行去手动调用 CreateGraphics 画图,那种做法在窗口被遮挡后会让棋子消失。统一在 OnPaint 里根据棋盘数组的状态画出来,数据驱动界面,界面不直接改数据。

3.2 四方向五连判定:边界处理与方向向量的写法

五子棋胜负判定看起来简单,写错的人却不少。常见错误包括:只检查了横向和纵向、漏了斜向;从当前点向一个方向数满五个就返回,没有考虑两边夹住的情况;数组越界。把四个方向写成方向向量数组,统一处理,“判断五连”这件事就变成一个循环遍历方向、每个方向双向计数的过程。

private static readonly int[][] Directions = { new[] { 0, 1 }, // 横向向右 new[] { 1, 0 }, // 纵向向下 new[] { 1, 1 }, // 右下斜线 new[] { 1, -1 } // 左下斜线 }; public bool CheckWin(int row, int col) { int color = Board[row, col]; if (color == 0) return false; foreach (var dir in Directions) { int count = 1; // 当前落子本身算一个 count += CountDirection(row, col, dir[0], dir[1], color); count += CountDirection(row, col, -dir[0], -dir[1], color); if (count >= 5) { return true; } } return false; } private int CountDirection(int row, int col, int dr, int dc, int color) { int count = 0; int r = row + dr; int c = col + dc; while (r >= 0 && r < BoardSize && c >= 0 && c < BoardSize && Board[r, c] == color) { count++; r += dr; c += dc; } return count; }

这里要注意方向向量(1, -1)表示从左下到右上,配合(0,1)、(1,0)、(1,1)就覆盖了横、竖、两条对角线,不需要额外再写四个“反向”向量,因为 CountDirection 会分别朝正反两个方向数。边界检查放在 while 条件里,保证第一次越界就停下,不会抛出索引异常。很多人翻车在“下到棋盘边缘时五连判定漏判”,实际上就是边界条件写错,导致边缘位置的斜线方向没被数到。

3.3 简单 AI 的评分思路:打分权重和先防后攻的参数

大学项目的人工智能不必做到 AlphaGo 级别,也不需要引入 rapfi 这类开源引擎,做一个“能缠斗、不会明显乱下”的评分式 AI 就足够了。核心思路是:遍历所有空位,对每个空位分别计算“我下这里能形成多大优势”和“对手下这里能形成多大威胁”,加权求和后取最高分落子。

public Point GetBestMove(List<Point> emptyPoints) { Point best = emptyPoints[0]; int bestScore = -1; foreach (var p in emptyPoints) { int attack = EvaluatePoint(p, AiColor); int defend = EvaluatePoint(p, HumanColor); int total = (int)(attack * 1.0 + defend * 3.0); if (total > bestScore) { bestScore = total; best = p; } } return best; } private int EvaluatePoint(Point p, int color) { int score = 0; foreach (var dir in Directions) { int count = 1; // 假设这个空位已经落下 color count += CountDirection(p.X, p.Y, dir[0], dir[1], color); count += CountDirection(p.X, p.Y, -dir[0], -dir[1], color); score += WeightOf(count); } return score; } private int WeightOf(int count) { switch (count) { case 1: return 1; case 2: return 10; case 3: return 100; case 4: return 10000; default: return 100000; // 成五 } }

attack 和 defend 的权重比是这套 AI 的“性格开关”。上面给出的 1:3 是偏防守的参数,适合陪练:AI 会优先堵你的活三和冲四,不会闷头自己连五。想要 AI 更激进,把防守权重从 3 降到 1 即可,但那样 AI 容易被对手带节奏。一个容易被忽视的细节是:在下子在四个方向都连成五时,权重函数直接返回 100000,保证了“能赢的棋一定走”;同样地,当对手存在成五点时,防守分会被无限放大,AI 不会视而不见。这套方法虽然简单,但实测能对新手形成足够压力,也能在答辩时讲清楚每一个参数的含义。

4. 数据库模块:用 SQLite 把棋局存下来,回放才算完整

4.1 为什么选 SQLite:免安装、单文件、答辩易展示

数据库选型是很多大学项目反复纠结的地方。用 SQL Server 需要在机器上装数据库服务、配连接字符串、处理登录认证;用 MySQL 也一样,演示时换个电脑可能就连接失败。而“源码+数据库”里的数据库文件如果是指一个能被交付物直接带走的文件,SQLite 是唯一比较省心的答案:它不需要安装任何服务,数据库就是一个 .db 文件,拷走目录就等于带走了全部棋局数据。

用 SQLite 的另一个好处是答辩时可以直接用可视化工具打开文件看表数据,老师很难从逻辑上挑出“数据库不落地”的毛病。技术选型上,我用的是 Microsoft.Data.Sqlite 这个 NuGet 包,在 Visual Studio 的包管理器控制台执行安装命令即可,不依赖额外运行时。

Install-Package Microsoft.Data.Sqlite

安装完成后,连接字符串只需要指定数据库文件路径。如果文件在程序目录下,程序首次运行时会自动创建;如果文件不存在,SQLite 的连接甚至不会立刻报错,真正执行建表语句时才需要处理目录和文件的细节。整体来说,SQLite 在大学项目的部署复杂度是最低一档,实践上几乎没有理由为了“更正式”去换成重型数据库管理系统。

4.2 三张表的设计与增删改查

数据库模块的设计目标是支持“整局棋保存”和“按棋局回放”,只存一个二维数组是不行的,那样无法区分执棋顺序,也无法按步数逐手回放。我通常建三张表:Game 表记录一局棋的宏观信息,Move 表记录每一步棋的坐标和顺序,Player 表记录参与玩家与胜场数。

CREATE TABLE IF NOT EXISTS Game ( GameId INTEGER PRIMARY KEY AUTOINCREMENT, Winner TEXT NOT NULL, StartTime TEXT NOT NULL DEFAULT (datetime('now','localtime')), StepCount INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS Move ( MoveId INTEGER PRIMARY KEY AUTOINCREMENT, GameId INTEGER NOT NULL, StepNo INTEGER NOT NULL, RowIdx INTEGER NOT NULL, ColIdx INTEGER NOT NULL, Player TEXT NOT NULL, FOREIGN KEY (GameId) REFERENCES Game(GameId) ); CREATE TABLE IF NOT EXISTS Player ( PlayerId INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL UNIQUE, WinCount INTEGER NOT NULL DEFAULT 0 );

这张设计的核心是 Move 表与 Game 表通过 GameId 建立外键关系,一局棋对应多步棋。保存一局棋时最重要的一点是使用事务:先插入 Game 得到新的 GameId,再循环插入所有 Move。如果不加事务,中途任何一步失败都会留下“只有棋局没有棋步”的孤儿数据,回放时就会莫名其妙少子。用事务包住,要么整局棋全部写入,要么不写,这是一个很容易被忽视的边界。

public int SaveGame(string winner, List<Move> moves) { using var conn = new SqliteConnection(_connStr); conn.Open(); using var tx = conn.BeginTransaction(); // 先插入棋局主表,拿到自增主键 var cmd = conn.CreateCommand(); cmd.Transaction = tx; cmd.CommandText = @" INSERT INTO Game(Winner, StepCount) VALUES ($winner, $count); SELECT last_insert_rowid();"; cmd.Parameters.AddWithValue("$winner", winner); cmd.Parameters.AddWithValue("$count", moves.Count); int gameId = Convert.ToInt32(cmd.ExecuteScalar()); // 再逐条插入棋步,统一挂在同一个事务下 foreach (var m in moves) { var moveCmd = conn.CreateCommand(); moveCmd.Transaction = tx; moveCmd.CommandText = @" INSERT INTO Move(GameId, StepNo, RowIdx, ColIdx, Player) VALUES ($gid, $step, $row, $col, $player);"; moveCmd.Parameters.AddWithValue("$gid", gameId); moveCmd.Parameters.AddWithValue("$step", m.StepNo); moveCmd.Parameters.AddWithValue("$row", m.Row); moveCmd.Parameters.AddWithValue("$col", m.Col); moveCmd.Parameters.AddWithValue("$player", m.Player); moveCmd.ExecuteNonQuery(); } tx.Commit(); return gameId; }

注意这里没有任何一条 SQL 是拼接字符串生成的,所有值都走参数化。一方面是为了防止 SQL 注入,另一方面参数化能让字符串里的单引号等特殊字符安全入库,不会因为玩家名里带了一个特殊符号导致整局棋保存失败。ExecuteScalar 取得的是 SQLite 自增主键,这样后插入的 Move 能准确关联到刚才创建的 Game。脏数据、乱码这一类问题,大部分都出在没走参数化这一层。

4.3 落子序列保存与回放:从数据库读到棋盘动画

回放是数据库模块最直接的展示功能。实现思路很简单:从数据库查出某一局的所有 Move,按 StepNo 排序,再用一个 Timer 定时把每一步棋放到棋盘上。真正值得关注的是查询代码和定时器的配合方式。

public List<Move> LoadMoves(int gameId) { var moves = new List<Move>(); using var conn = new SqliteConnection(_connStr); conn.Open(); var cmd = conn.CreateCommand(); cmd.CommandText = @" SELECT StepNo, RowIdx, ColIdx, Player FROM Move WHERE GameId = $gid ORDER BY StepNo;"; cmd.Parameters.AddWithValue("$gid", gameId); using var reader = cmd.ExecuteReader(); while (reader.Read()) { moves.Add(new Move { StepNo = reader.GetInt32(0), Row = reader.GetInt32(1), Col = reader.GetInt32(2), Player = reader.GetString(3) }); } return moves; }

回放时不能把整个回放过程放在一个 for 循环里睡 300 毫秒再画棋子,那样界面会卡死。正确的做法是先把所有步骤读进内存,然后启动一个 System.Windows.Forms.Timer,每次 Tick 只放一颗棋子,步数加一,直到放完为止。Timer 的 Interval 属性建议设成 300 到 500 毫秒,太慢会显得拖沓,太快则看不清落子顺序。每一步的落子仍然走正常的下棋方法,这样回放结束时还能继续用 CheckWin 验证这局棋的胜负结果,形成用户可见的数据闭环。

“数据库增删改查”是课设里最常见的考察点,这一套代码把增删改查完整覆盖了:插入棋局和棋步、按 GameId 查询棋步,玩家统计则是按 Name 更新 WinCount 时用 UPDATE;删除功能用于清空测试数据,掉进坑里的概率不大,但有时候删除主表时需要先删掉对应 Move 再删 Game,遵守外键顺序可以避免残留无效记录。

5. 避坑记:C# 五子棋项目最容易翻车的 5 个地方

5.1 现象:鼠标点交叉点,棋子却落到隔壁格子

原因:像素坐标换算用的不是行列值,而是直接把鼠标位置除以格子大小,忽略了 Margin 留白,或者 ToBoard 的命中半径设得太小。还有一种隐蔽情况:在窗口 DPI 缩放比例不是 100% 的电脑上,鼠标坐标和绘制坐标不一致,导致点击偏移。解决:统一用第 2.3 节里的 ToBoard 方法,它在缩放时也会出问题,所以实际上更稳妥的是在窗体 Load 事件里把 AutoScaleMode 设为 None,固定像素单位,课程设计项目不需要为高 DPI 做额外适配。把命中半径放宽到格子边长的一半,可大幅提升点击成功率。

5.2 现象:拖动窗口边缘时整个棋盘疯狂闪烁

原因:没有开启双缓冲,或者同时使用了多种重绘方式(既重写了 OnPaint 又在事件里调用 CreateGraphics)。闪烁的本质是背景擦除和前景绘制交替出现,肉眼看到的就是白屏和棋子交错。解决:用一种可靠方式固定重绘路径。构造函数里注册 DoubleBuffered 为 true,所有绘制逻辑只放在 OnPaint 里。显示新棋子时调用 Invalidate 让系统安排重绘,而不是自己调用 Refresh 再立刻画图。改完以后如果还闪,检查是否在 Paint 事件里创建了新的 Pen 或 Brush 没有释放,GDI+ 对象泄漏会拖慢绘制。这个问题的标准答案就四个字:双缓冲。

5.3 现象:项目在自己电脑上正常,换台电脑就提示打不开数据库

原因:数据库路径用了 Environment.CurrentDirectory。它在 Visual Studio 调试时指向 binDebug 目录,双击 exe 运行时指向 exe 所在目录,某些场景下还可能是 C 盘的系统目录。一旦路径不一致,程序可能在错误的位置找不到 .db 文件,或在错误位置新建了一个空白库让用户误以为数据丢失。解决:统一用 AppDomain.CurrentDomain.BaseDirectory 拼接数据库文件名。注意在打开连接前手动创建目录:

string dbDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Data"); Directory.CreateDirectory(dbDir); string dbPath = Path.Combine(dbDir, "gobang.db");

这个方法解决的不是运行逻辑问题,而是交付问题。把数据库文件放到 exe 同级的 Data 目录下,整个项目拷贝给别人时,只要目录完整就能跑通,不会在答辩时因为路径问题当场翻车。数据库路径属于“黑匣子”问题,平时运行正常根本不会注意,一旦换环境就全盘暴露,建议从第一天就按这个方式写。

5.4 现象:AI 落子时窗口卡住,过两秒才恢复

原因:AI 计算在 UI 线程里同步执行,遍历所有空位并评分时阻塞了消息循环,窗口自然就“无响应”了。解决:把 AI 落子放到线程池里,计算完成后再跳回 UI 线程更新棋盘。常见做法是 Task.Run 包住 GetBestMove,然后在 ContinueWith 里用 Invoke 更新界面。更进一步,缩小搜索空间也是一种优化:当前棋局刚落下一子时,只扫描距离最后落子点三格半径内的空位,棋盘边缘位置根本不会生成有效威胁,评分结果不会受影响。

Task.Run(() => _ai.GetBestMove(GetNearbyEmptyPoints())) .ContinueWith(t => { var p = t.Result; PlacePiece(p.Row, p.Col, AiColor); }, TaskScheduler.FromCurrentSynchronizationContext());

这里关键的改动是把“计算”和“画棋”拆到不同线程。ContinueWith 里指定了从当前同步上下文调度,确保回到 UI 线程执行 PlacePiece,不会触发跨线程访问控件的异常。如果 AI 还是慢,多半是 EvaluatePoint 里没有做“成五短路”,可以在找到 100000 分后直接返回,不需要继续算其他方向。

5.5 现象:数据库中玩家名显示乱码,回放到一半棋子不对

原因:乱码大概率是读取时用了错误的编码,SQLite 本身按 UTF-8 存储,Microsoft.Data.Sqlite 默认行为是对的;真正出问题的通常是 Player 表里插入了异常字符,或者查询时 Convert.ToString 对 NULL 值抛了异常。回放棋子不对则是 Move 表没有按 StepNo 排序。解决:写入读出都走参数化,读取时用 reader.GetString 或 Convert.ToString 的空安全转换,确保整局棋的每步棋在保存前都已经按顺序赋值。

string player = reader.IsDBNull(3) ? string.Empty : reader.GetString(3);

顺带一提,回放结束前不要把棋盘清零,否则最后一步棋子会消失。这个 bug 很像支付逻辑里的“先扣款后退款”,界面状态和数据库状态对不上,答辩时会被老师一眼看穿。检查回放逻辑时,把最后几步的棋盘状态截图和数据库 StepNo 对比,是最高效的排查方式。

6. 答辩演示的价值点:把“能玩”升级成“能讲清楚”

一个五子棋项目如果只停在“能玩”,答辩时大概三分钟就讲完了,剩下的时间会变成老师帮你找 bug。想拿高分,得把演示顺序设计成一条故事线:第一局人机对战展示玩法和 AI,第二局展示回放功能,第三局打开数据库表结构,说明每一步棋是怎么落进 Move 表的。时间控制在 8 到 10 分钟,每讲一个模块就切一次画面,比干讲代码直观得多。

我建议加一个小功能:把棋谱导出成文本文件。每行记录“回合号、行、列、棋子颜色”,这个动作在答辩时非常加分。它把数据库里的数据变成了人能读懂的东西,老师可以现场打开文件看到第 17 手在哪里下的,代码量只需要十几行。同时窗口标题动态显示当前步数,例如“黑方第 23 手”,也会让演示显得更完整。

// 窗口标题实时更新当前落子数 this.Text = $"C# 五子棋 —— 已落 {_stepCount} 手,当前执棋:{_currentPlayer}";

到了项目后期,还有一个容易忽略却很重要的习惯:正式答辩前一天,把数据库文件删掉,从空库重新跑三局完整对局,然后打开表检查数据。这个操作能暴露路径问题、事务残留、回放错乱等一系列隐藏问题。不要只在调试状态下测试,要从“把项目拷贝到另一台机器”的角度走一遍流程。

我自己的经验是,代码功能写完只算完成一半,另外一半在数据闭环上。能存、能读、能回放、能导出,才是一个有说服力的“源码+数据库”项目。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询