ARTICLE DETAIL

资讯详情

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

C#五子棋课程设计实战:WinForms界面+SQLite数据库完整实现

C#五子棋课程设计实战:WinForms界面+SQLite数据库完整实现 简介这是一份面向计算机专业学生与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(ListPoint 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, ListMove 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 ListMove LoadMoves(int gameId) { var moves new ListMove(); 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};到了项目后期还有一个容易忽略却很重要的习惯正式答辩前一天把数据库文件删掉从空库重新跑三局完整对局然后打开表检查数据。这个操作能暴露路径问题、事务残留、回放错乱等一系列隐藏问题。不要只在调试状态下测试要从“把项目拷贝到另一台机器”的角度走一遍流程。我自己的经验是代码功能写完只算完成一半另外一半在数据闭环上。能存、能读、能回放、能导出才是一个有说服力的“源码数据库”项目。希望帮到你。本文还有配套的精品资源点击获取
返回列表