使用C语言实现扫雷游戏,并在控制台进行游玩
0 可以跳过的垃圾话
想着练练手,刚好看见舍友买了个扫雷游戏,于是就想着来写个扫雷看看,再试试能不能给它加点不一样的功能。当然,图形不会像现在的扫雷游戏那么精美就是了。
1 分析扫雷游戏
扫雷游戏的大体分析放在了下面的game.h里
2 扫雷游戏主体
代码运行于visual studio 2022中
我一共写了三个文件,包括两个.c源文件和一个.h头文件
分别是test.c、game.c、game.h。
<game.h>
//我把需要用到的头文件、宏定义、部分自定义函数声明放在了game.h里
//这样后续在.c文件里,直接#include "game.h"就好
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE 5
#define CHECK '='
//扫雷游戏-minesweeper的结构:开始菜单、棋盘布局、布雷格局、扫雷功能、游戏结算
//开始菜单在test.c中直接写就可以
//游戏本体
//棋盘
//空白棋盘(初始化棋盘)
void initialize_board(char board[ROWS][COLS], int rows, int cols, char component);
//表面棋盘
//显示9*9的棋盘,使用二维字符数组,以放置“*”或ASCII码的方式来表示棋盘的位置
void print_board(char board[ROWS][COLS], int row, int col);
//布雷棋盘
void set_mine(char board[ROWS][COLS], int row, int col, int mine);
//扫雷
//探查指定位置,使用scanf读入键盘输入
//读入行列后,与布雷棋盘比对,给出踩中雷的提示或者没踩中雷,而周围有多少雷的提示
void find_mine(char mine[ROWS][COLS], char surface[ROWS][COLS], int rows, int cols);
//以递归的方式,实现自动排除连续的“周围无雷”地块
<game.c>
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void initialize_board(char board[ROWS][COLS], int rows, int cols, char component)
{
//初始化就是给数组中的各个元素赋值,这里是给字符数组赋值,两个循环直接搞定即可
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
board[i][j] = component;
}
}
}
void print_board(char board[ROWS][COLS], int row, int col)
{
for (int i = 0; i <= col; i++)
{
printf("%2d", i);
}
printf("\n");
//这里就是先用来测试初始化有没有成功,而且后续用来把表面棋盘打印出来,因为打印的棋盘只要在中间的9*9,不需要11*11,所以使用1到ROL(9)就可以了
for (int i = 1; i <= row; i++)
{
printf("%2d", i);//打印行号
for (int j = 1; j <= col; j++)
{
printf("%2c", board[i][j]);//给一定的栏宽把它们对齐
}
printf("\n");//换行
}
}
void set_mine(char board[ROWS][COLS], int row, int col, int mine)
{
for (int i = 1; i <= mine; i++) {
board[rand() % row + 1][rand() % col + 1] = '1';//设置mine个雷,雷的范围在ROW*COL的内部棋盘内
}
}
int count_mine(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
if (row <= 0 || col <= 0)
{
return -1;
}
else
{
//如果没踩中雷,则在表面棋盘显示周围8格内的雷的数量,
//即[row-1][col-1],[row-1][col],[row-1][col+1]
//与[row][col-1], ,[row][col+1]
//与[row+1][col-1],[row+1][col],[row+1][col+1]
int mines = mine[row - 1][col - 1] + mine[row - 1][col] + mine[row - 1][col + 1] + mine[row][col - 1] + mine[row][col + 1] + mine[row + 1][col - 1] + mine[row + 1][col] + mine[row + 1][col + 1];
//下面这段与check_auto中使用的符号有关,在不使用新的二维数组的情况下,我能做到的就是给数据修正
//为了方便后续修改,我给用上了宏定义变量,名为CHECK,在game.h处修改
if (mine[row - 1][col - 1] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row - 1][col] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row - 1][col + 1] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row][col - 1] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row][col + 1] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row + 1][col - 1] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row + 1][col] == CHECK)
{
mines -= CHECK - '0';
}if (mine[row + 1][col + 1] == CHECK)
{
mines -= CHECK - '0';
}return mines - 8 * '0';
}
}
void find_mine(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
//如果踩中雷,则游戏结束
if (1 <= row && row <= ROW && 1 <= col && col <= COL)
{
if(surface[row][col] == '*')
{
surface[row][col] = count_mine(mine, surface, row, col) + '0';
}
}
}
void auto_surrounding(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
if (1 <= row && row <= ROW && 1 <= col && col <= COL)
{
// find_mine(mine, surface, row - 1, col - 1);
find_mine(mine, surface, row - 1, col);
// find_mine(mine, surface, row - 1, col + 1);
find_mine(mine, surface, row, col - 1);
find_mine(mine, surface, row, col + 1);
// find_mine(mine, surface, row + 1, col - 1);
find_mine(mine, surface, row + 1, col);
// find_mine(mine, surface, row + 1, col + 1);
}
}
int check(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col)
{
int flag = 0;
// if (1 <= row && row <= ROW && 1 <= col && col <= COL)
// {
if (surface[row][col] == '*')
{
return flag + 1;
}
// }
return 0;
}
void auto_sweeper(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col, int mines, int depth)
{
// system("cls"); //测试用,带此段代码运行测试时,请在进入游戏、输入非地雷坐标后按住任意键以观察显示变化
// printf("depth = %d", depth);
// print_board(surface, ROW, COL);
// print_board(mine, ROW, COL);
// system("pause");
if (mines == 0)
{
int flag = 0;
auto_surrounding(mine, surface, row, col);
if ((1 <= row && row <= ROW) && (1 <= col && col <= COL))//防止越界,导致探查的位置超出棋盘
{
//以上、下、左、右的方式把没雷的地方走一遍
if (flag = check(mine, surface, row - 2, col)) {
mines = count_mine(mine, surface, row - 1, col);
auto_sweeper(mine, surface, row - 1, col, mines, depth + 1);//上
}
if (flag = check(mine, surface, row, col + 2)) {
mines = count_mine(mine, surface, row, col + 1);
auto_sweeper(mine, surface, row, col + 1, mines, depth + 1);//右
}
if (flag = check(mine, surface, row + 2, col)) {
mines = count_mine(mine, surface, row + 1, col);
auto_sweeper(mine, surface, row + 1, col, mines, depth + 1);//下
}
if (flag = check(mine, surface, row, col - 2)) {
mines = count_mine(mine, surface, row, col - 1);
auto_sweeper(mine, surface, row, col - 1, mines, depth + 1);//左
}
}
}
}
void check_auto(char mine[ROWS][COLS], char surface[ROWS][COLS], int row, int col, int flag, int depth)
{
for (int i = 1; i <= ROW; i++)
{
for (int j = 1; j <= COL; j++)
{
if (surface[i][j] == '0' && mine[i][j] != CHECK)
{
mine[i][j] = CHECK;//'='关联count_mine函数中的mines的计算公式,ASCII中‘=’=61
auto_sweeper(mine, surface, i, j, flag, 0);
}
}
}
}
int success(char mine[ROWS][COLS], char surface[ROWS][COLS])
{
int marked = 0;
int leftover = 0;
for (int i = 1; i <= ROW; i++)
{
for (int j = 1; j <= COL; j++)
{
if (surface[i][j] == '#' && mine[i][j] == '1')//正确标记贡献的地雷数
{
marked++;
}
if (surface[i][j] == '*') {
leftover++;
}
}
}
return (marked != MINE) ? (leftover + marked): (marked);
//使用三目操作符,在marked不为MINE的时候,返回leftover + marked,由于marked的增长条件,它不会大于MINE
//而当leftover + marked不为MINE时,有这种情况:
//未标记或者已标记的地块中存在没有地雷且未探查的。
//而我设置的游戏胜利条件是此函数返回的值为MINE才会胜利并结束游戏
}
void game()
{
int mark = 0;
printf("test\n");
char surface_board[ROWS][COLS];
char mine_board[ROWS][COLS];
initialize_board(surface_board, ROWS, COLS, '*');//这里数组实参填数组名即可,无需加【】【】,那样就是表示数组中的具体元素了
initialize_board(mine_board, ROWS, COLS, '0');//ROWS和COLS这里是11,为了方便后续进行地雷的判断。最后的字符表示初始化时的字符内容,也就是初始化后数组所有元素的内容应该时字符‘0’
set_mine(mine_board, ROW, COL, MINE);//布雷
// print_board(surface_board, ROW, COL);//打印棋盘
// print_board(mine_board, ROW, COL);
//探查地雷
while(1){
system("cls");
print_board(surface_board, ROW, COL);//打印棋盘
print_board(mine_board, ROW, COL);
int i = 0;
int row, col;
printf("请输入要探查的行号,若要进入标记模式,请输入0:");
scanf("%d", &row);
if (row == 0)
{
while (1) {
printf("若要退出标记模式,请输入0行0列\n");
printf("标记同一位置偶数次,即可取消标记\n");
printf("剩余标记次数:%d\n", MINE - mark);
printf("请输入要标记的行、列,使用空格或回车隔开:");
scanf("%d%d", &row, &col);
if (row == 0 && col == 0) {
break;
}
else if (mark >= MINE) {
printf("标记次数已耗尽,标记失败\n");
}
else if ((0 < row || row < ROW || 0 < col || col < COL) && (surface_board[row][col] == '*' || surface_board[row][col] == '#'))
{
if (surface_board[row][col] == '#')
{
surface_board[row][col] == '*';
mark--;
}
else {
surface_board[row][col] = '#';
mark++;
}
i = success(mine_board, surface_board);
if (i == MINE)
{
system("cls");
print_board(surface_board, ROW, COL);//打印棋盘
print_board(mine_board, ROW, COL);
printf("恭喜你找到了所有的雷!游戏胜利!\n");
// system("pause");
break;
}
}
else {
printf("请输入棋盘内的行列号!\n");
system("pause");
}
system("cls");
print_board(surface_board, ROW, COL);//打印棋盘
print_board(mine_board, ROW, COL);
}
if (i == MINE)
break;
}
else {
printf("请输入要探查的列号:");
scanf("%d", &col);
if (0 > row || row > ROW || 0 > col || col > COL)
{
printf("请输入棋盘内的行列号!\n");
system("pause");
continue;
}
find_mine(mine_board, surface_board, row, col);
if (mine_board[row][col] == '1')
{
printf("很遗憾,你踩中地雷了\n");
system("pause");
break;
}
int flag = count_mine(mine_board, surface_board, row, col);
auto_sweeper(mine_board, surface_board, row, col, flag, 0);
check_auto(mine_board, surface_board, row, col, flag, 0);//给自动延伸(auto_sweeper)查缺补漏
//此处后续加一个功能,当探查地块周围没有雷时,自动探查周围直到地块数字不为0为止
i = success(mine_board, surface_board);
if (i == MINE)
{
system("cls");
print_board(surface_board, ROW, COL);//打印棋盘
print_board(mine_board, ROW, COL);
printf("恭喜你找到了所有的雷!游戏胜利!\n");
break;
}
}
}
Sleep(8000);
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
printf("* * * * * * * * * * * * * *\n");
printf("* M I N E - S W E E P E R *\n");
printf("* * author:fireqian * *\n");
printf("* * * *\n");
printf("* * enter 1 to start * *\n");
printf("* * enter 0 to exit * *\n");
printf("* * * * * * * * * * * * * *\n");
}
int main()
{
int start = 1;
//菜单
menu();
while (scanf("%d", &start) != EOF)
{
srand((unsigned int)time(NULL));//每一局游戏的不同种子
switch (start) {
case 1://开始游戏
printf("开始游戏,加载中...\n");//提示操作结果
Sleep(300);
system("cls");
game();
system("pause");
system("cls");
menu();
break;
case 0://结束游戏
break;
default:
printf("请输入1或0,以开始或结束游戏\n");
system("pause");
system("cls");
menu();
break;
}
if (start == 0) {
break;//实现结束游戏,终止程序while循环
}
}
}
12.23 这里放个运行的图片(其实自动延伸还有个bug)
12.22 game.c和test.c的主体我都写好了,等我摸一会拓展功能在复制上来。分析在那个时候一块写
12.23 啊,写那个遇见0地雷可自动延伸的功能发现挺多bug的,简单修修凑合用了。代码写的太乱了,还没整理。要是各位读者也想写这个功能,不妨试试8方向判定,我这个是4方向判定的,时间太晚了,11点23分了,我先休息(打街霸6)去了,今天也是摸了,晚安。
12.24 早上11点,想到了探查周围8格其实可以改成周围4格,就是auto_surrounding函数可以只留下上、下、左、右四个方向的地块探查,防止出bug。今天稍晚再写个插旗标记的功能吧(说是插旗,其实没有旗帜符号,我到时候用@来代替吧),然后要是有时间就再写一个游戏胜利的判定。
关于如何减少检查次数(也就是不让check_auto检查已经检查过的地方),我第一想法是再设置第三个棋盘(也就是第三个二维数组),用来标记检查痕迹,用有无检查痕迹作为check_auto的一个检查条件,避免重复检查。
(现在还没写出来,简单的思路就是先新建一个同样大小的二维数组,再初始化。然后在check_auto里加入一个功能,使检查过的地块在check棋盘里变为某一约定好的值,这里我还是想用char类型,毕竟一个char才一个字节。再然后在check_auto里加一个if(check[i][j]) != ‘某一约定好的值’)就行了)。
12.25 昨天晚上睡不着,突然脑子里想到,不需要追加新的棋盘也能做避免重复检查的标记。反正自动延展不会在地雷地块生效,就在布雷棋盘上做标记就是了,不过记号要和地雷区分开来。写了才深刻感受到什么叫史山代码,哈哈。一环套一环的,没有注释的话不知道在干嘛。想方便还是直接做个新的二维数组吧,省心。这里我没做,就直接在地雷的二维数组里改,当场出现bug,修了一会哈哈。
顺带摸了标记功能,然后加了点保护措施。标记功能就懒得再打包成一个函数了,就这样吧。