☰
C#五子棋课程设计:WinForms+ADO.NET+SQL Server完整源码与数据库实战
2026/10/8 3:46:26 网站建设 项目流程

简介:这份资源是面向高校计算机专业学生的C#五子棋课程项目完整源码包,适合正在准备课程设计、期末大作业或想通过实战巩固面向对象编程的学习者。项目覆盖人机对战与局域网联机两种模式,涉及Minimax或Alpha-Beta剪枝AI算法、TCP/IP网络通信、SQLite数据库存储玩家信息与对战记录,并包含BGM播放、悔棋、Windows Forms界面交互等实用功能。压缩包共200个文件,以53个cs源码、47个png界面素材、33个resx与30个resources资源文件为主,另含9个dll依赖、4个mp3音频及mdf/ldf数据库文件,整体约53.01MB,结构完整可直接编译运行。目前已有615人学习下载,读者可参照源码理解棋盘状态同步、心跳机制、ADO.NET数据操作与游戏逻辑分层设计,是提升综合编程能力的实战参考。

1. C# 五子棋大学项目:从源码到数据库,一套能跑通的完整方案

很多同学做 C# 五子棋课程设计时,卡住的地方往往不是棋盘绘制,而是「源码写完了,数据库怎么接进去」。我见过太多版本:棋盘用 Panel 画得挺漂亮,胜负判断也能跑,但一提到数据库就变成把数据写进 txt 文件,或者干脆用内存里的 List 假装持久化。这个标题里的「源码+数据库」其实点出了一个真实需求——你需要一套能演示完整数据链路的五子棋项目:棋盘逻辑、胜负判定、玩家信息、对局记录、排行榜,全部落到数据库里。

这篇文章面向正在做 C# 课程设计、想拿一个能讲清楚架构的项目去答辩的同学,也面向想用五子棋练手 WinForms + ADO.NET 的初学者。我会把项目拆成可复现的步骤:先定技术选型,再建数据库表,然后写棋盘和胜负判断,接着把数据存取接进去,最后讲几个我踩过的坑。整套方案用 WinForms + SQL Server LocalDB 或 SQLite 都能跑,代码结构清晰,答辩时能讲出「为什么这么设计」。

2. 技术选型与数据库设计:为什么用 WinForms + ADO.NET 而不是 EF Core

2.1 大学项目里 WinForms 仍然是性价比最高的选择

做课程设计,时间通常只有两三周,答辩时老师看的是「功能完整、代码能讲清楚、数据能存下来」。WPF 的 MVVM 架构更现代,但学习曲线陡,绑定写错一个属性名就白屏,调试成本高。WinForms 虽然老,但拖控件、写事件、直接调 GDI+ 画棋盘,对新手最友好。我一般建议:主界面用 WinForms,棋盘用 Panel 的 Paint 事件画,鼠标点击用 MouseClick 事件换算坐标,这样代码量可控,逻辑也直观。

数据库方面,SQL Server LocalDB 是 Visual Studio 自带的,不需要额外装服务,连接字符串写(localdb)\MSSQLLocalDB就能用。如果学校机房不让装 SQL Server,退而求其次用 SQLite,只需要一个 NuGet 包System.Data.SQLite,数据库就是一个文件,拷贝走就能演示。两种方案在代码层面差别不大,主要是连接字符串和参数占位符不同。

提示:如果答辩环境不能联网,提前把 NuGet 包离线安装好,或者直接用 LocalDB,避免现场装包翻车。

2.2 数据库表设计:三张表撑起玩家、对局、落子记录

很多同学只建一张Users表就完事,结果对局记录没地方存,排行榜只能靠内存。我建议至少三张表:

表名字段用途
PlayersId, Name, CreatedAt玩家基本信息
GamesId, BlackPlayerId, WhitePlayerId, WinnerId, StartedAt, EndedAt每局对战记录
MovesId, GameId, PlayerId, X, Y, MoveOrder每一步落子

Players表存玩家,Games表存对局,Moves表存每一步。这样排行榜可以按WinnerId统计胜场,复盘可以按MoveOrder回放。建表 SQL 如下:

CREATE TABLE Players ( Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL UNIQUE, CreatedAt DATETIME DEFAULT GETDATE() ); CREATE TABLE Games ( Id INT IDENTITY(1,1) PRIMARY KEY, BlackPlayerId INT NOT NULL, WhitePlayerId INT NOT NULL, WinnerId INT NULL, StartedAt DATETIME DEFAULT GETDATE(), EndedAt DATETIME NULL, FOREIGN KEY (BlackPlayerId) REFERENCES Players(Id), FOREIGN KEY (WhitePlayerId) REFERENCES Players(Id), FOREIGN KEY (WinnerId) REFERENCES Players(Id) ); CREATE TABLE Moves ( Id INT IDENTITY(1,1) PRIMARY KEY, GameId INT NOT NULL, PlayerId INT NOT NULL, X INT NOT NULL, Y INT NOT NULL, MoveOrder INT NOT NULL, FOREIGN KEY (GameId) REFERENCES Games(Id), FOREIGN KEY (PlayerId) REFERENCES Players(Id) );

WinnerId允许为 NULL,表示平局或未结束。Moves表的MoveOrder从 1 开始递增,回放时按这个字段排序。注意X和Y存的是棋盘坐标(0 到 14),不是像素坐标,这样跟界面解耦。

2.3 用 ADO.NET 还是 EF Core:课程设计的取舍

EF Core 写起来快,db.Players.Add(player)就完事,但课程设计答辩时老师常问「SQL 语句在哪」,用 EF Core 反而不好展示。ADO.NET 虽然要手写SqlCommand,但每一步都透明,适合教学场景。我一般用 ADO.NET,封装一个DatabaseHelper类,把连接字符串和常用操作包起来。

public static class DatabaseHelper { private static readonly string ConnStr = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=GomokuDB;Integrated Security=True"; public static SqlConnection GetConnection() { var conn = new SqlConnection(ConnStr); conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn = GetConnection()) using (var cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } public static object ExecuteScalar(string sql, params SqlParameter[] parameters) { using (var conn = GetConnection()) using (var cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteScalar(); } } }

GetConnection每次新建连接,ADO.NET 有连接池,不会真的反复打开关闭。ExecuteNonQuery用于 INSERT/UPDATE/DELETE,ExecuteScalar用于 SELECT 单值(比如刚插入的 Id)。参数用SqlParameter传,不要拼字符串,否则名字里带单引号就翻车。

3. 棋盘绘制与胜负判断:GDI+ 画格子和五连检测的完整实现

3.1 用 Panel 的 Paint 事件画 15x15 棋盘

棋盘不用 PictureBox 贴图,直接用 GDI+ 画,代码可控,缩放也方便。核心是算好格子大小和边距,让棋盘居中。

private const int BoardSize = 15; private const int CellSize = 36; private const int Margin = 24; private int[,] board = new int[BoardSize, BoardSize]; // 0空 1黑 2白 private void panelBoard_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; // 画网格线 using (var pen = new Pen(Color.Black, 1)) { for (int i = 0; i < BoardSize; i++) { int x = Margin + i * CellSize; int y = Margin + i * CellSize; g.DrawLine(pen, Margin, y, Margin + (BoardSize - 1) * CellSize, y); g.DrawLine(pen, x, Margin, x, Margin + (BoardSize - 1) * CellSize); } } // 画棋子 for (int r = 0; r < BoardSize; r++) { for (int c = 0; c < BoardSize; c++) { if (board[r, c] == 0) continue; int cx = Margin + c * CellSize; int cy = Margin + r * CellSize; var brush = board[r, c] == 1 ? Brushes.Black : Brushes.White; g.FillEllipse(brush, cx - CellSize / 2 + 2, cy - CellSize / 2 + 2, CellSize - 4, CellSize - 4); if (board[r, c] == 2) g.DrawEllipse(Pens.Black, cx - CellSize / 2 + 2, cy - CellSize / 2 + 2, CellSize - 4, CellSize - 4); } } }

CellSize是格子间距,Margin是棋盘边距。棋子画在交叉点上,所以圆心坐标是Margin + c * CellSize。白棋要额外描边,否则在浅色背景上看不清。每次落子后调panelBoard.Invalidate()触发重绘。

3.2 鼠标点击换算棋盘坐标

点击事件里把像素坐标转成行列索引,注意四舍五入到最近的交叉点。

private void panelBoard_MouseClick(object sender, MouseEventArgs e) { int col = (int)Math.Round((e.X - Margin) / (double)CellSize); int row = (int)Math.Round((e.Y - Margin) / (double)CellSize); if (row < 0 || row >= BoardSize || col < 0 || col >= BoardSize) return; if (board[row, col] != 0) return; board[row, col] = currentPlayer; panelBoard.Invalidate(); // 记录落子到数据库 SaveMove(currentGameId, currentPlayerId, row, col, moveOrder++); if (CheckWin(row, col, currentPlayer)) { MessageBox.Show(currentPlayer == 1 ? "黑棋胜" : "白棋胜"); EndGame(currentPlayerId); return; } currentPlayer = currentPlayer == 1 ? 2 : 1; }

Math.Round保证点在线附近也能落到最近交叉点。SaveMove和EndGame是数据库操作,后面讲。moveOrder是全局计数器,每局开始重置为 1。

3.3 五连检测:四个方向扫描,注意边界

胜负判断是五子棋的核心,思路是从落子点向四个方向延伸,统计连续同色棋子数。

private bool CheckWin(int row, int col, int player) { int[][] dirs = new int[][] { new int[] { 0, 1 }, // 横 new int[] { 1, 0 }, // 竖 new int[] { 1, 1 }, // 右下 new int[] { 1, -1 } // 左下 }; foreach (var dir in dirs) { int count = 1; for (int step = 1; step < 5; step++) { int r = row + dir[0] * step; int c = col + dir[1] * step; if (r < 0 || r >= BoardSize || c < 0 || c >= BoardSize) break; if (board[r, c] != player) break; count++; } for (int step = 1; step < 5; step++) { int r = row - dir[0] * step; int c = col - dir[1] * step; if (r < 0 || r >= BoardSize || c < 0 || c >= BoardSize) break; if (board[r, c] != player) break; count++; } if (count >= 5) return true; } return false; }

四个方向分别向正负两个方向扫,count初始为 1(落子本身)。边界检查不能省,否则数组越界直接崩。count >= 5而不是== 5,因为长连也算赢(除非你规则里禁长连,那另说)。

4. 把对局写进数据库:玩家、落子、胜负的持久化

4.1 玩家登录与注册:用参数化查询防注入

启动时先让玩家输入名字,存在就查 Id,不存在就插入。

public static int GetOrCreatePlayer(string name) { var result = DatabaseHelper.ExecuteScalar( "SELECT Id FROM Players WHERE Name = @name", new SqlParameter("@name", name)); if (result != null) return (int)result; DatabaseHelper.ExecuteNonQuery( "INSERT INTO Players (Name) VALUES (@name)", new SqlParameter("@name", name)); return (int)DatabaseHelper.ExecuteScalar( "SELECT Id FROM Players WHERE Name = @name", new SqlParameter("@name", name)); }

先查后插,避免重复。@name是参数占位符,SQL Server 用@,SQLite 用@或?都行。不要用字符串拼接,否则名字叫O'Brien就报错。

4.2 开局时插入 Games 记录并拿到 GameId

每局开始,先插入一条 Games 记录,拿到自增 Id 作为currentGameId。

public static int StartGame(int blackId, int whiteId) { DatabaseHelper.ExecuteNonQuery( "INSERT INTO Games (BlackPlayerId, WhitePlayerId) VALUES (@b, @w)", new SqlParameter("@b", blackId), new SqlParameter("@w", whiteId)); return (int)DatabaseHelper.ExecuteScalar("SELECT SCOPE_IDENTITY()"); }

SCOPE_IDENTITY()返回当前会话最后插入的自增 Id,比@@IDENTITY安全,不会拿到触发器插入的 Id。SQLite 用last_insert_rowid()。

4.3 每步落子写入 Moves 表

落子时调SaveMove,把 GameId、PlayerId、坐标、顺序写进去。

public static void SaveMove(int gameId, int playerId, int x, int y, int order) { DatabaseHelper.ExecuteNonQuery( "INSERT INTO Moves (GameId, PlayerId, X, Y, MoveOrder) VALUES (@g, @p, @x, @y, @o)", new SqlParameter("@g", gameId), new SqlParameter("@p", playerId), new SqlParameter("@x", x), new SqlParameter("@y", y), new SqlParameter("@o", order)); }

MoveOrder从 1 递增,回放时ORDER BY MoveOrder就能还原对局。注意X和Y存的是行列索引,不是像素。

4.4 结束时更新 WinnerId 和 EndedAt

胜负判定后,更新 Games 表。

public static void EndGame(int gameId, int? winnerId) { DatabaseHelper.ExecuteNonQuery( "UPDATE Games SET WinnerId = @w, EndedAt = GETDATE() WHERE Id = @id", new SqlParameter("@w", (object)winnerId ?? DBNull.Value), new SqlParameter("@id", gameId)); }

winnerId为 null 时传DBNull.Value,不能直接传 null,否则 SqlParameter 会报错。平局就传 null。

4.5 排行榜查询:按胜场排序

排行榜用一条 SQL 搞定。

SELECT p.Name, COUNT(*) AS Wins FROM Games g JOIN Players p ON g.WinnerId = p.Id WHERE g.WinnerId IS NOT NULL GROUP BY p.Name ORDER BY Wins DESC;

在 C# 里用SqlDataReader读出来填到 ListView 或 DataGridView。注意COUNT(*)统计的是胜场,不是对局数。如果要显示总对局数,再 join 一次。

5. 避坑与排查:课程设计里最容易翻车的五个地方

5.1 连接字符串写错,LocalDB 实例名对不上

现象:运行时报「无法连接到 SQL Server」,或者「找不到 LocalDB 实例」。原因:连接字符串里实例名写成了(localdb)\v11.0或.\SQLEXPRESS,但机器上装的是MSSQLLocalDB。解决:在 Visual Studio 的「SQL Server 对象资源管理器」里看实际实例名,或者用sqllocaldb info命令列出所有实例。连接字符串统一用(localdb)\MSSQLLocalDB,这是 VS 2019/2022 默认的。

5.2 棋盘重绘闪烁,棋子画一半消失

现象:落子后棋盘闪一下,棋子有时显示不全。原因:Panel默认没开双缓冲,Invalidate触发重绘时先擦背景再画,中间态被看到。解决:自定义一个继承 Panel 的类,构造函数里设DoubleBuffered = true和SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true)。或者直接在 Form 的构造函数里对 panel 设DoubleBuffered,但 Panel 的该属性是 protected,得用反射或子类。

5.3 落子顺序错乱,回放时棋子位置对不上

现象:回放对局时,棋子出现的顺序和实际下棋顺序不一致。原因:MoveOrder用了全局静态变量,新开一局没重置,或者多局并发时互相覆盖。解决:每局开始时把moveOrder重置为 1,并且把它作为Game对象的字段,不要用 static。插入时严格按当前局递增,查询时ORDER BY MoveOrder。

5.4 参数化查询里 DBNull 和 null 混用

现象:平局时更新WinnerId报「参数化查询未提供参数值」。原因:C# 的null和数据库的DBNull.Value不是一回事,SqlParameter不接受 null。解决:统一写(object)value ?? DBNull.Value,或者判断if (value == null) param.Value = DBNull.Value;。这个坑在WinnerId、EndedAt这类可空字段上特别常见。

5.5 数据库文件没跟着项目走,换台机器就找不到

现象:在自己电脑上跑得好好的,拷到同学电脑或答辩机房就报「数据库不存在」。原因:用了 SQLite 但数据库文件路径写的是绝对路径,或者用了 LocalDB 但没导出建表脚本。解决:SQLite 用相对路径|DataDirectory|,把 .db 文件放在输出目录;LocalDB 则把建表 SQL 存成init.sql,程序启动时检查表是否存在,不存在就执行建表。这样换机器只要跑一次程序就能自动建库。

6. 进阶技巧:用 Moves 表做对局回放和 AI 落子分析

6.1 从 Moves 表回放整局对弈

有了Moves表,回放就是按MoveOrder顺序重放落子。写一个ReplayGame(int gameId)方法,先清空棋盘,再逐步读取。

public static List<MoveRecord> GetMoves(int gameId) { var list = new List<MoveRecord>(); using (var conn = DatabaseHelper.GetConnection()) using (var cmd = new SqlCommand( "SELECT PlayerId, X, Y, MoveOrder FROM Moves WHERE GameId = @g ORDER BY MoveOrder", conn)) { cmd.Parameters.AddWithValue("@g", gameId); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new MoveRecord { PlayerId = reader.GetInt32(0), X = reader.GetInt32(1), Y = reader.GetInt32(2), Order = reader.GetInt32(3) }); } } } return list; }

拿到列表后,用一个Timer每隔 500ms 重放一步,调board[x, y] = playerId然后Invalidate()。这样答辩时能演示「历史对局回放」,比单纯下棋更有说服力。注意PlayerId要映射回黑棋(1)还是白棋(2),可以在Games表里查BlackPlayerId和WhitePlayerId对比。

6.2 用落子数据做简单的 AI 评分

如果想再加点亮点,可以用Moves表统计每个位置的落子频率,做一个最简 AI:在空位里选一个「历史上下得最多」的位置。虽然粗糙,但能讲出「数据驱动」的思路。

public static (int x, int y) SuggestMove(int gameId) { var sql = @" SELECT TOP 1 X, Y, COUNT(*) AS Freq FROM Moves WHERE GameId = @g GROUP BY X, Y ORDER BY Freq DESC"; using (var conn = DatabaseHelper.GetConnection()) using (var cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@g", gameId); using (var reader = cmd.ExecuteReader()) { if (reader.Read()) return (reader.GetInt32(0), reader.GetInt32(1)); } } return (7, 7); // 默认天元 }

这个查询统计当前对局里出现次数最多的坐标,作为推荐落子点。实际 AI 当然要用极大极小或蒙特卡洛,但课程设计里能展示「从数据库读数据、做简单统计、给出建议」这条链路,已经比纯随机落子强很多。注意TOP 1是 SQL Server 语法,SQLite 用LIMIT 1。

6.3 导出对局记录到 CSV 做答辩材料

答辩时老师可能要看数据,把Moves表导成 CSV 很实用。

public static void ExportMovesToCsv(int gameId, string path) { var moves = GetMoves(gameId); using (var writer = new StreamWriter(path)) { writer.WriteLine("Order,PlayerId,X,Y"); foreach (var m in moves) writer.WriteLine($"{m.Order},{m.PlayerId},{m.X},{m.Y}"); } }

导出后用 Excel 打开,能画个落子热力图,答辩时放 PPT 里很直观。路径用Path.Combine(Application.StartupPath, "moves.csv"),避免权限问题。

6.4 我自己的习惯:先跑通数据链路,再优化界面

我做这类课程设计有个固定顺序:先建库建表,写一个控制台程序把增删改查跑通,确认数据能进能出;然后再做 WinForms 界面,把按钮事件接到已经验证过的数据库方法上;最后才调界面细节和动画。这样如果数据库连不上,问题一定在连接字符串或 SQL,不会跟界面代码混在一起。血泪经验是:别一上来就拖控件画界面,等界面画好了才发现数据库连不上,回头改代码更痛苦。希望帮到你。

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

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

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

立即咨询