游戏规则
只有点开所有的非雷的格子,游戏才可取得胜利。即使是找到所有的藏有雷的格子,但没有点击除雷之外的所有的格子,游戏仍在进行中。在此期间,可以在我们认为是雷的格子上插上红旗,来提醒我们此处有雷。知道了游戏规则,那么让我们借助C语言以基础模式为例来编写一个扫雷游戏的代码吧。
设计思路
玩过扫雷游戏的同学肯定都知道,在游戏的开始界面,最先映入眼帘的是一个9*9的棋盘格,左上角是雷的个数提醒,中间的是游戏重新开始的笑脸标志,右边是时间提醒。每点开一个格子,如果是雷则游戏结束;如果不是雷,则会显示周围雷的个数。
所以在编写代码的过程中,我们按照这个思路来开展我们的工作:
- 设置菜单栏,以便玩家知晓什么操作会开始游戏,什么操作会退出游戏。
- 设置游戏的棋盘格(可在棋盘格的上面和左面各加一行/列,以此来提示我们后面操作的格子的位置,即设置一个10x10的棋格盘。但是在统计位于四周的格子的周围雷的数量时,为了能够使用处于其他地方的格子的统计方法,也是为了简便过程,因为设置为11x11的棋盘格)
- 以“1”表示此处有雷,“0”表示此处无雷。但是有雷的标识“1”可能会与周围有几个雷的“1”产生冲突。所以创建2个数组,一个用来存储雷的信息,一个用来向玩家展示游戏界面。
- 初始化数组,一个初始化为“0”来作为对雷的操作的数组,一个初始化为“*”来作为向玩家展示的数组。
- 打印棋盘格,玩家在游戏时可以看到游戏界面。
- 设置雷,在棋盘格中随机的放置10个雷。
- 标记雷,在棋盘格中可以标记10个雷且只可以标记10个雷。若要取消标记,再次标记此位置即可取消。
- 要注意坐标的合法性。
每次操作前,先选择标记or点开。当选择标记时,会对标记的个数进行提示;当选择点开时,会显示雷的信息。
代码展示
1.主函数
int main()
{
int input = 0;
srand((unsigned)time(NULL));
do
{
menu();
printf("请选择:->");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择有误,请重试\n");
break;
}
} while (input);
return 0;
}
2.游戏部分
<1>初始化数组
首先要初始化,不然在打印时可能会打印出一堆乱码。利用for循环使得数组中的内容变为我们想要展示的内容。
因为涉及到两个数组的初始化,所以在参数的设计部分会将要初始化的字符加入其中,用一个函数实现两个数组的初始化,其中一个数组全部初始化为“0”,另一个全部初始化为“*”。
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
<2>打印棋盘
同样利用for循环将数组中的内容打印出来。为了后续可以方便找到格子是位于哪一行哪一列,可以借用我们创建的 i 和 j 来实现列号和行号的展示,其中还打印了“|”和“-”将行列号与棋盘内容分割开。
棋盘部分打印中的中间的9x9部分,最上面打印列号,最左面打印行号,这样使得棋盘具有可读性,便于之后对格子进行一些操作。
void DisPlayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (j = 0; j <= col ; j++)
{
printf("%d ", j);
}
printf("\n");
for (j = 0; j < col; j++)
{
printf("--");
}
printf("-\n");
for (i = 1; i <= row; i++)
{
printf("%d|", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
<3>放置雷
利用rand()函数来产生随机数,达到随机放置雷的目的。因为雷的位置是随机产生的,所以在放置雷的过程中,可能会出现在同一个位置放置多个雷的情况,所以只有当该格子是原始内容即我们初始化的“0”时,才会在此放置雷。
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int a = rand() % row + 1;
int b = rand() % col + 1;
if (mine[a][b] == '0')
{
mine[a][b] = '1';
count--;
}
}
}
<4>标记or点开
在Choose函数中可以输入数字,以实现标记和点开的功能。
利用switch语句来选择是要标记某个位置还是要点开某个位置。创建一个全局变量flag,当游戏失败时,flag为0,此时循环结束。其他情况都会一直循环是要点开还是要标记,直到游戏胜利。
do
{
int ret = Choose();
switch (ret)
{
case 1:
FindMine(mine, show, ROW, COL);//在mine中排查雷,放到show中
break;
case 2:
MarkMine(mine, show, ROW, COL);
break;
default:
printf("选择错误,请重新选择\n");
}
} while (flag);
(1)标记
创建mark变量来计算我们标记的数量,以防超过我们设计的数量。
因为只有十个雷,所以只能标记十次。当达到标记次数时,便不能标记。每次标记结束后,会弹出提示信息。
再次标记已被标记了的位置时,该位置的标记会取消。
void MarkMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
static mark = 0;
printf("请输入要标记的位置:->");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{
if (show[x][y] == '$')
{
printf("此标记已被取消\n");
show[x][y] = '*';
DisPlayBoard(show, row, col);
mark--;
}
else if (show[x][y] == '*')
{
if (mark == EASY_COUNT)
{
printf("你已标记%d次,无法在此标记\n", EASY_COUNT);
}
else
{
show[x][y] = '$';
mark++;
DisPlayBoard(show, row, col);
}
}
else
{
printf("此位置已被排查,不能标记\n");
}
printf("当前已标记%d次,还可再标记%d次\n", mark, EASY_COUNT - mark);
}
else
{
printf("选择的位置超出范围,请重新选择\n");
}
}
(2)点开
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
//输入排查的坐标,检查坐标处是不是雷
//是雷,游戏结束;不是雷,统计周围有几个雷,并存储到show
int x = 0;
int y = 0;
static int win = 0;
printf("请输入要排查的位置:->");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{
if (show[x][y] != '*')
{
printf("此位置不可进行排查!!!请重新选择\n");
}
if (mine[x][y] == '1')
{
printf("很遗憾,此处是雷,游戏结束\n");
DisPlayBoard(mine, row, col);
flag = 0;
}
else
{
unfold(mine, show, row, col, x, y);
DisPlayBoard(show, row, col);
win++;
}
}
else
{
printf("坐标超出范围,请重新选择\n");
}
if (win == row * col - EASY_COUNT)
{
printf("你已排除出所有非雷的位置,");
Win(mine);
}
}
输入要点开的位置,如果是雷,那么游戏结束;如果不是雷,那么显示周围雷的个数。
<5>判定输赢
在上面的点开部分中的FindMine()函数中,创建一个win变量,每点开一次就自加1。因为游戏取得胜利的方法是,点开所有非雷的格子,所以当win的值等于非雷格子的数量时,游戏胜利,这也是为什么我们用static修饰的原因。
优化思路
可以按照真正的扫雷游戏来添加一些功能,使我们的扫雷游戏更有可玩性。
- 确保第一次点开不会点到雷。
- 如果点开的位置的周围雷的个数是0,那么向四面八方展开直到某个格子周围雷的个数不为0。
- 添加计时功能。
- 可以选择重开一局。
完成优化
1.还记得我们之前创建的win变量吗?当我们第一次点击格子时,会第一次调用FindMine()函数,此时win的值是0,所以当win的值为0且点击的格子有雷即第一次点击就点中雷时,要重写放置雷,也就是再次调用SetMine()函数,不过,要注意的是,再次调用ta之前,记得将棋盘格初始化,不然上一次放置的雷还是存在的。在初始化的时候,可以借助我们最开始写的初始化函数。
在这里,我有一个小疑问:既然调用的初始化函数会将会将mine数组全部初始化为0,那么棋盘格的行号和列号不就没了吗?事实上,他们还存在,因为我们展示出来的棋盘格实际上是show数组,这是我们当初创建2个数组的另一个作用。
我们自己在调试第一次点中雷会重新放置雷的时候,会调用DisPlayBoard()函数来观察是否实现这一功能,ta会把mine数组的内容都打印出来,那么此时行号和列号会是“0”吗?实际上,不会,仔细观察我们的DisPlayBoard()函数,行号和列号是按照 i 和 j 来打印的。
while (win == 0 && mine[x][y] == '1')
{
InitBoard(mine, ROWS, COLS, '0');
SetMine(mine, ROW, COL);
}
2.当我们要点开的这个格子不是雷时,会计算它周围的雷的个数。如果周围没有雷,会依次对它周围的8个格子进行上面的操作,直到统计的某个格子周围有雷为止。按照上面说的,我们可以很容易的想到要使用递归来完成这一操作。
【注意】这样递归展开的方式会出现,win的值不等于非雷格子的数量,而,玩家已经点开所有的非雷格子,这一情况。所以对win的处理要有些变化,不然即使点开所有的非雷格子,游戏也不会取得胜利。
void unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
int count = get_mine_count(mine, x, y);
if (count == 0)
show[x][y] = ' ';
else
show[x][y] = count + '0';
if(count == 0)
{
int i = 0;
for (i = -1; i <= 1; i++)
{
int j = 0;
for (j = -1; j <= 1; j++)
{
if (x + i > 0 && x + i <= row && y + j > 0 && y + j <= col && show[x+i][y+j] == '*')
{
unfold(mine, show, row, col, x + i, y + j);
}
}
}
}
}
将count的值传给win,此时当棋盘中的所有非雷位置被点开后,可以认为是游戏胜利。
int IsWin(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int i = 0;
int j = 0;
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (show[i][j] != '*')
count++;
}
}
return count;
}
3.借助time函数,在第一次输入“1”(开始游戏)时有表达式:start=time(NULL),在游戏胜利或失败即game()后有表达式:end=time(NULL),再使用difftime()函数即可得到他们的差值,即我们游戏所用时间。
只有在游戏结束时才会显示游戏用时,这一点与我们实际玩的扫雷有些差距。
4.每次操作前添加一个选项:返回菜单栏。输入数字0,就会跳转到菜单栏,这时可以选择重开游戏或者退出游戏。
switch语句中添加case 0,这样在每次操作前会有3个选择:点开、标记、退出。
写在最后
整个代码可能还有一些部分是可以优化的,但是目前我只想到这些;同时也许有一些地方有错误但是我没有发现,在运行过程中也很凑巧的没有发现ta,等等的其他问题。毕竟有时候“当局者迷”,希望大家可以谅解。