大一上学期C程序课程项目
控制台中的扫雷游戏捏
- LY:
game.c - CYX: 全部
- ChatGPT: 顾问
这是一个运行在控制台的扫雷游戏。点击操作说明按钮可以查看详细操作方式。
macro.h: 宏定义mouse.c: 读取鼠标事件,处理输出game.c: 游戏主逻辑solve.c: 自动解扫雷算法main.c: 主程序
-
init_all()函数:- 初始化控制台输出编码为GBK2312。
- 调用
initHandlers()函数来初始化一些输入输出句柄。 - 隐藏光标并设置转义字符功能。
- 初始化预设的邻居信息。
-
init_game(int w, int h, int m)函数:- 根据传入的宽(w)、高(h)和地雷数(m)参数,释放并重新分配游戏地图和推理模块所需的数据结构。
-
middle_action(int _x, int _y)函数:- 处理鼠标中键点击事件。根据用户是否开启了高级辅助模式,自动展开周边区域或进行逻辑判断操作。
-
highlight_nbr(int _cx, int _cy)函数:- 高亮显示指定位置周围的邻居格子。
-
初始化:调用
init_all()函数初始化全局变量和环境设置。 -
游戏界面循环:根据
page变量的不同值展示不同菜单或游戏界面。-
标题页:各种功能的主入口。
-
设置页:自定义游戏参数。
-
游戏准备页:初始化游戏地图的页面,只持续一下便会跳转到游戏页。
-
游戏页:游戏中的页面。
-
关于页:显示一些额外信息的页面。
-
-
刷新优化:通过
fflush(stdout)刷新输出缓冲区,确保内容立即显示在控制台上;在部分情况下强制刷新画面以清除残余图像。 -
结束游戏:当用户选择退出时,恢复控制台输入模式和字符编码至初始状态,重置文本格式,并再次释放游戏资源。
-
主要变量:
int** map:用于存储扫雷地图的二维数组,每个元素通过位运算来表示格子状态(是否有雷、是否翻开、是否插旗等)。int width, height, mines等:定义了地图尺寸、雷的数量以及一些生成雷的临时变量。。
-
主要函数:
fast_add_mask():根据字符串描述快捷添加新的邻接模式到dx[]和dy[]数组中,用于确定不同雷区布局规则。pos2ind()和ind2pos():将二维坐标和一维索引相互转换,主要用于推理模块确定变量的编号。inbound():判断给定坐标是否在地图范围内。malloc_map()和free_map():分别用于分配和释放地图内存空间。gen_map():生成雷区地图,确保第一次点击的位置不包含地雷,并计算周围地雷数量。open():翻开指定位置的格子,如果该格子无雷且周围没有地雷,则递归翻开其相邻的格子。get_attr():从编码好的数字中获取特定属性,如是否为地雷、是否已翻开等。print():图形化打印当前扫雷地图。debug_print():用于调试的打印函数,显示所有格子的状态信息。getCurrentTime():获取当前系统时间戳。
-
主要逻辑:
- 初始化时分配地图内存,并设置初始游戏状态。
- 生成地图时,确保玩家首次点击的地方不会是地雷,并计算剩余格子的地雷分布及周围雷数。
- 翻开格子时处理递归翻开空格子区域以及检测胜利条件(当翻开所有非雷格子时,游戏胜利)。
- 主要变量:
int** eq:用于存储求解器方程组的二维数组。int* pivot:用于存储主元素数组。int** ai_res:存储判断结果。
- 主要函数:
extern了一些game.c中的变量和函数。malloc_eq()和free_eq():分配和释放求解器所需矩阵、主元素数组以及其他辅助数据结构的内存空间。print_eq():打印方程组。row_op(),row_imul(),find_pivot(),cancel_ro():对方程组进行初等行变换。ai_decide():判断在某个格子上是否有雷,并更新方程组。add_total_restrict_eq():添加一个所有格子加起来等于总雷数的约束条件。add_eq():依据格子的雷数信息对周围邻居生成新的方程并加入到方程组中。elim_eq():对方程组进行消元处理,直到找到唯一解或者无变化。print_ai_res():打印推断出的各格子是否有雷的的结果。
- 定义了页面编号
- 定义了
get_attr可以获取的属性 - 定义了游戏中的状态
- 定义了转义字符序列的宏
- 定义了显示时用到的文本、颜色
- 定义了鼠标事件状态
- 定义了宏函数
BUTTON, FOR_NBR, FOR_MAP,简化部分代码
-
主要变量:
mouse_pos:存储鼠标在屏幕上的位置,以字符为单位。csbi,cfi,prevMode,hOutput,hInput:用于控制台相关的句柄、信息结构体及模式,用于获取和设置控制台属性。last_mouse_state,cur_mouse_state,mouse_x,mouse_y,mouse_action:记录当前及上一时刻的鼠标状态(左键、右键、中键),以及鼠标的横纵坐标与具体动作(如按下、抬起)。直接用于主循环中的判断。
-
主要函数:
setCursorPosition():设置光标位置到指定坐标。setForeColor()和setBackColor():设置文本前景色和背景色。resetTextFormat():重置文本格式为默认样式。clearConsoleScreen():清空控制台屏幕并移动光标至原点。editConsoleMode():修改控制台模式以启用鼠标输入。setScreenSize():设置控制台窗口大小。initHandlers():初始化控制台输出和输入句柄。updateMouseKeyState():更新鼠标按键状态。switchBuffer():双缓冲,减少屏幕刷新时的闪烁现象。checkMouseAction():判断当前鼠标操作的动作类型。SystemCLS():执行系统清屏命令,不可频繁使用。
- 十分精简,可以运行在 Windows 11 系统的控制台上,只依赖内置库即可编译运行。
- 主要使用鼠标操作,减少了手动输入坐标完成操作或使用方向键控制光标导致的操作不流畅问题。
- 每个文件使用
#ifndef\n#define\n...\n#endif保证文件只被引用一次,在交叉引用时不会出现重复展开的情况。借鉴于各大C标准库。
#define BUTTON(TEXT, X, Y, FUNC) \
do { \
printf(CSI "0m" CSI "7m" CSI "%dG" CSI "%dd%s" CSI "0m", (X) + 1, \
(Y) + 1, TEXT); \
if (mouse_action == MOUSE_LEFTUP && mouse_x >= X && \
mouse_x < X + strlen(TEXT) && mouse_y == Y) { \
force_flush = 1; \
do \
FUNC while (0); \
} \
} while (0)这个宏可以简化大量重复放置按钮的代码,可以像一般的打印的方式在代码中插入按钮,并指定触发后的行为。使用方式类似于:
BUTTON("随机游戏", 20, 8, {
page = PAGE_GAME_ENTER;
user_h = rand() % 10 + 10;
user_w = rand() % 10 + 10;
user_m = rand() % 5 + user_h * user_w / 6;
mask_id = rand() % totmask;
});使用do{...}while(0)可以在执行一遍指定代码的情况下,强制要求语句后跟分号,在不写分号时编译器便会报错,让这条语句和上下文更一致。同理,do FUNC while(0);强制要求触发行为语句被{}包裹。
#define FOR_NBR(DX, DY) \
for (int DX##_##DY##_##i = 0, DX = dx[mask_id][DX##_##DY##_##i], \
DY = dy[mask_id][DX##_##DY##_##i]; \
DX##_##DY##_##i < dcard[mask_id]; \
DX##_##DY##_##i++, DX = dx[mask_id][DX##_##DY##_##i], \
DY = dy[mask_id][DX##_##DY##_##i])
#define FOR_MAP(X, Y) \
for (int Y = 0; Y < height; Y++) \
for (int X = 0; X < width; X++)宏定义FOR_NBR和FOR_MAP可以简化循环语句,使用方式类似于:
FOR_NBR(_dx, _dy) {
int new_x = temp_x[i] - _dx;
int new_y = temp_y[i] - _dy;
if (inbound(new_x, new_y)) {
map[new_x][new_y] += 8;
// 显示的数字从第3位开始存储,刚好+8
}
}使用##连接关键字,创建临时的循环变量,可以防止嵌套宏定义时,变量名冲突。FOR_MAP写了但没有人用。
int dx[50][50];
int dy[50][50];
int dcard[50];
int totmask;
/*
使用x表示方格本身,使用o表示这个方格统计格子时的邻居,空格表示跳过。
例如:"ooo\\noxo\\nooo" 表示通常的八联通游戏模式。
*/
void fast_add_mask(char* str) {
int x = 0, y = 0, cx = -1, cy = -1;
int len = strlen(str);
for (int i = 0; i < len; i++) {
if (str[i] == '\n') {
x = 0;
y++;
continue;
}
if (str[i] == 'x') {
cx = x, cy = y;
break;
}
x++;
}
if (cx != -1 && cy != -1) {
dcard[totmask] = 0;
x = 0, y = 0;
for (int i = 0; i < len; i++) {
if (str[i] == '\n') {
x = 0;
y++;
continue;
}
if (str[i] == 'o') {
dx[totmask][dcard[totmask]] = x - cx;
dy[totmask][dcard[totmask]] = y - cy;
dcard[totmask]++;
}
x++;
}
totmask++;
}
}多种邻居规则,使用fast_add_mask函数添加规则。
dx和dy的存储邻居相对于自身的坐标增量。dcard存储每种规则下邻居的个数。totmask存储总规则数。这种方法可以更灵活的添加规则,还可以使得邻居关系不具备自反性,增加策略性、游戏性、趣味性,也使得代码扩展性强。
主程序一个while循环,检测鼠标事件并更新屏幕,使用page变量标识当前页面内容,将有明显改变的页面放置在不同的switch case分支中。
只保留了unsigned long long getCurrentTime();这一个函数。
其他很多代码(包括LY的代码)都被我否了重新写了。
CYX:
- 在写自动解扫雷的程序时,发现打开推理功能后,每重开几次游戏就会崩溃。一开始先是注释掉了释放推理模块所用内存的代码,发现可以正常运行,但这种不释放内存的行为有很大的安全隐患。后来发现,如果打开推理模块,但游戏时不点开格子或点开的格子离边缘比较远,则可以正确运行。最终发现,在根据格子信息添加新线索时,没有检查是否超出边界。又因为为了降低线索存储区的数组维度,我将两个坐标组合成了一个数,导致这个数可能访问到了超出范围的内存。最终成功解决了这个问题。
- 添加计时模块时,发现之前的代码只在鼠标发生状态变化时,才更新屏幕状态,导致时间无法实时更新。一开始重新写一段代码,检测鼠标的按键状态,但导致无法实时获取鼠标的坐标。后来发现,库函数中有一个可以统计未处理的事件个数的函数,只要获取到这个值,判断到是0就可以直接更新屏幕状态,而不用等待鼠标事件。
- 刚刚最后一遍审查代码的时候,发现一个bug。
get_attr函数在获取格子数字的时候,错误的假设了数字范围为0~15,但在现有的邻居规则下,大于15的数字是可能存在的。这会导致自动翻开功能和自动推理功能的错误。还好得到了及时的修复。 - 最大的收获莫过于翻看函数的文档过程中的学习。同时觉得,微软的中文文档翻译水平有待提升,看了一段时间后果断切换回了英文版。
LY:
- 读懂含有五个基本功能的文件,在大部分不清楚的情况下了解其相互结构关系、主要函数和变量的功能
- 了解了诸如ifndef define的细节、诸如main中大循环判断状态的基本算法,诸如main调用.c/.h文件的程序交互结构