C++数独游戏课程设计:EasyX图形界面与回溯算法实战指南
2026/9/14 3:00:46 网站建设 项目流程

简介:基于C++与EasyX实现的数独游戏课程设计,面向需要完成课程设计、大作业或入门游戏开发的学习者。程序完整覆盖了从用户名录入、关卡选择、导入文档初局、计时计分、九宫格绘制到玩家得分排序的全流程,交互简洁,适合初学者通过源码理解图形界面编程与游戏逻辑设计。资源包压缩后约7.03MB,共47个文件,以C++源码、Visual Studio工程文件(sln/vcxproj)、编译生成的可执行文件(exe)、程序运行截图(png)以及文档说明(txt/md)为主,同时也包含调试记录等过程文件,便于读者直接运行体验或对照学习。目前已有347人学习下载。这份资料可帮助读者快速掌握EasyX图形库的基本用法、数独生成与求解思路、鼠标交互及定时器实现,并可直接作为课程设计报告或项目演示的参考。

1. 数独游戏课程设计为什么首选 EasyX:从控制台到图形界面的关键一跳

很多人在数据结构或 C++ 课程设计里选数独,第一反应是控制台里用二维数组加回溯算法跑出答案,再把 9x9 数字打印出来。这套东西代码量不大,但交上去之后老师往往会问一句:游戏界面在哪?玩家怎么交互?这时候才发现,课程设计的要求不只是“算法正确”,还要有基本的图形界面和操作反馈。用 EasyX 做数独,恰好是把“算法核心”和“界面呈现”一次性补齐的最短路径。

EasyX 是面向 C/C++ 的简单图形库,头文件只有几个,不需要额外安装复杂依赖,在 Visual Studio 里配置一次就能跑。它解决的核心痛点是:C++ 标准库不提供绘图和鼠标键盘事件接口,而 EasyX 把这些能力压缩成几十个函数。对课程设计来说,这意味着你可以把精力集中在数独生成、求解、用户交互上,而不是花一半时间研究 Windows API 或 Qt 的信号槽机制。本文按“规则与数据模型 → 生成算法 → 求解与提示 → 界面交互 → 进阶技巧”的顺序展开,每一步都有可直接抄的代码和参数说明,适合已经掌握 C++ 基础语法、但第一次做图形界面项目的读者。你会发现,数独的算法难度低于想象,真正的分水岭在于如何把回溯、随机化、输入校验这些逻辑组织成可维护的工程结构。

2. 数独规则与核心数据模型:用二维数组还是位运算

2.1 数独的数学约束与棋盘表示

数独是 9x9 网格,分为 9 个 3x3 宫。合法盘面要求每一行、每一列、每一宫内的数字 1~9 不重复。课程设计里最常见的错误是一上来就写界面,结果数据结构设计得乱七八糟,后面算法很难接。我的建议是先用一个 C++ 类把“盘面逻辑”和“界面绘制”彻底分开,比如SudokuBoard负责数据合法性、候选数计算、求解;EasyXGame负责绘图、鼠标输入、游戏状态切换。

最小数据模型可以定义为:

class SudokuBoard { public: int grid[9][9]; // 0 表示空格,1~9 为已填数字 bool isFixed[9][9]; // true 表示题目初始数字,玩家不可修改 bool isConflict[9][9]; // 标记冲突位置,用于界面红框提示 };

grid是整个游戏的核心载体。isFixed的作用是区分“题目数字”和“玩家填入数字”,这在界面绘制时决定该数字显示为黑色还是蓝色。isConflict是可选但很有用的字段,它让界面能实时反馈错误,而不是等到玩家提交整个盘面时才给出“有冲突”的笼统提示。

2.2 行列宫的冲突检测:三种方式与效率对比

检测某个位置填入数字 n 是否合法,有三种常见做法。

最直接的是遍历行列宫:

bool isValid(int row, int col, int n) { for (int i = 0; i < 9; i++) { if (grid[row][i] == n || grid[i][col] == n) return false; } int sr = (row / 3) * 3, sc = (col / 3) * 3; for (int i = sr; i < sr + 3; i++) { for (int j = sc; j < sc + 3; j++) { if (grid[i][j] == n) return false; } } return true; }

这段代码逻辑清晰,但每次校验都要循环 9 + 9 + 9 次。对玩家手动填一个数字来说,这个开销可以忽略;但在生成完整终盘时,回溯算法会调用isValid数十万次,此时isValid会成为性能瓶颈。

更高效的方案是位运算。用 9 个 int 记录每行已用数字,9 个 int 记录每列,9 个 int 记录每宫,每个 int 的第 k 位表示数字 k+1 是否已出现:

class SudokuBoard { private: int rowMask[9], colMask[9], boxMask[9]; public: bool isValidFast(int row, int col, int n) { int bit = 1 << (n - 1); if (rowMask[row] & bit) return false; if (colMask[col] & bit) return false; int box = (row / 3) * 3 + (col / 3); if (boxMask[box] & bit) return false; return true; } void setNumber(int row, int col, int n) { int bit = 1 << (n - 1); grid[row][col] = n; rowMask[row] |= bit; colMask[col] |= bit; boxMask[(row / 3) * 3 + (col / 3)] |= bit; } void clearNumber(int row, int col) { int n = grid[row][col]; if (n == 0) return; int bit = 1 << (n - 1); grid[row][col] = 0; rowMask[row] &= ~bit; colMask[col] &= ~bit; boxMask[(row / 3) * 3 + (col / 3)] &= ~bit; } };

setNumberclearNumber是伴随数独状态更新的同步操作,必须在每次填入或擦除时调用,保证掩码和grid始终一致。这里有一个很容易忽略的坑:手动在grid上赋值却忘记更新掩码,会导致后续isValidFast误判。建议把grid设为私有成员,只暴露setNumbergetNumber接口,从工程上杜绝这种不一致。

2.3 选型的权衡建议

如果你追求课程设计代码可读性高、答辩时能解释清楚,用isValid遍历法就够了。但如果你希望生成终盘速度更快,或者想展示一点算法功底,位运算是加分项。一个折中方案是:生成终盘用isValidFast,玩家手动输入时用普通isValid,后者虽然慢一些,但代码更直观,也方便调试。实际上isValid在单次调用上的耗时是微秒级的,只有放在回溯循环里才有差异。以上两种方案我都用过,课程设计阶段我倾向位运算,因为代码量没有增加多少,但性能提升了一个数量级。

3. 唯一解数独生成:从终盘回溯到挖洞算法

3.1 生成完整终盘的两种路线

数独生成有两阶段:先生成一个合法的 9x9 终盘,再根据难度挖去若干数字。常见误区是直接从空盘开始随机填数,这很容易陷入死胡同——某个空格没有任何可选数字。反过来,先构造终盘再挖洞,路径要可靠得多。

构造终盘的经典做法是回溯 + 随机洗牌:

bool fillBoard(int pos) { if (pos == 81) return true; int row = pos / 9, col = pos % 9; int nums[9] = {1,2,3,4,5,6,7,8,9}; // 随机打乱,保证每次生成的终盘不同 for (int i = 8; i > 0; i--) { int j = rand() % (i + 1); std::swap(nums[i], nums[j]); } for (int i = 0; i < 9; i++) { int n = nums[i]; if (isValidFast(row, col, n)) { setNumber(row, col, n); if (fillBoard(pos + 1)) return true; clearNumber(row, col); } } return false; }

pos从 0 到 80 表示 81 个格子,每层递归尝试随机顺序的数字。rand()必须在程序入口用srand(time(0))设置种子,否则每次运行生成的是同一套终盘。生成单个终盘的速度极快,通常在毫秒级,因为它几乎不会回溯太深。

3.2 挖洞算法与难度控制

有了终盘,挖洞就不是随机删几个数字那么简单。随机删可能会留下多解盘面,而数独游戏的基本要求是“唯一解”。常用做法是逐格尝试删除,每删一格就用求解器验证唯一性,如果发现多解就撤销删除。

struct Cell { int row, col; }; vector<Cell> generatePuzzle(int difficulty) { // 先生成终盘 fillBoard(0); // 收集所有格子并随机打乱 vector<Cell> cells; for (int i = 0; i < 9; i++) for (int j = 0; j < 9; j++) cells.push_back({i, j}); for (int i = cells.size() - 1; i > 0; i--) { int j = rand() % (i + 1); std::swap(cells[i], cells[j]); } int target = difficulty; // 要挖掉的格子数 int removed = 0; for (Cell c : cells) { if (removed >= target) break; int backup = grid[c.row][c.col]; clearNumber(c.row, c.col); if (countSolutions(grid, 2) == 1) { removed++; } else { setNumber(c.row, c.col, backup); // 恢复,避免多解 } } // 将当前盘面中非空数字设为 isFixed for (int i = 0; i < 9; i++) for (int j = 0; j < 9; j++) isFixed[i][j] = (grid[i][j] != 0); return cells; }

countSolutions(grid, limit)是带上限的求解器,找到第 2 个解就提前返回,避免数完所有解。limit参数在这里传 2,是为了判断“删掉这个格子后是否仍然唯一”。这种做法保证最终盘面有唯一解,但挖洞数量可能达不到目标值。

3.3 难度分级:挖洞数量不是唯一指标

难度不只看空格数量,还看推理链长度。比如简单题挖 35 个洞可能仍能直观填出,难题挖 40 个洞就需要候选数删减、区块摒除等高级技巧。课程设计阶段可以按挖洞数粗分三档:

难度挖洞数范围特点
简单30~36每行每宫保留较多提示数,单步可推
中等37~45需要候选数排除,部分格子要先填中间值
困难46~55依赖多步骤推理,候选数较多

注意挖洞数越多不一定越难,因为空格分布位置同样重要。一个只在右下角集中挖洞的盘面可能比均匀挖洞但洞数少的盘面简单。所以更专业的实现是用求解器记录玩家解题过程中的“推理步数”,但课程设计用挖洞数加随机位置已经足够。

3.4 生成阶段的验收方法

生成的题目需要快速验证三件事:终盘合法、题目唯一解、难度符合预期。最简单的自检是在生成后重新求解,如果解出的盘面和你挖洞前的网格逐格一致,说明挖洞逻辑没破坏唯一性。这里要注意:删除数字时必须用clearNumber同步清除行列宫掩码,如果直接写grid[row][col] = 0,后续的求解和验证都会用到旧掩码,产生难以排查的诡异 bug。

4. 求解器与玩家智能提示:用递归回溯编写冻结检测

4.1 求解器的核心框架

数独求解和生成终盘在代码上没有本质区别,只是一个从空盘开始,一个从部分填充的盘面开始。

int countSolutions(SudokuBoard& board, int limit) { int cnt = 0; std::function<void(int)> dfs = [&](int pos) { if (cnt >= limit) return; if (pos == 81) { cnt++; return; } int row = pos / 9, col = pos % 9; if (board.grid[row][col] != 0) { dfs(pos + 1); return; } for (int n = 1; n <= 9; n++) { if (board.isValidFast(row, col, n)) { board.setNumber(row, col, n); dfs(pos + 1); board.clearNumber(row, col); } } }; dfs(0); return cnt; }

limit是一个关键设计:当我们需要唯一性验证时传 2,尽量少做无用功;当我们需要完整解时传 1,返回后棋盘上保存的就是最终解。注意dfs用 lambda 表达式递归捕获自身,C++ 里需要std::function,直接auto dfs = [&](int pos){ dfs(pos+1); }会编译报错,因为 lambda 的捕获列表在声明时尚未包含自身。这是一个很容易被忽视的 C++ 语法细节,面试里也常考。

4.2 显示候选数与错误提示功能

在界面中显示候选小数字是一个很实用的功能,玩家点击一个空格时,EasyX 可以在该格子的 3x3 子区域里绘制 1~9 的小号数字。计算候选数的逻辑可以复用isValidFast

vector<int> getCandidates(int row, int col) { vector<int> res; if (grid[row][col] != 0) return res; for (int n = 1; n <= 9; n++) { if (isValidFast(row, col, n)) res.push_back(n); } return res; }

这个函数在界面层被调用,每帧或只在玩家点击时刷新。要注意的是,候选数标记不应保存在SudokuBoard中,因为它是派生数据,而不是盘面状态的一部分。混淆这两者会让代码变得混乱。

4.3 提示功能的三种实现层次

数独游戏常用的“提示”功能有三个层次,课程设计实现前两层即可。

层次一是给出当前格子的一个合法数字。这个最简单,直接从getCandidates里随机挑一个。层次二是给出当前整个盘面的某个空格的“唯一确定值”,即候选数只有一个的格子。这需要扫描所有空格。层次三是用求解器解出完整答案,把某个空格的值填上。这个可以借用solveOnce函数,但需要注意:求解出的完整答案必须与当前玩家已填数字一致,否则说明盘面冲突或题目本身有错。

bool solveOnce(SudokuBoard& board) { return board.fillBoard(0); // 复用生成终盘的 fillBoard }

fillBoard会从第一个空格开始回溯,遇到非空格直接跳过。调用前如果盘面已经有冲突,isValidFast会检测到并返回 false,最终回溯失败。这个函数也可以用于玩家按下“查看答案”按键时,直接绘制完整网格。

4.4 干预难度与死局判定的设计决策

玩家填错数字后,求解器仍然能找到解,这可能让“唯一解”验证与“玩家实时输入”产生矛盾。设计决策是:当玩家填入数字时,只做行、列、宫冲突检测,不立即判定最终是否有解。更合理的做法是把“有解性检测”放到“检查答案”按钮触发时执行,这样玩家的每一步不会被粗暴打断,但也存在游戏后期才发现无解的体验问题。折中方案是维护一个“冲突标记”,只在冲突时用红色标注,不阻断输入,具体见第 6 章。

5. 基于 EasyX 的交互实现:鼠标点击、数字选择与绘制刷新

5.1 EasyX 图形窗口的初始化与配置

在 Visual Studio 2022 中使用 EasyX 需要先安装 EasyX 库,并在项目属性里把“字符集”设为“使用多字节字符集”,否则initgraphouttextxy中传入中文字符串时会报编码错误。

#include <graphics.h> #include <conio.h> #include <time.h> const int CELL_SIZE = 60; // 每格像素 const int BOARD_X = 40; // 棋盘左上角 x const int BOARD_Y = 40; // 棋盘左上角 y const int BOARD_SIZE = 9 * CELL_SIZE; // 540 void initGameWindow() { initgraph(BOARD_X * 2 + BOARD_SIZE, BOARD_Y * 2 + BOARD_SIZE + 100); setbkcolor(WHITE); cleardevice(); }

CELL_SIZE决定了棋盘整体大小。60 像素时格子间距清晰,鼠标点击命中率也高。如果屏幕分辨率较低,可以改为 50 或 48。BOARD_XBOARD_Y让棋盘不是紧贴窗口边缘,视觉上更舒适。cleardevice用背景色清除窗口,不清除的话前一次绘制会残留。

5.2 绘制静态棋盘与数字

绘制棋盘是逐线循环,但要注意粗线和细线的区分:

void drawBoard() { setlinecolor(RGB(180, 180, 180)); for (int i = 0; i <= 9; i++) { int x = BOARD_X + i * CELL_SIZE; int y = BOARD_Y + i * CELL_SIZE; if (i % 3 == 0) { setlinecolor(RGB(60, 60, 60)); setlinestyle(PS_SOLID, 3); // 粗线,分隔 3x3 宫 } else { setlinecolor(RGB(180, 180, 180)); setlinestyle(PS_SOLID, 1); } line(x, BOARD_Y, x, BOARD_Y + BOARD_SIZE); line(BOARD_X, y, BOARD_X + BOARD_SIZE, y); } setlinestyle(PS_SOLID, 1); }

setlinestyle(PS_SOLID, 3)的第二个参数是线宽,单位是像素。每次循环前都要重新设置,因为之前可能被改成其他样式。绘制数字时要用settextstyle设置字体大小,然后按中心对齐:

void drawNumbers(SudokuBoard& board) { for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { int n = board.grid[i][j]; if (n == 0) continue; int x = BOARD_X + j * CELL_SIZE + CELL_SIZE / 2; int y = BOARD_Y + i * CELL_SIZE + CELL_SIZE / 2; if (board.isFixed[i][j]) { settextcolor(RGB(30, 30, 30)); // 题目数字黑色 } else { settextcolor(RGB(70, 120, 220)); // 玩家数字蓝色 } if (board.isConflict[i][j]) { settextcolor(RGB(220, 50, 50)); // 冲突数字红色 } settextstyle(32, 0, _T("Consolas")); TCHAR buf[2] = {0}; _stprintf_s(buf, _T("%d"), n); outtextxy(x - 12, y - 20, buf); // 粗略居中 } } }

outtextxy不像DrawText那样支持对齐参数,需要手动把文本中心对准格子中心,x - 12y - 20是基于 32 号字体宽高估计的经验值。不同机器字号渲染略有差异,建议在初始设置后先跑一遍看效果。

5.3 鼠标事件与游戏状态机

EasyX 的鼠标输入通过MOUSEMSG获取,常用的消息类型是WM_LBUTTONDOWN。要做的是把像素坐标换算成格子坐标:

MOUSEMSG m = GetMouseMsg(); if (m.uMsg == WM_LBUTTONDOWN) { int col = (m.x - BOARD_X) / CELL_SIZE; int row = (m.y - BOARD_Y) / CELL_SIZE; if (row >= 0 && row < 9 && col >= 0 && col < 9) { selectedRow = row; selectedCol = col; } }

(m.x - BOARD_X) / CELL_SIZE利用整数除法向下取整得到格子列号。必须判断colrow是否在 0~8 之间,否则点击窗口边界外的坐标会产生越界错误。

数字输入有两种常见方案。一种是用键盘数字键 1~9 输入,另一种是点击右侧的数字选择按钮。键盘方案更快速,但课程设计演示时鼠标点击更直观。比较完善的方案是在窗口右侧绘制 9 个小按钮区域,然后用相同的坐标换算判断点击了哪个数字:

const int NUM_BTN_X = BOARD_X + BOARD_SIZE + 30; const int NUM_BTN_Y = BOARD_Y; void drawNumberButtons(int selectedNum) { for (int n = 1; n <= 9; n++) { int x = NUM_BTN_X; int y = NUM_BTN_Y + (n - 1) * (CELL_SIZE + 10); if (n == selectedNum) { setfillcolor(RGB(220, 230, 255)); } else { setfillcolor(RGB(245, 245, 245)); } fillrectangle(x, y, x + CELL_SIZE, y + CELL_SIZE); settextcolor(RGB(50, 50, 50)); settextstyle(30, 0, _T("Consolas")); TCHAR buf[2] = {0}; _stprintf_s(buf, _T("%d"), n); outtextxy(x + CELL_SIZE / 2 - 10, y + 15, buf); } }

这里selectedNum由鼠标点击判断,如果点击位置在数字按钮区域内就更新它。随后中央棋盘响应点击时,把选中格子的值设为selectedNum,并调用setNumber更新掩码。

5.4 主循环与刷新策略

EasyX 程序的主循环需要处理事件、更新逻辑、重绘画面。最粗糙的写法是每帧全量重绘,但这样会闪烁。EasyX 提供BeginBatchDrawEndBatchDraw双缓冲机制,在循环开始和结束时调用:

BeginBatchDraw(); while (!isGameOver) { // 处理鼠标和键盘事件 while (MouseHit()) { MOUSEMSG m = GetMouseMsg(); handleMouse(m); } // 清屏并重绘 cleardevice(); drawBoard(); drawNumbers(board); drawSelectedCell(selectedRow, selectedCol); drawNumberButtons(selectedNum); drawGameStatus(board); FlushBatchDraw(); Sleep(16); // 约 60 FPS } EndBatchDraw();

Sleep(16)避免了 CPU 空转,也保证动画平滑。MouseHit()是 EasyX 的非阻塞鼠标事件查询,返回 true 表示有待处理事件;GetMouseMsg()在有事件时才会返回。这两个函数配合,可以让主循环不因等待鼠标输入而卡死。

5.5 字符集与中文显示的两个坑

EasyX 在 VS 中显示中文会乱码,最常见的原因是项目使用了 Unicode 字符集,而代码里传的是窄字符串。解决方案是把项目属性里的“字符集”改为“使用多字节字符集”,或者统一用_T()宏包裹字符串。另一个坑是outtextxy只能输出TCHAR字符串,不能直接用std::stringstd::to_string的结果,必须转换为TCHAR数组。推荐做法是:

std::string s = std::to_string(n); TCHAR buf[16] = {0}; #ifdef _UNICODE MultiByteToWideChar(CP_ACP, 0, s.c_str(), -1, buf, 16); #else strcpy_s(buf, s.c_str()); #endif

这段代码同时兼容多字节和 Unicode 项目配置,但也可以偷懒直接把项目设为多字节字符集。课程设计答辩现场如果出现乱码,第一检查项永远是字符集配置,其次才是代码里的编码问题。

6. 进阶技巧:撤销重做、计时统计与冲突实时反馈的工程化实现

6.1 基于栈的撤销重做机制

撤销功能听起来简单,但实现得不好会导致内存爆炸或状态错乱。不要每次操作都保存整个 9x9 棋盘,那 81 个 int 的内存占用不大,但频繁拷贝会让代码显得笨拙。常见的做法是只记录“操作差量”:格子坐标和操作前后的数值。

struct MoveRecord { int row, col; int beforeVal; int afterVal; }; class UndoManager { private: std::stack<MoveRecord> undoStack; std::stack<MoveRecord> redoStack; public: void pushMove(int row, int col, int before, int after) { undoStack.push({row, col, before, after}); std::stack<MoveRecord>().swap(redoStack); // 清空重做栈 } bool undo(SudokuBoard& board) { if (undoStack.empty()) return false; MoveRecord m = undoStack.top(); undoStack.pop(); board.setNumber(m.row, m.col, m.beforeVal); redoStack.push(m); return true; } bool redo(SudokuBoard& board) { if (redoStack.empty()) return false; MoveRecord m = redoStack.top(); redoStack.pop(); board.setNumber(m.row, m.col, m.afterVal); undoStack.push(m); return true; } };

setNumber在这里需要处理 0 的情况,所以它的定义应该是:n == 0时调用clearNumber,否则设置数字。撤销和重做栈的高度在课程设计阶段不会超过几百,不需要担心内存。这个类放在SudokuBoard外部,作为游戏层的一个组件,比塞进SudokuBoard更符合单一职责原则。

6.2 计时与错误次数统计

计时功能有两种实现:实时累计帧数和读取系统时钟。帧数法容易受Sleep(16)影响产生误差,系统时钟法更可靠。用<chrono>实现:

auto startTime = std::chrono::steady_clock::now(); std::string formatElapsed() { auto now = std::chrono::steady_clock::now(); auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count(); int h = seconds / 3600; int m = (seconds % 3600) / 60; int s = seconds % 60; char buf[16]; snprintf(buf, sizeof(buf), "%02d:%02d:%02d", h, m, s); return std::string(buf); }

steady_clock是单调时钟,不受系统修改时间影响,比system_clock更适合做计时器。错误次数的统计可以在handleMouse中,在填入数字后调用一次checkConflictAt(row, col),如果发现该位置与已有数字冲突则mistakeCount++。这里有个细节:checkConflictAt只做即时冲突检查,不做全盘合法性校验,因为全盘校验代价高且没必要每步都做。

6.3 冲突实时反馈的完整实现

冲突高亮比简单判断“该格是否与同行同列同宫重复”更复杂,因为还要考虑玩家输入顺序。例如玩家在 A 格填 5,B 格填 3,C 格又填 5,则 A 和 C 都变成冲突状态,而 B 不受影响。所以每次落子后,需要重新扫描该格所在行、列、宫的所有已填数字:

void updateConflict(SudokuBoard& board, int row, int col) { for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { board.isConflict[i][j] = false; } } for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { if (board.grid[i][j] == 0) continue; int n = board.grid[i][j]; // 检查是否与行内其他格子重复 for (int k = 0; k < 9; k++) { if (k == j) continue; if (board.grid[i][k] == n) { board.isConflict[i][j] = true; board.isConflict[i][k] = true; } } // 列和宫同理... } } }

这个三重循环在每步落子时执行,最坏情况是 81 个格子各扫描 3 次,开销仍然很小,不需要优化为位运算。isConflict数组在drawNumbers中被读取,用红色覆盖蓝色或黑色。如果玩家清除冲突数字后仍觉红色残留,就说明updateConflict的扫描逻辑漏掉了该格,排查方向是循环边界和宫坐标计算。

6.4 胜负判定与键盘快捷键

游戏胜利判定不能只看空格数是否为 0,因为空格全填满不等于所有数字合法。正确判定是遍历所有格子,检查是否满足数独三大约束。复用isValidFast的位运算掩码可以更高效,但全盘遍历只有 81 格,直接用三重循环即可。加上 EasyX 键盘输入支持功能键VK_ESCAPE和回车:

void handleKey(char ch) { if (ch >= '1' && ch <= '9') { selectedNum = ch - '0'; } else if (ch == 'u' || ch == 'U') { undoManager.undo(board); } else if (ch == 'z' || ch == 'Z') { undoManager.redo(board); } else if (ch == VK_RETURN) { if (checkWin(board)) { settextstyle(40, 0, _T("Consolas")); outtextxy(180, BOARD_Y + BOARD_SIZE + 30, _T("Congratulations!")); } } }

键盘数字输入和鼠标点击可以共存,这要求selectedNum在绘图前统一读取。课程设计的完整体验是:玩家可以先用鼠标点选数字按钮或键盘输入数字,再用鼠标点击棋盘落子,撤销用 U,重做用 Z,回车提交答案。这种多模态交互在答辩展示时能给老师留下不错的印象。

6.5 工程组织与答辩演示的检查清单

把代码拆成SudokuBoard.h/cppSolver.h/cppUndoManager.h/cppGameUI.h/cpp四个模块是最稳的工程划分。SudokuBoard不包含任何 EasyX 头文件,这样逻辑层可以单独用命令行测试,比如写一段单元测试验证生成盘面的唯一解数量,这一步在答辩时是很有说服力的材料。展示前的自检建议按以下顺序:先生成一道中等难度题目,检查所有isFixed格子是否都正确标记;再选一个空格填入与题目冲突的数字,确认红框高亮出现;连续执行撤销重做各五次,确认棋盘状态与预期一致;最后检查输出字体是否在实验室电脑上正常渲染,避免因缺字体导致乱码。

EasyX 还有一个常被忽略的能力:调用saveimage可以把当前窗口保存为 BMP 文件,用于在实验报告里插入运行截图。这个功能在写课程设计报告时很好用,不需要在运行时用系统截图工具去截取窗口,直接在代码里放一个快捷键触发保存即可。

以上就是把“基于 C++ 使用 EasyX 实现的数独游戏课程设计”做成可演示、可答辩的完整路径。核心思想是:数据结构先用位运算把校验速度提上去,生成终盘用回溯加随机,挖洞时用求解器保证唯一解,界面层把绘制、事件、状态三者分离,最后用撤销栈和计时器完善游戏感。每一步都可以单独测试,最后再缝合到一起。如果你在实现过程中遇到生成题目明显偏难或偏简单的情况,优先调整target挖洞数,而不是修改回溯算法本身。

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

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

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

立即咨询