C#五子棋源码+数据库:从课程作业到工程化实战

发布时间:2026/10/13 8:05:07

C#五子棋源码+数据库:从课程作业到工程化实战 简介这份C#五子棋大学项目源码配套数据库面向计算机专业学生与C#初学者用于课程设计、毕业项目参考或自学练手。项目完整实现人机对战与局域网联机两种模式涵盖面向对象编程、Minimax/Alpha-Beta剪枝AI、TCP/IP网络通信、SQLite数据库存储玩家信息与对战记录并加入BGM播放、悔棋、Windows Forms界面交互等实用功能是理解C#综合开发的典型范例。压缩包共200个文件约53.01MB以53个cs源码文件为核心辅以47个png界面素材、33个resx与30个resources资源文件、9个dll依赖库、4个mp3背景音乐以及sln解决方案、mdf/ldf数据库、config配置等结构完整可直接编译运行。目前已有615人学习下载适合需要完整项目案例、排错思路与模块化代码参考的读者。1. 从一份 C# 五子棋源码说起大学项目里藏着多少工程细节很多同学做 C# 五子棋第一反应是“画个棋盘、判个五连”就交差结果答辩时被问“你的落子记录存哪了”“悔棋怎么恢复状态”“两个人换台机器还能不能接着下”就卡住了。这个标题里的“源码数据库”其实点出了一个关键分水岭纯内存版五子棋只能算课程作业带数据库持久化的版本才摸到了桌面应用工程化的门槛。它解决的核心问题是棋局状态的可追溯与可恢复——每一步落子、每一局胜负、每一个玩家都不再随窗口关闭而蒸发。适合谁正在做 C# 课程设计、想拿一个完整项目练 WinForms 或 WPF 数据绑定、或者想搞明白“桌面端怎么和本地数据库打交道”的在校生和初级开发者。下面我按实际动手顺序把选型、建库、落子逻辑、存盘和排错拆开讲。2. 技术选型与数据库设计为什么用 SQLite 而不是 SQL Server2.1 桌面五子棋的存储方案对比大学项目最常见的翻车点是选了 SQL Server Express 然后发现同学电脑上没装实例演示时直接连不上库。我一般会推荐 SQLite原因很直接单文件、零配置、随程序目录走拷贝整个文件夹就能换机器跑。下面这张表是我带过几个课程设计后总结的选型依据。方案部署成本适合场景主要坑点SQLite极低一个 .db 文件单机桌面、课程设计、小型工具并发写入弱不适合多客户端同时写SQL Server Express中需安装实例需要多机访问、教学演示数据库服务换机器要重装连接字符串易错LiteDB极低NoSQL 单文件不想写 SQL 的小项目生态资料少答辩时老师可能不认纯 XML/JSON极低只存配置或简单记录查询和统计要自己写数据量大后难维护选 SQLite 的另一个理由是 C# 生态里Microsoft.Data.Sqlite或System.Data.SQLite都很成熟NuGet 一装就能用不需要额外装服务。对于“大学项目”这个定位把精力花在棋局逻辑和界面上比折腾数据库服务划算得多。2.2 三张表把棋局、落子、玩家管清楚数据库设计不要一上来就想着“一张表存所有”那样后面查历史战绩会非常痛苦。我习惯拆成三张表玩家表、对局表、落子表。玩家表存昵称和创建时间对局表存谁和谁下、谁赢了、什么时候开始的落子表存每一步的坐标、顺序和属于哪一局。这样查“某玩家胜率”只需要 join 对局表查“某局棋谱”只需要按对局 ID 拉落子表。-- 玩家表只存基础身份信息昵称唯一 CREATE TABLE Players ( PlayerId INTEGER PRIMARY KEY AUTOINCREMENT, NickName TEXT NOT NULL UNIQUE, CreatedAt TEXT NOT NULL DEFAULT (datetime(now,localtime)) ); -- 对局表记录一局棋的元信息WinnerId 为空表示和棋或未结束 CREATE TABLE Matches ( MatchId INTEGER PRIMARY KEY AUTOINCREMENT, BlackId INTEGER NOT NULL, WhiteId INTEGER NOT NULL, WinnerId INTEGER, StartedAt TEXT NOT NULL DEFAULT (datetime(now,localtime)), EndedAt TEXT, FOREIGN KEY (BlackId) REFERENCES Players(PlayerId), FOREIGN KEY (WhiteId) REFERENCES Players(PlayerId) ); -- 落子表StepNo 从 1 开始保证棋谱可回放 CREATE TABLE Moves ( MoveId INTEGER PRIMARY KEY AUTOINCREMENT, MatchId INTEGER NOT NULL, StepNo INTEGER NOT NULL, PosX INTEGER NOT NULL, PosY INTEGER NOT NULL, PlayerId INTEGER NOT NULL, FOREIGN KEY (MatchId) REFERENCES Matches(MatchId) );逻辑说明Players的NickName加唯一约束避免同一个昵称重复注册导致战绩分散。Matches里WinnerId允许为空是为了支持“未结束”和“和棋”两种状态后面统计胜率时用WHERE WinnerId IS NOT NULL过滤。Moves的StepNo是回放棋谱的关键悔棋时删掉最大 StepNo 那一条即可不需要重排。参数上坐标PosX、PosY用整数存 0 到 1415×15 棋盘比存字符串省空间也方便校验。注意SQLite 没有原生布尔类型别用BIT或BOOLEAN去存“是否先手”用整数 0/1 或者直接靠BlackId/WhiteId区分更稳。2.3 在 C# 里建库并插入第一局对弈建库不要手动用工具建完再拷来拷去直接在程序启动时检查文件是否存在不存在就执行建表脚本。这样换机器、删库重来都只需要跑一次程序。using Microsoft.Data.Sqlite; public static class DbInitializer { private const string DbPath gomoku.db; private static string ConnStr $Data Source{DbPath}; public static void EnsureCreated() { bool isNew !File.Exists(DbPath); using var conn new SqliteConnection(ConnStr); conn.Open(); // 无论新旧都执行 IF NOT EXISTS避免重复建表报错 string sql CREATE TABLE IF NOT EXISTS Players (...); CREATE TABLE IF NOT EXISTS Matches (...); CREATE TABLE IF NOT EXISTS Moves (...);; using var cmd new SqliteCommand(sql, conn); cmd.ExecuteNonQuery(); if (isNew) { // 首次运行插入两个演示玩家方便直接开局 using var seed new SqliteCommand( INSERT INTO Players(NickName) VALUES(玩家A),(玩家B);, conn); seed.ExecuteNonQuery(); } } }逻辑说明EnsureCreated先判断文件是否存在只用于决定要不要插演示数据建表语句本身带IF NOT EXISTS所以重复调用安全。连接字符串用Data Sourcegomoku.db表示数据库文件就在程序运行目录打包时把这个文件一起带走即可。参数上如果你想让数据库放到AppData下避免权限问题把DbPath换成Path.Combine(Environment.GetFolderPath(...), gomoku.db)就行但课程设计演示通常放当前目录更直观。3. 落子、判胜与悔棋核心逻辑怎么写才不翻车3.1 15×15 棋盘的落子与五连判定棋盘用二维数组int[15,15]0 空、1 黑、2 白。落子前先校验坐标范围和该点是否为空然后写入数组并刷新界面。判胜不要每次全盘扫描只从当前落子点向四个方向延伸计数效率高也好写。private int[,] board new int[15, 15]; // 返回 true 表示落子成功 public bool PlaceStone(int x, int y, int player) { if (x 0 || x 15 || y 0 || y 15) return false; if (board[x, y] ! 0) return false; // 已有子拒绝 board[x, y] player; return true; } // 从 (x,y) 出发判断是否五连 public bool CheckWin(int x, int y, int player) { // 四个方向横、竖、主对角、副对角 int[][] dirs { new[]{1,0}, new[]{0,1}, new[]{1,1}, new[]{1,-1} }; foreach (var d in dirs) { int count 1; // 包含当前这颗 // 正方向延伸 for (int i 1; i 5; i) { int nx x d[0] * i, ny y d[1] * i; if (nx 0 || nx 15 || ny 0 || ny 15) break; if (board[nx, ny] ! player) break; count; } // 反方向延伸 for (int i 1; i 5; i) { int nx x - d[0] * i, ny y - d[1] * i; if (nx 0 || nx 15 || ny 0 || ny 15) break; if (board[nx, ny] ! player) break; count; } if (count 5) return true; } return false; }逻辑说明PlaceStone只负责校验和写数组不碰界面这样单元测试好写。CheckWin的方向数组里{1,-1}是副对角线注意 y 减小时不要越界循环里已经做了边界判断。参数上count 5而不是 5是因为有些规则允许长连也算赢如果你的课程要求“恰好五连”才赢改成 5并在超过时判负即可这个规则差异答辩时经常被问到。3.2 每一步落子同步写入数据库落子和存库要放在同一个操作里先写库再刷界面或者先刷界面再写库都行但必须保证 StepNo 连续。我一般用SELECT IFNULL(MAX(StepNo),0)1拿下一步序号避免自己维护计数器出错。public void SaveMove(int matchId, int x, int y, int playerId) { using var conn new SqliteConnection(Data Sourcegomoku.db); conn.Open(); using var tx conn.BeginTransaction(); // 事务保证序号和插入一致 int nextStep; using (var q new SqliteCommand( SELECT IFNULL(MAX(StepNo),0)1 FROM Moves WHERE MatchIdm, conn, tx)) { q.Parameters.AddWithValue(m, matchId); nextStep Convert.ToInt32(q.ExecuteScalar()); } using (var ins new SqliteCommand( INSERT INTO Moves(MatchId,StepNo,PosX,PosY,PlayerId) VALUES(m,s,x,y,p), conn, tx)) { ins.Parameters.AddWithValue(m, matchId); ins.Parameters.AddWithValue(s, nextStep); ins.Parameters.AddWithValue(x, x); ins.Parameters.AddWithValue(y, y); ins.Parameters.AddWithValue(p, playerId); ins.ExecuteNonQuery(); } tx.Commit(); }逻辑说明用事务把“查最大序号”和“插入”包起来防止极端情况下两条记录拿到同一个 StepNo。参数全部用AddWithValue而不是字符串拼接避免坐标里混入奇怪字符导致 SQL 出错。IFNULL(MAX(StepNo),0)1在空表时返回 1符合从 1 开始的设计。3.3 悔棋删最后一步并恢复界面状态悔棋不是简单把数组置 0还要删数据库里对应的落子记录并且如果刚才是胜局得把对局表的 WinnerId 清掉。顺序建议先删库再改内存这样万一删库失败界面不会出现“看起来悔了但库里还在”的假象。public bool UndoLastMove(int matchId) { using var conn new SqliteConnection(Data Sourcegomoku.db); conn.Open(); using var tx conn.BeginTransaction(); // 查出最后一步 int lastX, lastY, lastPlayer; using (var q new SqliteCommand( SELECT PosX,PosY,PlayerId FROM Moves WHERE MatchIdm ORDER BY StepNo DESC LIMIT 1, conn, tx)) { q.Parameters.AddWithValue(m, matchId); using var r q.ExecuteReader(); if (!r.Read()) return false; // 没有可悔的棋 lastX r.GetInt32(0); lastY r.GetInt32(1); lastPlayer r.GetInt32(2); } using (var del new SqliteCommand( DELETE FROM Moves WHERE MatchIdm AND StepNo (SELECT MAX(StepNo) FROM Moves WHERE MatchIdm), conn, tx)) { del.Parameters.AddWithValue(m, matchId); del.ExecuteNonQuery(); } // 如果这局已判胜悔棋后要撤销胜负标记 using (var clear new SqliteCommand( UPDATE Matches SET WinnerIdNULL, EndedAtNULL WHERE MatchIdm, conn, tx)) { clear.Parameters.AddWithValue(m, matchId); clear.ExecuteNonQuery(); } tx.Commit(); board[lastX, lastY] 0; // 内存棋盘同步清空 return true; }逻辑说明先查最后一步拿到坐标再删记录最后清胜负标记三步在一个事务里。ORDER BY StepNo DESC LIMIT 1保证拿到的是最新一步。参数上matchId必须传对否则会误删别的对局记录这是血泪经验——早期版本没加WHERE MatchId测试时把演示数据全删了。4. 避坑与排查五子棋数据库最容易翻车的 5 个点4.1 现象程序换台电脑就报“无法打开数据库文件”原因通常是连接字符串写了绝对路径或者数据库文件被设成“如果较新则复制”导致每次运行覆盖。解决连接字符串用相对路径Data Sourcegomoku.db并在项目里把 .db 文件的“复制到输出目录”设为“不复制”由程序自己创建。如果必须带初始数据用“如果较新则复制”但确保建表脚本幂等。4.2 现象落子后界面有子但重启程序棋谱丢了原因多半是落子只改了内存数组没调SaveMove或者SaveMove里事务没提交。排查时先在SaveMove末尾打日志确认tx.Commit()执行到再查数据库文件是否在预期目录。另一个常见原因是用了using var conn但没Open()SQLite 在部分版本下不会自动打开。4.3 现象悔棋后还能继续下但胜局记录没清掉这是只删了Moves没更新Matches的典型症状。解决就是上面UndoLastMove里那段UPDATE Matches SET WinnerIdNULL。注意如果你们规则要求“悔棋后本局作废”那就改成把Matches整条标记为无效而不是清 WinnerId这个要按课程要求定。4.4 现象判胜偶尔漏判尤其是斜向五连原因通常是方向数组写错比如副对角线用了{1,1}和{-1,-1}但没覆盖{1,-1}或者边界判断只判了正方向没判反方向。排查时写一个单元测试手动摆一个斜向五连调CheckWin看返回值。参数上四个方向必须成对出现且每个方向正反都要延伸。4.5 现象多局同时进行时 StepNo 重复SQLite 并发写入弱如果开了多个窗口同时写同一个库MAX(StepNo)1可能拿到相同值。解决课程设计层面限制同一时间只有一局在进行如果确实要多局给Moves表加UNIQUE(MatchId, StepNo)约束插入冲突时重试。这个约束加上后即使逻辑有并发问题数据库也会帮你挡住脏数据。5. 把棋谱导出成可回放记录一个值得加的进阶技巧带数据库的五子棋最容易被忽略的进阶用法是“棋谱回放”。你已经有Moves表按StepNo排好序只要按顺序读出来就能在界面上一步步重演整局。这个功能在答辩时非常加分因为它证明你的数据不是死存储而是可复用的。具体做法查Moves时按StepNo ASC排序用一个Timer或者按钮逐步把坐标画到棋盘上。下面这段是导出成文本棋谱的代码方便你贴到报告里或者做复盘。public string ExportRecord(int matchId) { var sb new System.Text.StringBuilder(); using var conn new SqliteConnection(Data Sourcegomoku.db); conn.Open(); using var cmd new SqliteCommand( SELECT StepNo,PosX,PosY,PlayerId FROM Moves WHERE MatchIdm ORDER BY StepNo ASC, conn); cmd.Parameters.AddWithValue(m, matchId); using var r cmd.ExecuteReader(); while (r.Read()) { // 输出格式步数. 玩家(坐标) 例如 1. 黑(7,7) string side r.GetInt32(3) 1 ? 黑 : 白; sb.AppendLine(${r.GetInt32(0)}. {side}({r.GetInt32(1)},{r.GetInt32(2)})); } return sb.ToString(); }逻辑说明ORDER BY StepNo ASC是回放正确性的关键少了这个排序棋谱顺序就乱了。PlayerId转成“黑/白”只是为了可读实际回放时还是用 ID 去查玩家昵称更准。参数上如果你想让棋谱带时间戳可以在Moves表加一列PlayedAt默认datetime(now,localtime)导出时一起读出来。回放功能还有一个隐藏好处它逼你把“界面状态”和“数据库状态”彻底分开。界面只负责画数据只从库里读这样悔棋、重开、换机器都不会出现状态不一致。我自己的习惯是任何带持久化的桌面项目都先写一个“从库重建界面”的函数再写“操作写库”的函数两边独立测试。这个习惯帮我省掉了无数次“界面和数据库对不上”的排查时间。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/13 8:00:06

Open Code Review:一种以透明度为基础设施的异步协作范式

1. 项目概述:这不是代码检查,而是一场协作范式的重构“open-code-review”这个词组乍看像一个技术名词,但其实它根本不是某个工具、平台或协议的官方命名——它是一类实践行为的精准描述,是开发者群体在真实协作中自发形成的一套共…

2026/10/13 8:00:06

AgentScope核心解析:Actor模型与多智能体协作框架详解

AgentScope 这个名字,最近在关注 Agent 应用开发的人应该都听过。但说实话,我第一次接触它的时候挺懵的:网上资料东一榔头西一棒子,有人说它是编排框架,有人说它是分布式调度工具,还有人提它跟低代码平台有…

2026/10/13 8:00:06

MIT 6.824 Lab1:用Go实现容错MapReduce调度器

1. 这不是“写个MapReduce”,而是MIT分布式系统课的第一次真实心跳 刚接触6.824这门课的同学,看到“lab1”三个字,容易下意识以为是“照着文档敲几行代码就能跑通的入门练习”。我当年也是这么想的——直到在凌晨三点盯着屏幕里反复崩溃的wor…

2026/10/13 12:41:33

好数对计数问题:从暴力循环到哈希表一次遍历的算法优化

“好数对的数目”这个题目,我在好几个技术社群里都看到有人讨论。它看起来简单,很多人觉得无非就是双层循环暴力数一下,但真正动手写的时候才发现,这道题其实藏着不少和计数、哈希表运用相关的门道。作为一道经典的入门级算法题&a…

2026/10/13 12:41:33

HTML基础避坑实战笔记:从DOCTYPE到XSS注入全解析

1. 这份HTML笔记整理的出发点 HTML大概是所有编程语言里最让人“轻敌”的一个。你随便百度一下&#xff0c;满屏都是“前端入门第一步”“三小时学会网页制作”&#xff0c;好像写几个标签就完事了。但真正做着做着就会发现&#xff0c;光一个 <!doctype html><html …

2026/10/13 12:41:33

金三银四微派内推全攻略:从技术栈拆解到面试实操

金三银四一到&#xff0c;微信里各种“内推”消息就开始刷屏&#xff0c;而今年被问得最多的是“微派”。很多朋友跑来问我&#xff1a;这家公司到底怎么样&#xff1f;内推进去有没有捷径&#xff1f;面试会考什么&#xff1f;这篇文章我就围绕“内推、金三银四、微派”这三个…

2026/10/13 12:36:31

C#直连TensorFlow C API实现工业级模型加载与预测

1. 项目概述&#xff1a;为什么C#工程师需要亲手加载TensorFlow模型做预测最近在某工业质检系统升级中&#xff0c;我接手了一个棘手任务&#xff1a;把实验室里用Python训练好的缺陷识别模型&#xff0c;无缝集成进客户现场已运行五年的C#上位机软件里。客户明确拒绝重写整套U…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”&#xff0c;而是它给出一段看起来合理的说明&#xff0c;程序却从中猜错优先级。通俗做法是&#xff1a;要求模型只交 JSON&#xff08;JavaScript Object Notation&#xff0c;轻量数据格式&#xff09;&#xff0c;再让代码验证它。Gem…

2026/10/12 12:01:26

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里&#xff0c;有A同学跑来问我&#xff1a;选什么毕设题目最稳妥&#xff0c;既能让评审老师觉得工作量够&#xff0c;又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友&#xff0c;在看完一堆“Hello World”和基础组件之后&#xff0c;大概率都会撞上同一堵墙&#xff1a;StatefulWidget 里那堆 initState、build、dispose 方法&#xff0c;到底什么时候被调用&#xff1f;为什么顺序是那样&#xff1f;在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介&#xff1a;这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师&#xff0c;聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案&#xff0c;解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件&#xff0c;约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型&#xff0c;最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支&#xff0c;让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图&#xff0c;十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑