大家好,今天让我们一起来用C语言写一个电脑自带的经典小游戏——扫雷。
程序目标
- 通过代码编写,实现扫雷游戏的基本功能;
- 明白C语言实现游戏功能代码通用写法,掌握模块化编程;
- 锻炼自己的编程能力,学习新知识。
程序设计
界面初始化
以埋了10个炸弹的9×9的布局为例,如图所示
为了实现这个界面我们可以创建一个二维数组,并进行初始化,我就全部初始化为‘*’了,最终的效果如下图
其中上方和左侧的数字对应行列,方便玩家选择排雷点。
设置炸弹
当界面初始化完成后,我们就需要随机设置10个炸弹,炸弹用字符‘1’表示,没有炸弹用字符‘0’表示,这里我们可以通过rand()
函数进行实现,但注意我们还需要在主函数里调用srand()
函数来设置rand()
函数的随机种子。效果如下
注意此处界面的二维数组,并不是我们之前初始化的二维数组,也就是说在这个游戏中我们创建了两个二维数组,用于对外显示进行打印的为数组show[][]
,用于实现设置雷和对雷个数进行判断等功能的为数组mine[][]
。
那么为什么要设置两个二维数组呢?那么假设我们只设置了一个二维数组,那么我们会在这个数组设置炸弹,接下来玩家输入排雷点,这个时候我们肯定需要将数组打印出来,因为需要知道是否踩到了雷以及该坐标周围8个元素还有多少个雷,可是我们就这样打印出来的话,我们前面设置的雷也一样被打印出来了,这就相当于直接告诉了玩家雷设置的位置,这样显然是不行的。
判断炸弹个数
当我们输入排雷点位置后,如果刚好踩到雷,那么游戏就结束,如果没有踩到雷,则需要返回该坐标周围8个元素含有的雷的个数,示意图如下
在编写代码需要注意的是,这里的1、0、3都是字符型,并不是整型。
展开一片功能
在我们玩电脑上的扫雷游戏时,发现当我们第一次输入排雷点的时候,如果该位置不是雷,打印出来的界面可能是这样的
也有可能是这样的
前者使我们前面所设想的正常情况,可后者一下子却展开出了这么一大片区域,这是怎么回事呢?
后者的功能我们就称之为展开一片功能,其具体功能时如何实现的呢,如下图所示
在排雷点没有雷的前提下,排雷点周围8个元素都没有雷,即该坐标返回值为‘0’,为了更符合游戏规则,我们可以令其返回值为空格’ ’,这个时候我们可以再定义一个函数,这个函数实现的功能为当排雷点的返回值为‘0’时,再去判断该坐标周围8个元素,看他们的返回值是否也为‘0’,如果也为‘0’,则再判断这个坐标周围8个元素返回值是否为’0‘,然后再进行判断,一直如此,直至判断坐标返回值不在为’0‘,就停止。这是一个循环的过程,我们可以用递归来实现。
但需要注意的是,在上面这种情况下,往往一个坐标位置会被递归许多次,也就是会出现死递归。为了防止这种情况的出现,提高代码运行效率,我们就需要再写一个函数来实现判断这个坐标位置是不是已经被判断过了,如果已经判断过了,则不在进行判断,没判断过,则继续判断。
炸弹位置标记
在我们玩扫雷游戏的过程中,我们发现它还有一个功能,那就是当我们判断某一个位置为炸弹时,可以进行标记,在本代码中,标记为’0‘,有标记也就有取消,这样我们就实现了炸弹位置的标记和取消标记。
排雷成功
最后我们还需要实现判断排雷成功的功能,我们一共有10个雷需要排,在我们排雷的过程中,当我们知道某一个坐标为雷的时候,为了排雷成功,我们只可能作出两种选择:1.标记雷的位置;2.不去踩这个雷。在前面我们说了标记雷位置的符号为’0‘,如果我们不去踩这个雷,那么最后剩下的也就是我们初始化的字符’*‘。
所以我们可以作出如下判断,当用于显示的show[][]
二维数组中’0‘与’*'的个数之和为10,则说明我们排雷成功。
代码编写
我一共创建了三个文件来实现扫雷游戏的功能,分别是main.c
、game.c
、game.h
,即两个源文件一个头文件。
main.c
文件
首先创建一个main()
函数,用来实现开始游戏的基本功能。
int main()
{
srand((unsigned int)time(NULL));//设置rand()函数随机数种子
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("非法输入,请重新选择\n");
break;
}
} while (input != 0);
return 0;
}
menu()
函数实现打印游戏开始菜单的功能
void menu(void)
{
printf("*********扫雷游戏*********\n");
printf("**** 1.start 0.exit ****\n");
printf("**************************\n");
}
game()
函数实现游戏的主体功能
void game(void)
{
char hide[ROWS][COLS] = { 0 };//用来存放雷,不打印显示
char show[ROWS][COLS] = { 0 };//用来打印,将排雷结果显示在玩家面前
char sign[ROWS][COLS] = { 0 };//再死递归中,用来判断坐标点是否已经被排查过,不打印显示
//初始化
InitHide(hide, ROWS, COLS);
InitShow(show, ROWS, COLS);
InitSign(sign, ROWS, COLS);
//打印初始化棋盘
PrintBoard(show, ROWS, COLS);
//随机设置炸弹的位置
SetMine(hide, ROWS, COLS);
while (1)
{
int count = 0;//初始化炸弹个数
int ret = 0;//初始化排雷点周围8个元素含炸弹个数
//初始化排雷点坐标
int x = 0;
int y = 0;
printf("请输入排雷点:>");
scanf("%d %d", &x, &y);
if ((x > 0) && (x < ROWS - 1) && (y > 0) && (y < COLS - 1))//判断输入坐标是否合法
{
if (hide[x][y] == '1')//判断排雷点是否有炸弹
{
PrintBoard(hide, ROWS, COLS);//打印炸弹位置
printf("炸弹爆炸,游戏结束\n");
break;
}
else
{
if (show[x][y] == '*')//判断输入排雷点是否已经被占用
{
ret = MineCount(hide, x, y);//返回坐标点周围8个元素的炸弹个数
if (ret == 0)
{
//实现一片展开功能
SpreadBoard(hide, show, sign, ROWS, COLS, x, y);
}
else
{
show[x][y] = '0' + ret;//数组show是char类型的,48对应的字符为'0'
}
PrintBoard(show, ROWS, COLS);
}
else
{
printf("该位置已被选择,请重新输入\n");
}
}
}
else
{
printf("非法输入,请重新选择\n");
}
MarkBomb(show, ROWS, COLS);//标记或者取消标记炸弹位置
//判断是否排雷成功
count = IsWin(show, ROWS, COLS);
if (count == 10)
{
printf("恭喜你,排雷成功\n");
break;
}
}
}
game.c
文件
二维数组初始化
void InitBoard(char board[ROWS][COLS], int row, int col, char c)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = c;
}
}
}
打印数组
void PrintBoard(char Board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf(" ");//在第一行先提前打印一个空格,保证与接下来每列都对齐
for (i = 1; i < row - 1; i++)
{
printf(" %d", i);//在第一行打印序号
}
printf("\n");
for (i = 1; i < row - 1; i++)
{
printf("%d ", i);//在每一列开头打印序号
for (j = 1; j < col - 1; j++)
{
printf("%c ", Board[i][j]);//打印二维数组中每一个元素
}
printf("\n");
}
}
随机设置炸弹位置
void SetMine(char hide[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int i = 0;
for (i = 0; i < MINECOUNTS; i++)
{
x = (rand() % ROW) + 1;//横坐标为1~9
y = (rand() % COL) + 1;//纵坐标为1~9
if (hide[x][y] == '0')//防止随机设置的炸弹位置坐标重复
{
hide[x][y] = '1';
continue;
}
i--;
}
}
数排雷点周围8个元素炸弹个数
int MineCount(char hide[ROWS][COLS], int a, int b)
{
int count = 0;
int i = 0;
int j = 0;
//遍历该坐标周围8个元素,计算炸弹个数
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (hide[a + i][b + j] == '1')
{
count++;
}
}
}
return count;
}
一片展开功能实现
//标记已经判断过的排雷点,防止死递归
char Mark(char sign[ROWS][COLS], int e, int f)
{
if (sign[e][f] == ' ')//如果没有被判断过,先进行标记,标记为'#',再返回' '
{
sign[e][f] = '#';
return ' ';
}
if (sign[e][f] == '#')//如果已经判断过了,则返回'#'
{
return '#';
}
}
void SpreadBoard(char hide[ROWS][COLS], char show[ROWS][COLS], char sign[ROWS][COLS], int row, int col, int c, int d)
{
int i = 0;
int j = 0;
int ret = 0;
if (((c > 0) && (c < row - 1) && (d > 0) && (d < row - 1)) && (MineCount(hide, c, d) == 0))//限制条件:1.在二维数组边界范围内;2.排雷点周围8个元素炸弹数为0
{
//Mark(sign, c, d)函数标记已经判断过的排雷点,标记记号为'#'
if (Mark(sign, c, d) != '#')//判断该位置是否已经被标记过了,防止死递归
{
show[c][d] = ' ';//赋予空格
//重复判断排雷点周围8个元素,即开始“递”
SpreadBoard(hide, show, sign, row, col, c - 1, d - 1);
SpreadBoard(hide, show, sign, row, col, c - 1, d);
SpreadBoard(hide, show, sign, row, col, c - 1, d + 1);
SpreadBoard(hide, show, sign, row, col, c, d - 1);
SpreadBoard(hide, show, sign, row, col, c, d + 1);
SpreadBoard(hide, show, sign, row, col, c + 1, d - 1);
SpreadBoard(hide, show, sign, row, col, c + 1, d);
SpreadBoard(hide, show, sign, row, col, c + 1, d + 1);
}
}
else
{
show[c][d] = '0' + MineCount(hide, c, d);//打印数字字符,开始“归”
}
}
标记炸弹点位置
void Cancelmark(char show[ROWS][COLS], int row, int col)
{
int input = 0;
do
{
printf("是否取消炸弹标记,是-1,否-0:>");
scanf("%d", &input);
switch(input)
{
case 1:
printf("请输入取消标记炸弹点坐标:>");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
show[x][y] = '*';//取消炸弹点标记,重新标为'*'
PrintBoard(show, row, col);
break;
case 0:
break;
default:
printf("错误选择,请重新输入\n");
break;
}
} while (input != 0);
}
void MarkBomb(char show[ROWS][COLS], int row, int col)
{
int input = 0;
int count = 0;
do
{
//选择是否发现炸弹
printf("是否发现炸弹,是-1,否-0:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入炸弹坐标点:>");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
show[x][y] = '0';//发现炸弹并标记炸弹位置为字符'0'
PrintBoard(show, row, col);
count++;
//当标记炸弹数大于等于1时,选择是否取消炸弹标记点
if (count >= 1)
{
Cancelmark(show, row, col);
}
break;
case 0:
break;
default:
printf("选择错误,请重新输入\n");
break;
}
} while (input != 0);
}
判断排雷成功
int IsWin(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int i = 0;
int j = 0;
for (i = 1; i < row - 1; i++)
{
for (j = 1; j < col - 1; j++)
{
if (show[i][j] == '0')//如果有炸弹标记符号,则count++
{
count++;
}
if (show[i][j] == '*')//如果有'*'符号,则count++
{
count++;
}
}
}
return count;
}
程序运行结果
附录
main.c
代码
#include "game.h"
void menu(void)
{
printf("*********扫雷游戏*********\n");
printf("**** 1.start 0.exit ****\n");
printf("**************************\n");
}
void game(void)
{
char hide[ROWS][COLS] = { 0 };//用来存放雷,不打印显示
char show[ROWS][COLS] = { 0 };//用来打印,将排雷结果显示在玩家面前
char sign[ROWS][COLS] = { 0 };//再死递归中,用来判断坐标点是否已经被排查过,不打印显示
//初始化
InitBoard(hide, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
InitBoard(sign, ROWS, COLS, ' ');
//打印初始化棋盘
PrintBoard(show, ROWS, COLS);
//随机设置炸弹的位置
SetMine(hide, ROWS, COLS);
while (1)
{
int count = 0;//初始化炸弹个数
int ret = 0;//初始化排雷点周围8个元素含炸弹个数
//初始化排雷点坐标
int x = 0;
int y = 0;
printf("请输入排雷点:>");
scanf("%d %d", &x, &y);
if ((x > 0) && (x < ROWS - 1) && (y > 0) && (y < COLS - 1))//判断输入坐标是否合法
{
if (hide[x][y] == '1')//判断排雷点是否有炸弹
{
PrintBoard(hide, ROWS, COLS);//打印炸弹位置
printf("炸弹爆炸,游戏结束\n");
break;
}
else
{
if (show[x][y] == '*')//判断输入排雷点是否已经被占用
{
ret = MineCount(hide, x, y);//返回坐标点周围8个元素的炸弹个数
if (ret == 0)
{
//实现一片展开功能
SpreadBoard(hide, show, sign, ROWS, COLS, x, y);
}
else
{
show[x][y] = '0' + ret;//数组show是char类型的,48对应的字符为'0'
}
PrintBoard(show, ROWS, COLS);
}
else
{
printf("该位置已被选择,请重新输入\n");
}
}
}
else
{
printf("非法输入,请重新选择\n");
}
MarkBomb(show, ROWS, COLS);//标记或者取消标记炸弹位置
//判断是否排雷成功
count = IsWin(show, ROWS, COLS);
if (count == 10)
{
printf("恭喜你,排雷成功\n");
break;
}
}
}
int main()
{
srand((unsigned int)time(NULL));//设置rand()函数随机数种子
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("非法输入,请重新选择\n");
break;
}
} while (input != 0);
return 0;
}
game.c
代码
#include "game.h"
void InitBoard(char board[ROWS][COLS], int row, int col, char c)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = c;
}
}
}
void PrintBoard(char Board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf(" ");//在第一行先提前打印一个空格,保证与接下来每列都对齐
for (i = 1; i < row - 1; i++)
{
printf(" %d", i);//在第一行打印序号
}
printf("\n");
for (i = 1; i < row - 1; i++)
{
printf("%d ", i);//在每一列开头打印序号
for (j = 1; j < col - 1; j++)
{
printf("%c ", Board[i][j]);//打印二维数组中每一个元素
}
printf("\n");
}
}
void SetMine(char hide[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int i = 0;
for (i = 0; i < MINECOUNTS; i++)
{
x = (rand() % ROW) + 1;//横坐标为1~9
y = (rand() % COL) + 1;//纵坐标为1~9
if (hide[x][y] == '0')//防止随机设置的炸弹位置坐标重复
{
hide[x][y] = '1';
continue;
}
i--;
}
}
int MineCount(char hide[ROWS][COLS], int a, int b)
{
int count = 0;
int i = 0;
int j = 0;
//遍历该坐标周围8个元素,计算炸弹个数
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (hide[a + i][b + j] == '1')
{
count++;
}
}
}
return count;
}
//标记已经判断过的排雷点,防止死递归
char Mark(char sign[ROWS][COLS], int e, int f)
{
if (sign[e][f] == ' ')//如果没有被判断过,先进行标记,标记为'#',再返回' '
{
sign[e][f] = '#';
return ' ';
}
if (sign[e][f] == '#')//如果已经判断过了,则返回'#'
{
return '#';
}
}
void SpreadBoard(char hide[ROWS][COLS], char show[ROWS][COLS], char sign[ROWS][COLS], int row, int col, int c, int d)
{
int i = 0;
int j = 0;
int ret = 0;
if (((c > 0) && (c < row - 1) && (d > 0) && (d < row - 1)) && (MineCount(hide, c, d) == 0))//限制条件:1.在二维数组边界范围内;2.排雷点周围8个元素炸弹数为0
{
//Mark(sign, c, d)函数标记已经判断过的排雷点,标记记号为'#'
if (Mark(sign, c, d) != '#')//判断该位置是否已经被标记过了,防止死递归
{
show[c][d] = ' ';//赋予空格
//重复判断排雷点周围8个元素,即开始“递”
SpreadBoard(hide, show, sign, row, col, c - 1, d - 1);
SpreadBoard(hide, show, sign, row, col, c - 1, d);
SpreadBoard(hide, show, sign, row, col, c - 1, d + 1);
SpreadBoard(hide, show, sign, row, col, c, d - 1);
SpreadBoard(hide, show, sign, row, col, c, d + 1);
SpreadBoard(hide, show, sign, row, col, c + 1, d - 1);
SpreadBoard(hide, show, sign, row, col, c + 1, d);
SpreadBoard(hide, show, sign, row, col, c + 1, d + 1);
}
}
else
{
show[c][d] = '0' + MineCount(hide, c, d);//打印数字字符,开始“归”
}
}
void Cancelmark(char show[ROWS][COLS], int row, int col)
{
int input = 0;
do
{
printf("是否取消炸弹标记,是-1,否-0:>");
scanf("%d", &input);
switch(input)
{
case 1:
printf("请输入取消标记炸弹点坐标:>");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
show[x][y] = '*';//取消炸弹点标记,重新标为'*'
PrintBoard(show, row, col);
break;
case 0:
break;
default:
printf("错误选择,请重新输入\n");
break;
}
} while (input != 0);
}
void MarkBomb(char show[ROWS][COLS], int row, int col)
{
int input = 0;
int count = 0;
do
{
//选择是否发现炸弹
printf("是否发现炸弹,是-1,否-0:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入炸弹坐标点:>");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);
show[x][y] = '0';//发现炸弹并标记炸弹位置为字符'0'
PrintBoard(show, row, col);
count++;
//当标记炸弹数大于等于1时,选择是否取消炸弹标记点
if (count >= 1)
{
Cancelmark(show, row, col);
}
break;
case 0:
break;
default:
printf("选择错误,请重新输入\n");
break;
}
} while (input != 0);
}
int IsWin(char show[ROWS][COLS], int row, int col)
{
int count = 0;
int i = 0;
int j = 0;
for (i = 1; i < row - 1; i++)
{
for (j = 1; j < col - 1; j++)
{
if (show[i][j] == '0')//如果有炸弹标记符号,则count++
{
count++;
}
if (show[i][j] == '*')//如果有'*'符号,则count++
{
count++;
}
}
}
return count;
}
game.h
代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define MINECOUNTS 10
void InitBoard(char board[ROWS][COLS], int row, int col, char c);
void SetMine(char hide[ROWS][COLS], int row, int col);
void PrintBoard(char Board[ROWS][COLS], int row, int col);
int MineCount(char hide[ROWS][COLS], int a, int b);
void SpreadBoard(char hide[ROWS][COLS], char show[ROWS][COLS], char sign[ROWS][COLS], int row, int col, int c, int d);
void MarkBomb(char show[ROWS][COLS], int row, int col);
int IsWin(char show[ROWS][COLS], int row, int col);