C语言实现扫雷(标记/取消标记雷、自动展开)

时间:2022-11-11 12:00:59

前言

扫雷的设计和三子棋大同小异。这次我在之前的扫雷基础上加入了标记/取消标记雷和显示雷的数量的功能。希望对大家能有帮助。

这里以9*9的棋盘为例。先附上游戏的最终效果:

C语言实现扫雷(标记/取消标记雷、自动展开)

扫雷的C语言实现分为两个模块:

1.测试模块

2.游戏模块



测试模块

先给出主函数,在主函数加入测试模块test()

int main()
{
test();
return 0;
}

同样用printf()打印出一个简易的菜单,将其封装在menu()

const void menu()
{
printf("***--------- *扫雷* -------***\n");
printf("***------ * 1.play * ------***\n");
printf("***------ * 0.exit * ------***\n");
printf("***--------------------------***\n");
}

用户输入1,进行玩游戏;输入0,退出游戏;输入其他值,提示用户重新输入

实现这个逻辑的代码如下>

void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择>:");//提示用户输入
scanf("%d", &input);
switch (input)
{
case 1:
system("cls");
game();//扫雷游戏
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
Sleep(3000);
system("cls");
break;
}
} while (input);
}

在while的判断部分中放入input,当用户输入非零时循环不终止;输入0时退出游戏,并退出循环。这就完成了test()模块,并引出了一个game()模块。

下面会在game()中实现游戏的基本逻辑。



游戏模块


定义和初始化棋盘

为了进行扫雷操作时不干扰棋盘中雷的布局,这里定义两个二维数组。一个数组存储布雷的信息,另一个数组存储扫雷的信息(展示用)。

为了便于后续布雷和增加游戏的可玩性,将两个数组的内容分别初始化为'1'‘*’

因为两个数组初始化内容不同,故将初始化内容也作为初始化函数的参数。

需要注意的是,在后续判断一个位置周围雷的信息时,需要对该位置周围8个位置进行遍历,为了避免对边缘位置进行判断时出现数组越界的情况,需要将两个数组行、列数各增加2。

C语言实现扫雷(标记/取消标记雷、自动展开)

void Initboard(char board[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}


布雷

用时间戳设置随机数种子,实现布雷的随机性。

srand((unsigned int)time(NULL));

为了后续便于判断和提示,布雷时将‘1’作为雷的标记,将'0'作为非雷的标记。这是因为在判断一个位置周围雷的数量时,可以直接将字符'1'相加减去数个字符'0'便可直接得到该位置周围雷的数量。

在布雷的过程中也要注意避免在一个位置重复布雷。

void Setmine(char board[ROWS][COLS], int rows, int cols)
{
int times = EASY_COUNT;//EASY_COUNT代表布雷的数量
while (times)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
times--;
}
}
}


打印棋盘

在初始化时是在整个数组上进行操作,但在打印棋盘展示给玩家时,只需要打印中间部分9*9大小的棋盘即可。为了便于玩家辨认坐标,可以给每行每列加上序列号,并为棋盘加上边框

void Displayboard(char board[ROWS][COLS], int row, int col)
{
for (int i = 0; i <= row; i++)//序列号
{
printf("%d ", i);
}
printf("\n");
for (int i = 0; i <= col; i++)//边框
{
printf("--");
}

printf("\n");

for (int i = 1; i <= row; i++)//数据部分
{
printf("%d|", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("||\n");//边框
}

for (int i = 0; i <= col; i++)//边框
{
printf("--");
}
printf("\n");
}


扫雷


操作列表

玩家正式进入游戏部分后,可以有三个操作选择

1.扫雷操作

2.标记雷操作

3.取消标记雷操作


设计一个操作列表并提示玩家进行选择>

const void chose_list()
{
printf("\n|----------------|\n");
printf("|---- 1.扫雷 ----|\n");
printf("|--- 2.标记雷 ---|\n");
printf("|--3.取消标记雷--|\n");
printf("|----------------|\n");
printf("\n");
}


扫雷操作

当玩家选择输入1时,进入扫雷操作部分,提示用户输入扫雷坐标。若输入的坐标是雷,则提示玩家扫雷失败并退出扫雷部分。若输入坐标不是雷,则进入判断和提示

int Swapmine(char mine[ROWS][COLS], char show[ROWS][COLS], int rows, int cols,int* count)
{
/*
1.被炸死或者扫雷成功返回0
2.否则返回1
*/
int x, y;
while (1)
{
printf("请输入要扫雷的坐标\n");
printf("(若要退出该操作,请输入0 0)\n>:");
scanf("%d %d", &x, &y);
if (x==0 && y==0)
{
system("cls");
Displayboard(show, ROW, COL);
return 1;
}
//判断输入是否合法
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
//合法
if (show[x][y] != '*')
{
printf("该位置已被检查!\n");
continue;
}

if (mine[x][y] == '1')//是雷
{
printf("很遗憾,你被炸死了!\n");
Sleep(3000);
system("cls");
Displayboard(show, ROW, COL);
Displayboard(mine, ROW, COL);
return 0;
}
else//不是雷
{
Exclude(mine, show, x, y);//判断和提示
system("cls");
Displayboard(show, ROW, COL);
print_count(count);

int win = get_win(show, ROW, COL);
if (win == EASY_COUNT)
{
printf("恭喜你,扫雷成功!!!\n");
Displayboard(mine, ROW, COL);
return 0;
}
return 1;
}
}
else
{
//不合法
printf("输入非法,请重新输入\n");
}
}
return 1;
}

这里的扫雷函数相比之前多了一个int* count参数,且返回类型变为了int,这些是为了顺利完成其他操作功能和显示剩余雷的数量的功能而设计的,在对应部分会讲到。玩家可以通过输入“0 0”退出该操作。


判断和提示

若用户输入的坐标不是雷,则需要遍历该位置周围的8个坐标,计算周围雷的数量并显示在棋盘上展示给玩家。

计算周围雷的数量>

int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x - 1][y] +
mine[x + 1][y] +
mine[x - 1][y + 1] +
mine[x][y + 1] +
mine[x + 1][y + 1] - 8 * '0';
}

为了实现自动展开的功能,这里用函数递归。

进行展开有三个条件:

  1. 该位置不是雷
  2. 该位置周围没有雷
  3. 该位置及周围的位置没有被检查(防止死递归)
void Exclude(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int counts = get_mine_count(mine, x, y);
if (counts != 0)
{
show[x][y] = counts + '0';//展示到棋盘上
}
else
{
show[x][y] = ' ';
if (show[x - 1][y - 1] == '*' && ((x - 1) >= 1 && (x - 1) <= ROW) && ((y - 1) >= 1 && (y - 1) <= COL))
{
Exclude(mine, show, x - 1, y - 1);
}
if (show[x][y - 1] == '*' && (x >= 1 && x <= ROW) && ((y - 1) >= 1 && (y - 1) <= COL))
{
Exclude(mine, show, x, y - 1);
}
if (show[x + 1][y - 1] == '*' && ((x + 1) >= 1 && (x + 1) <= ROW) && ((y - 1) >= 1 && (y - 1) <= COL))
{
Exclude(mine, show, x + 1, y - 1);
}
if (show[x - 1][y] == '*' && ((x - 1) >= 1 && (x - 1) <= ROW) && (y >= 1 && y <= COL))
{
Exclude(mine, show, x - 1, y);
}
if (show[x + 1][y] == '*' && ((x + 1) >= 1 && (x + 1) <= ROW) && (y >= 1 && y <= COL))
{
Exclude(mine, show, x + 1, y);
}
if (show[x - 1][y + 1] == '*' && ((x - 1) >= 1 && (x - 1) <= ROW) && ((y + 1) >= 1 && (y + 1) <= COL))
{
Exclude(mine, show, x - 1, y + 1);
}
if (show[x][y + 1] == '*' && (x >= 1 && x <= ROW) && ((y + 1) >= 1 && (y + 1) <= COL))
{
Exclude(mine, show, x, y + 1);
}
if (show[x + 1][y + 1] == '*' && ((x + 1) >= 1 && (x + 1) <= ROW) && ((y + 1) >= 1 && (y + 1) <= COL))
{
Exclude(mine, show, x + 1, y + 1);
}
}
}

需要注意的是,为了展开时沿9*9棋盘的外边缘进行蔓延,需要将坐标限制在9*9棋盘中。


扫雷成功判断

扫雷的游戏过程可以理解为尽可能清除棋盘上的'*',且棋盘上'*'可能出现的最少数量只能是雷的数量,此时所有雷的位置都被确定。所以可以计算9*9棋盘上'*'的数量win,若win等于所有雷的数量即可判定扫雷成功。

int get_win(char show[ROWS][COLS], int row, int col)
{
int ret = 0;
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (show[i][j] == '*')
{
ret++;
}
}
}
return ret;
}


标记/取消标记雷操作

玩家选择标记雷操作,提示玩家输入要标记的坐标。首先判断输入的合法性,若输入非法,提示玩家重新输入;若输入合法,则用'#'作为标记,修改show数组并展示。玩家可以通过输入“0 0”退出该操作。

void mark_mine(char show[ROWS][COLS],char mine[ROWS][COLS], int row, int col,int* count)
{
int x, y;
while (1)
{
printf("请输入要标记的坐标\n");
printf("(若要退出标记,请输入0 0)\n>:");
scanf("%d %d", &x, &y);
if (x == 0 && y == 0)
{
system("cls");
Displayboard(show, row, col);
break;
}

if (x > 0 && x <= row && y > 0 && y <= col)
{
//合法
if (show[x][y] == '#')
{
printf("该位置已被标记,请重新输入>\n");
}
else if (show[x][y] == ' ')
{
printf("该位置无法被标记,请重新输入>\n");
}
else
{
if (mine[x][y] == '1')
{
(*count)--;
}
show[x][y] = '#';
system("cls");
Displayboard(show, row, col);
print_count(count);
break;
}
}
else
{
//非法
printf("输入非法,请重新输入>");
}
}
}


取消标记操作的实现与标记操作类似。用户选择取消标记操作,提示用户输入取消标记操作的坐标,首先判断输入的合法性,若输入非法,提示玩家重新输入;若输入合法,则将'#'修改为‘*’,修改show数组并展示。玩家可以通过输入“0 0”退出该操作。


显示雷的剩余数量功能

玩家每次进行扫雷、标记/取消标记雷的操作后,在更新并打印棋盘的同时展示剩余雷的数量,可以增加游戏的可玩性。

game()中初始化雷的数量countEASY_COUNT,即初始雷的数量。当玩家标记的位置有雷时,默认玩家已经找到了一个雷,count减1;若玩家取消了某个位置的标记,且该位置有雷,则count加1(你不能笃定没人会这么干)。

首先写出一个具有显示功能的函数>

void print_count(int* count)//为了真正改变count,这里传递count的地址
{
printf("\n剩余雷的数量:%d\n", *count);
}

该函数只负责打印。修改count的任务交给了各种操作模块。也就是说计算和修改count的操作嵌入了各个操作模块中,print_count模块只负责显示。这就解释了上文中三个操作模块的参数列表中都含int* count参数的原因。

C语言实现扫雷(标记/取消标记雷、自动展开)


完整代码

game.h

#pragma once

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<Windows.h>


#define ROWS 11
#define COLS 11
#define ROW ROWS-2
#define COL COLS-2
#define EASY_COUNT 10

void Initboard(char board[ROWS][COLS], int rows, int cols, char set);
void Displayboard(char board[ROWS][COLS], int row, int col);
void Setmine(char board[ROWS][COLS], int rows, int cols);
int Swapmine(char mine[ROWS][COLS], char show[ROWS][COLS], int rows, int cols,int* count);
void Exclude(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
int get_win(char show[ROWS][COLS], int row, int col);
void mark_mine(char show[ROWS][COLS],char mine[ROWS][COLS], int row, int col, int* count);
void unmark_mine(char show[ROWS][COLS], char mine[ROWS][COLS],int row, int col,int* count);
void print_count(int* count);


test.c

#define _CRT_SECURE_NO_WARNINGS 1

//扫雷

#include"game02.h"

const void menu()
{
printf("***--------- *扫雷* -------***\n");
printf("***------ * 1.play * ------***\n");
printf("***------ * 0.exit * ------***\n");
printf("***--------------------------***\n");
}

const void chose_list()
{
printf("\n|----------------|\n");
printf("|---- 1.扫雷 ----|\n");
printf("|--- 2.标记雷 ---|\n");
printf("|--3.取消标记雷--|\n");
printf("|----------------|\n");
printf("\n");
}

void game()
{
int count = EASY_COUNT;
//定义棋盘
//初始化棋盘
//布雷
//
//扫雷

//定义棋盘
char mine[ROWS][ROWS] = { 0 };
char show[ROWS][COLS] = { 0 };

//初始化棋盘
Initboard(mine, ROWS, COLS, '0');
Initboard(show, ROWS, COLS, '*');

//Displayboard(mine,ROWS,COLS);//打印棋盘测试

//布雷
Setmine(mine, ROWS, COLS);
Displayboard(show, ROW, COL);
print_count(&count);
//Displayboard(mine,ROWS,COLS);//测试

int ret = 1;
int input = 0;
while (ret)
{
chose_list();//选择列表
printf("请选择你的操作>:");
scanf("%d", &input);
switch (input)
{
case 1:
//扫雷
ret = Swapmine(mine, show, ROWS, COLS,&count);
break;
case 2:
mark_mine(show,mine, ROW, COL,&count);//标记雷
break;
case 3:
unmark_mine(show,mine, ROW, COL,&count);//取消标记雷
break;
default:
system("cls");
Displayboard(show, ROW, COL);
printf("选择错误,请重新选择>\n");
break;
}
}
}

void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case 1:
system("cls");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
Sleep(3000);
system("cls");
break;
}
} while (input);
}

int main()
{
test();
return 0;
}


game.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"game02.h"


void Initboard(char board[ROWS][COLS], int rows, int cols, char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}

void Displayboard(char board[ROWS][COLS], int row, int col)
{
for (int i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 0; i <= col; i++)
{
printf("--");
}

printf("\n");

for (int i = 1; i <= row; i++)
{
printf("%d|", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("||\n");
}
for (int i = 0; i <= col; i++)
{
printf("--");
}
printf("\n");
}

void Setmine(char board[ROWS][COLS], int rows, int cols)
{
int times = EASY_COUNT;
while (times)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
times--;
}
}
}

int Swapmine(char mine[ROWS][COLS], char show[ROWS][COLS], int rows, int cols,int* count)
{
/*
1.被炸死或者扫雷成功返回0
2.否则返回1
*/
int x, y;
while (1)
{
printf("请输入要扫雷的坐标\n");
printf("(若要退出该操作,请输入0 0)\n>:");
scanf("%d %d", &x, &y);
if (x==0 && y==0)
{
system("cls");
Displayboard(show, ROW, COL);
return 1;
}
//判断输入是否合法
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
//合法
if (show[x][y] != '*')
{
printf("该位置已被检查!\n");
continue;
}

if (mine[x][y] == '1')//是雷
{
printf("很遗憾,你被炸死了!\n");
Sleep(3000);
system("cls");
Displayboard(show, ROW, COL);
Displayboard(mine, ROW, COL);
return 0;
}
else//不是雷
{
Exclude(mine, show, x, y);//判断和提示
system("cls");
Displayboard(show, ROW, COL);
print_count(count);

int win = get_win(show, ROW, COL);
if (win == EASY_COUNT)
{
printf("恭喜你,扫雷成功!!!\n");
Displayboard(mine, ROW, COL);
return 0;
}
return 1;
}
}
else
{
//不合法
printf("输入非法,请重新输入\n");
}
}
return 1;
}

int get_win(char show[ROWS][COLS], int row, int col)
{
int ret = 0;
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (show[i][j] == '*')
{
ret++;
}
}
}
return ret;
}

void Exclude(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int counts = get_mine_count(mine, x, y);
if (counts != 0)
{
show[x][y] = counts + '0';
}
else
{
show[x][y] = ' ';
if (show[x - 1][y - 1] == '*' && ((x - 1) >= 1 && (x - 1) <= ROW) && ((y - 1) >= 1 && (y - 1) <= COL))
{
Exclude(mine, show, x - 1, y - 1);
}
if (show[x][y - 1] == '*' && (x >= 1 && x <= ROW) && ((y - 1) >= 1 && (y - 1) <= COL))
{
Exclude(mine, show, x, y - 1);
}
if (show[x + 1][y - 1] == '*' && ((x + 1) >= 1 && (x + 1) <= ROW) && ((y - 1) >= 1 && (y - 1) <= COL))
{
Exclude(mine, show, x + 1, y - 1);
}
if (show[x - 1][y] == '*' && ((x - 1) >= 1 && (x - 1) <= ROW) && (y >= 1 && y <= COL))
{
Exclude(mine, show, x - 1, y);
}
if (show[x + 1][y] == '*' && ((x + 1) >= 1 && (x + 1) <= ROW) && (y >= 1 && y <= COL))
{
Exclude(mine, show, x + 1, y);
}
if (show[x - 1][y + 1] == '*' && ((x - 1) >= 1 && (x - 1) <= ROW) && ((y + 1) >= 1 && (y + 1) <= COL))
{
Exclude(mine, show, x - 1, y + 1);
}
if (show[x][y + 1] == '*' && (x >= 1 && x <= ROW) && ((y + 1) >= 1 && (y + 1) <= COL))
{
Exclude(mine, show, x, y + 1);
}
if (show[x + 1][y + 1] == '*' && ((x + 1) >= 1 && (x + 1) <= ROW) && ((y + 1) >= 1 && (y + 1) <= COL))
{
Exclude(mine, show, x + 1, y + 1);
}
}
}

int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1] +
mine[x][y - 1] +
mine[x + 1][y - 1] +
mine[x - 1][y] +
mine[x + 1][y] +
mine[x - 1][y + 1] +
mine[x][y + 1] +
mine[x + 1][y + 1] - 8 * '0';
}

void mark_mine(char show[ROWS][COLS],char mine[ROWS][COLS], int row, int col,int* count)
{
int x, y;
while (1)
{
printf("请输入要标记的坐标\n");
printf("(若要退出标记,请输入0 0)\n>:");
scanf("%d %d", &x, &y);
if (x == 0 && y == 0)
{
system("cls");
Displayboard(show, row, col);
break;
}

if (x > 0 && x <= row && y > 0 && y <= col)
{
//合法
if (show[x][y] == '#')
{
printf("该位置已被标记,请重新输入>\n");
}
else if (show[x][y] == ' ')
{
printf("该位置无法被标记,请重新输入>\n");
}
else
{
if (mine[x][y] == '1')
{
(*count)--;
}
show[x][y] = '#';
system("cls");
Displayboard(show, row, col);
print_count(count);
break;
}
}
else
{
//非法
printf("输入非法,请重新输入>");
}
}
}

void unmark_mine(char show[ROWS][COLS],char mine[ROWS][COLS], int row, int col,int* count)
{
int x, y;
while (1)
{
printf("请输入要标记的坐标\n");
printf("(若要退出该操作,请输入0 0)\n>:");
scanf("%d %d", &x, &y);
if (x==0 && y==0)
{
system("cls");
Displayboard(show, ROW, COL);
break;
}
if (x > 0 && x <= row && y > 0 && y <= col)
{
//合法
if (show[x][y] != '#')
{
printf("该位置未被标记,请重新输入>\n");
}
else
{
if (mine[x][y] == '1')
{
(*count)++;
}
show[x][y] = '*';
system("cls");
Displayboard(show, row, col);
print_count(count);
break;
}
}
else
{
//非法
printf("输入非法,请重新输入>\n");
}
}
}

void print_count(int* count)
{
printf("\n剩余雷的数量:%d\n", *count);
}