简介:这份资源是面向高校计算机专业学生的C#五子棋课程项目完整源码包,适合正在做课程设计、需要参考完整项目结构或准备答辩演示的同学。项目覆盖C#面向对象编程、人机对战AI算法、局域网联机通信、数据库存储玩家信息与对战记录,以及BGM播放、悔棋、界面交互等模块,可作为综合练习或二次开发的基础。压缩包共200个文件,约53.01MB,以cs源码、png界面素材、resx与resources资源文件为主,另含dll依赖、mp3背景音乐、config配置、exe可执行程序及mdf/ldf数据库文件,结构完整可直接运行。目前已有615人学习下载,能帮助读者快速理解五子棋项目的模块划分、AI决策与网络同步思路,并对照数据库设计完善自己的课程作业。
1. C# 五子棋大学项目:从源码到数据库,一套能跑通的完整方案
很多计算机专业的学生在做课程设计时,都会遇到一个经典选题——五子棋。但真正动手时才发现,网上能找到的要么是纯控制台版本,要么是只有界面没有数据持久化的半成品,想找一个带数据库、能存棋谱、能管理用户信息的完整 C# 项目并不容易。这里要讲的这套方案,核心就是用 C# WinForms 做界面交互,用 SQL Server 或 SQLite 做数据存储,把用户注册登录、对局记录、棋谱回放、胜负统计串成一条完整链路。它适合正在做课程设计的学生,也适合想用一个小项目练手 C# 数据库增删改查和 WinForms 绘图的开发者。下面从架构选型开始,一步步拆到能复现的程度。
2. 架构选型与环境搭建:WinForms + ADO.NET 为什么比 EF 更适合这个项目
2.1 技术栈的取舍逻辑
大学项目有一个很现实的特点:周期短、要求明确、答辩时需要能讲清楚每一层在做什么。基于这个前提,我一般会推荐 WinForms + ADO.NET 的组合,而不是 Entity Framework 或 WPF。原因有三点:第一,WinForms 的绘图 API 足够简单,五子棋棋盘本质上就是一个 15×15 的网格,用 GDI+ 的 Graphics 对象画线和圆就能搞定,不需要复杂的绑定机制;第二,ADO.NET 的 SqlConnection、SqlCommand、SqlDataReader 这套东西是 C# 数据库操作的基本功,答辩时老师问起来你能说清楚每一步在干什么,而 EF 的 LINQ 查询虽然优雅,但底层发生了什么很多学生讲不明白;第三,SQL Server Express 或 SQLite 的部署成本低,前者在 Visual Studio 安装时自带,后者只需要一个 NuGet 包。
数据库选型上,如果学校机房统一装了 SQL Server,那就用 SQL Server;如果希望项目能拷到任何电脑上直接跑,SQLite 更省事。两者的 C# 操作代码差异很小,主要是连接字符串和少量 SQL 方言的区别。下面以 SQL Server 为主来写,SQLite 的差异点会单独标注。
2.2 开发环境搭建的具体步骤
先确认 Visual Studio 版本。VS2019 或 VS2022 社区版都可以,安装时勾选「.NET 桌面开发」工作负载,这会自带 WinForms 设计器和 SQL Server Express LocalDB。如果你打算用 SQLite,还需要在 NuGet 里装 System.Data.SQLite 包。
创建项目的步骤:
# 在 Visual Studio 中操作,对应命令行等价描述 # 1. 新建项目 -> Windows 窗体应用 (.NET Framework) # 2. 项目名设为 GomokuGame # 3. 目标框架选 .NET Framework 4.7.2 或更高 # 4. 在解决方案资源管理器中右键项目 -> 添加 -> 新建项 -> 类 # 依次添加 GameLogic.cs、DatabaseHelper.cs、UserManager.cs项目结构建议这样划分:Form1.cs 负责界面和事件响应,GameLogic.cs 放棋盘状态和胜负判断,DatabaseHelper.cs 封装所有数据库连接和命令执行,UserManager.cs 处理用户注册登录。这样分层的好处是,答辩时你可以指着文件说「这是表现层,这是业务逻辑层,这是数据访问层」,老师一听就知道你理解了三层架构。
2.3 数据库表的建表语句与字段说明
数据库至少需要两张表:用户表和棋谱表。用户表存账号信息,棋谱表存每一步的落子记录。建表 SQL 如下:
-- 用户表 CREATE TABLE Users ( UserId INT PRIMARY KEY IDENTITY(1,1), Username NVARCHAR(50) NOT NULL UNIQUE, PasswordHash NVARCHAR(128) NOT NULL, CreatedAt DATETIME DEFAULT GETDATE(), TotalGames INT DEFAULT 0, Wins INT DEFAULT 0 ); -- 棋谱表 CREATE TABLE GameRecords ( RecordId INT PRIMARY KEY IDENTITY(1,1), UserId INT NOT NULL, OpponentName NVARCHAR(50), Result NVARCHAR(10), -- 'Win' / 'Lose' / 'Draw' Moves NVARCHAR(MAX), -- 存储落子序列,格式如 "7,7;8,8;7,8" PlayedAt DATETIME DEFAULT GETDATE(), FOREIGN KEY (UserId) REFERENCES Users(UserId) );Moves 字段用 NVARCHAR(MAX) 存一个分号分隔的坐标串,每个坐标是「行,列」的格式。这样做的好处是读写简单,不需要额外建一张落子明细表。如果棋谱很长,也可以考虑用 JSON 格式存,C# 端用 Newtonsoft.Json 序列化,但大学项目里分号分隔已经够用。
注意:PasswordHash 字段不要存明文密码。哪怕是个课程设计,用 SHA256 做一次哈希也是基本操作,答辩时这是一个加分项。
3. 棋盘绘制与落子逻辑:GDI+ 画格子和鼠标事件处理
3.1 用 Graphics 画一个 15×15 的棋盘
WinForms 里画棋盘,核心是重写窗体的 Paint 事件。棋盘边距留 30 像素,格子大小设为 35 像素,这样 15×15 的棋盘总宽是 14×35 + 60 = 550 像素左右,普通窗体放得下。
private const int BoardSize = 15; private const int CellSize = 35; private const int Margin = 30; private void Form1_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 画网格线 for (int i = 0; i < BoardSize; i++) { // 横线 g.DrawLine(Pens.Black, Margin, Margin + i * CellSize, Margin + (BoardSize - 1) * CellSize, Margin + i * CellSize); // 竖线 g.DrawLine(Pens.Black, Margin + i * CellSize, Margin, Margin + i * CellSize, Margin + (BoardSize - 1) * CellSize); } // 画天元和星位(可选,增加真实感) int center = Margin + 7 * CellSize; g.FillEllipse(Brushes.Black, center - 4, center - 4, 8, 8); }这段代码的逻辑很直接:循环 15 次,每次画一条横线和一条竖线,交点就是落子位置。SmoothingMode 设为 AntiAlias 是为了让线条和棋子边缘不出现锯齿。天元位置在 (7,7),用一个小黑圆点标记。
棋子的绘制放在鼠标点击事件里,而不是 Paint 里全部重画。每次落子后调用 Invalidate() 触发重绘,在 Paint 里根据棋盘状态数组画出所有已落子的棋子。棋盘状态用一个二维数组 int[15,15] 表示,0 为空,1 为黑子,2 为白子。
3.2 鼠标点击坐标到棋盘格子的映射
鼠标点击的坐标是像素值,需要转换成棋盘的行列索引。转换公式是:行列 = (鼠标坐标 - Margin + CellSize/2) / CellSize。加 CellSize/2 是为了让点击落在格子中心附近就算有效。
private int[,] board = new int[BoardSize, BoardSize]; private bool isBlackTurn = true; private void Form1_MouseClick(object sender, MouseEventArgs e) { int col = (e.X - Margin + CellSize / 2) / CellSize; int row = (e.Y - Margin + CellSize / 2) / CellSize; // 边界检查 if (row < 0 || row >= BoardSize || col < 0 || col >= BoardSize) return; if (board[row, col] != 0) return; // 该位置已有棋子 board[row, col] = isBlackTurn ? 1 : 2; isBlackTurn = !isBlackTurn; // 记录落子到棋谱列表 moveHistory.Add($"{row},{col}"); this.Invalidate(); // 触发重绘 // 检查胜负 if (CheckWin(row, col)) { string winner = board[row, col] == 1 ? "黑方" : "白方"; MessageBox.Show($"{winner}获胜!"); SaveGameRecord(winner); } }这段代码里几个关键点:边界检查防止数组越界,已有棋子的位置不能重复落子,moveHistory 列表用来后续存数据库。CheckWin 函数需要检查四个方向——水平、垂直、两条对角线,每个方向从当前落子点向两边延伸计数,达到 5 个就判胜。
3.3 胜负判断的四个方向扫描
CheckWin 的实现思路是:以刚落的棋子为中心,分别向四个方向的正反两侧统计同色棋子数量。如果某个方向的总数达到或超过 5,就返回 true。
private bool CheckWin(int row, int col) { int color = board[row, col]; // 四个方向:水平、垂直、主对角线、副对角线 int[][] directions = new int[][] { new int[] {0, 1}, // 水平 new int[] {1, 0}, // 垂直 new int[] {1, 1}, // 主对角线 new int[] {1, -1} // 副对角线 }; foreach (var dir in directions) { 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] != color) 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] != color) break; count++; } if (count >= 5) return true; } return false; }这个算法的复杂度是常数级,因为每个方向最多检查 4 步。注意副对角线的方向向量是 {1, -1},也就是行增加、列减少。很多初学者在这里容易把方向搞反,导致斜向五连判断失效,这是一个高频翻车点。
4. 数据库持久化:用户注册登录与棋谱存取
4.1 用 ADO.NET 封装数据库连接
DatabaseHelper 类的职责是提供统一的数据库访问入口。连接字符串根据数据库类型不同而不同,SQL Server LocalDB 的典型连接串是Server=(localdb)\MSSQLLocalDB;Database=GomokuDB;Integrated Security=true;,SQLite 则是Data Source=gomoku.db;Version=3;。
public static class DatabaseHelper { private static string connStr = @"Server=(localdb)\MSSQLLocalDB;Database=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(); } } }这里用 using 语句确保连接和命令对象在使用后自动释放,避免连接泄漏。参数化查询是必须的,不要用字符串拼接 SQL,否则会有 SQL 注入风险,答辩时老师很可能会问这个问题。
4.2 用户注册与登录的完整流程
注册时,先检查用户名是否已存在,如果不存在则插入新记录。密码用 SHA256 哈希后存储。
public static bool Register(string username, string password) { // 检查用户名是否已存在 string checkSql = "SELECT COUNT(*) FROM Users WHERE Username = @name"; int count = Convert.ToInt32(DatabaseHelper.ExecuteScalar(checkSql, new SqlParameter("@name", username))); if (count > 0) return false; // 插入新用户 string hash = ComputeSha256Hash(password); string insertSql = "INSERT INTO Users (Username, PasswordHash) VALUES (@name, @hash)"; int rows = DatabaseHelper.ExecuteNonQuery(insertSql, new SqlParameter("@name", username), new SqlParameter("@hash", hash)); return rows > 0; } private static string ComputeSha256Hash(string raw) { using (var sha = System.Security.Cryptography.SHA256.Create()) { byte[] bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(raw)); return BitConverter.ToString(bytes).Replace("-", "").ToLower(); } }登录逻辑类似,先根据用户名查出 PasswordHash,然后比对哈希值。如果匹配则登录成功,同时可以更新 LastLoginTime 字段。
4.3 棋谱的序列化存储与回放读取
一局结束后,把 moveHistory 列表拼接成分号分隔的字符串,连同用户 ID、对手名、胜负结果一起插入 GameRecords 表。
public static void SaveGameRecord(int userId, string opponent, string result, List<string> moves) { string movesStr = string.Join(";", moves); string sql = @"INSERT INTO GameRecords (UserId, OpponentName, Result, Moves) VALUES (@uid, @opp, @result, @moves)"; DatabaseHelper.ExecuteNonQuery(sql, new SqlParameter("@uid", userId), new SqlParameter("@opp", opponent), new SqlParameter("@result", result), new SqlParameter("@moves", movesStr)); }回放时,从数据库读出 Moves 字段,用 Split(';') 拆成数组,然后逐步在棋盘上重绘。回放功能是答辩时的亮点,老师看到你能把历史对局重新演示出来,印象分会高不少。
提示:如果棋谱很长,NVARCHAR(MAX) 的上限是 2GB,完全够用。但建议在插入前检查一下 movesStr 的长度,超过 8000 字符时考虑压缩或分段存储。
5. 避坑与排查:五子棋项目里最容易翻车的五个地方
5.1 棋盘重绘闪烁严重
现象:每次落子后调用 Invalidate(),整个棋盘闪烁一下,体验很差。原因:WinForms 默认的重绘机制会先用背景色擦除再画新内容,这个擦除过程被人眼捕捉到了。解决:把窗体的 DoubleBuffered 属性设为 true,或者在构造函数里设置this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);。双缓冲会让绘制在内存中完成后一次性输出,闪烁就消失了。
5.2 鼠标点击位置偏移
现象:点击棋盘的某个交叉点,棋子却画在了旁边一格。原因:坐标转换公式里没有考虑棋子的半径偏移,或者 Margin 值和绘制时的 Margin 不一致。解决:确保绘制和点击处理用的是同一组常量,并且在坐标转换时加上 CellSize/2 的偏移量。如果还是偏,可以在点击位置画一个临时红点调试,看看实际映射到了哪个格子。
5.3 数据库连接字符串写错导致运行时崩溃
现象:程序编译通过,一运行到数据库操作就抛异常,提示「无法打开登录所请求的数据库」。原因:连接字符串里的数据库名和实际建库名不一致,或者 LocalDB 实例名写错。解决:先在 Visual Studio 的「SQL Server 对象资源管理器」里确认数据库确实存在,然后复制属性窗口里的连接字符串。如果用 SQLite,检查 .db 文件是否被复制到了输出目录,必要时在文件属性里设置「复制到输出目录」。
5.4 胜负判断在边界位置漏判
现象:在棋盘边缘形成五连,程序却没有判胜。原因:CheckWin 里的边界检查写成了r < 0 || r > BoardSize,应该是r >= BoardSize。这种差一错误在数组操作里非常常见。解决:把边界检查单独写成一个函数IsValid(int r, int c),所有地方统一调用,避免重复写错。
5.5 多局游戏后棋盘状态未重置
现象:第一局结束后点「新游戏」,棋盘上还残留着上一局的棋子。原因:重置逻辑只清了 moveHistory 列表,没有把 board 数组清零,也没有重置 isBlackTurn。解决:写一个 ResetGame() 方法,把 board 数组用 Array.Clear 清零,moveHistory.Clear(),isBlackTurn 设回 true,然后 Invalidate()。每次新开局前调用一次。
6. 进阶技巧:用存储过程做排行榜和胜率统计
基础功能跑通之后,如果想在答辩时多拿几分,可以加一个排行榜功能。与其在 C# 里查所有记录再算,不如直接用 SQL 的聚合查询,甚至写成存储过程。
CREATE PROCEDURE GetLeaderboard AS BEGIN SELECT TOP 10 Username, TotalGames, Wins, CASE WHEN TotalGames = 0 THEN 0 ELSE CAST(Wins AS FLOAT) / TotalGames * 100 END AS WinRate FROM Users WHERE TotalGames > 0 ORDER BY WinRate DESC, Wins DESC; ENDC# 端调用这个存储过程,把 CommandType 设为 StoredProcedure,然后用 SqlDataReader 读结果绑定到 DataGridView 上。这样排行榜的排序和计算都在数据库端完成,C# 代码只需要负责展示。
另一个实用技巧是给 GameRecords 表的 UserId 和 PlayedAt 字段加索引。棋谱表随着对局增多会越来越大,没有索引的话查询「某用户最近十局」会越来越慢。加索引的语句是CREATE INDEX IX_GameRecords_UserId ON GameRecords(UserId);,这个操作在数据量超过几千条时效果明显。
我自己做这类项目时最大的教训是:不要等到所有界面都画好了才去接数据库。先把 DatabaseHelper 和建表脚本写好,用控制台程序测试通过增删改查,再回到 WinForms 里调用。这样数据库层的问题不会和界面层的问题混在一起,排查起来省一半时间。希望帮到你。
本文还有配套的精品资源,点击获取