目录
前言
想必大家在上学时代都或多或少玩过扫雷游戏,通过棋盘上已有的数据分析出雷所在的位置获取胜利,每一次游戏都带给我们强大的满足感与能力的提升。那么今天我就带领大家用C语言实现扫雷的编写。
let's go!
一.设计思路
基本的构思方向
首先,扫雷游戏结局包含两种情况
1.标记所有雷或掀开所有非雷地块
2.踩到雷,游戏结束
基本玩法:
掀开一个格子,如果是雷,游戏结束。
反之,显示周围有多少个雷。
如何达到上图的效果呢?
首先我们需要定义一个二维数组用于表示棋盘,我们在其上布置“地雷”。
假设我们用'0'表示安全,'1'表示雷的话,那么统计周围雷数就变得简单了:雷数(n)=周围八个格子的数值的和。
但这时候我们又会遇见一个问题:
如果一个地方被点开了,显示了其周围的总雷数(例如:4),在下次点开它周围格子时,就不免错误地将这个并不表示雷的'4'加上。就会造成bug
那么,设置表里两层棋盘或许是一个更好的选择:内层设置'雷',外层读取内层数据展示给用户显示周围雷数。
ps:下文中show/board2是外棋盘,mine/board1是雷棋盘。
之后,当用户选择外棋盘的坐标时,只需要统计内棋盘周围雷数就行了。
但这时候我们又会遇见另一个问题:在数组边界的格子并没有八个周围的格子
如何解决这个问题呢?我们可以把两个数组都开大一圈,这样访问时,排查边界的数据就不会越界了。eg:9*9的期望棋盘,我们开辟成11*11的棋盘,刚好大一圈。
准备基本框架
首先基于与用户的交互功能,需要以下功能
菜单界面:
用户在此选择开始游戏,结束游戏,输入非法提示重输。
显示棋盘:
未点开的地块统一用'?'表示,点开的地块用空格表示,如果周围有雷,显示'雷'数。
设置雷:
在游戏开始时用时间戳随机生成一定数目的雷。
排查雷
用户输入想排查的地块坐标,判定游戏状况:踩雷,安全
计数雷
用户输入想排查的地块坐标,游戏状况安全,计算周围八个格子的数
胜利,失败判定:
当踩雷时结束游戏,并显示菜单界面
好,现在进入正文——函数功能设置!
二.函数功能设置
代码的功能会在代码段进行解释
菜单界面
达到提示输入的效果就行
eg:
void menu() { printf("*******************************\n"); printf("***** 1. play 0. exit *****\n"); printf("*******************************\n"); }
主函数
因为我们玩游戏的时候希望的可能并非只是玩一局,这个时候就需要在游戏函数(game)上添加一个do while()循环!
void menu()
{
printf("*******************************\n");
printf("***** 1. play 0. exit *****\n");
printf("*******************************\n");
}
void text()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
system("cls");
switch (input)
{
case 1:
printf("游戏开始\n");
game();
Sleep(1000);
system("cls");
break;
case 0:
printf("退出游戏\n");
break;
default :
printf("输入非法,请重输!\n");
break;
}
} while (input);
}
ps:在这里,我们选择使用switch case语句以达到多分枝的目的
初始化
在这里我们设置棋盘的初始状态,使其元素统一地表现。
initboard(board1, ROW, COL, '0');//mine
initboard(board2, ROW, COL, '?');//show
由于棋盘初始化本质时将数组元素换成'0'或'?',是一个重复单调的过程,索性就把它封装成一个函数。以char set来确定替换元素。
通过双层循环遍历替换数组元素(set):
void initboard(char board[ROWS][COLS], int row, int col,char set)//初始化show棋盘和mine棋盘,set,传什么打印什么
{
int i = 0;
int j = 0;
for (i = 0; i < row+2; i++)
{
for (j = 0; j < col+2; j++)
{
board[i][j] = set;
}
}
}
函数调用过程:
char board1[ROWS][COLS] = { 0 };
char board2[ROWS][COLS] = { 0 };
显示棋盘
我们玩游戏显然不可能只是我们凭意识玩,所以就需要对每一步进行及时反馈(显示外棋盘到电脑)——对棋盘(不论内外棋盘)进行遍历打印。 display(board2, ROW, COL);
void display(char board[ROWS][COLS], int row, int col)//打印棋盘,传什么打印什么
{
int i = 0;
int j = 0;
for (j = 0; j < col + 1; j++)//打印数轴,便于用户输入坐标
{
printf(" %d", j);
}
printf("\n");
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");
printf("\n\n");//留出空格
}
}
ps:11 20行主要是为了保持排版美观,打印效果如下 相当美观
设置雷
我们需要在mine数组里布置雷。 setmine(board1, ROW, COL);
利用一个循环,每次随机生成一个坐标,如果这个位置不是雷,就在这个位置放雷,直到把所有的雷放完为止。
这一步就比较简单了,但随机数涉及到一个时间戳的概念,我在此不再赘述。
主函数部分要设置一个随机数种子(不需要理解)
void setmine(char board[ROWS][COLS], int row, int col)//埋雷
{
int count = easy_mine;
while(count>0)
{
/*rand()函数的意思是生成任意大小的一个数,
我们使用%row对row取余后,就限制到了0到row-1之间了,
为了用户方便输入又进行了+1*/
int i = rand() % row + 1;//雷的横坐标在[0,row]之间
int j = rand() % col + 1;//同理
if (board[i][j] == '0')//只有未被埋雷的地方才能埋雷;
{
count--;
board[i][j] = '1';
}
}
}
#include <time.h>//部分编译器需要加
int main()
{
srand((unsigned int)time(NULL));//设置由时间设置的随机数种子
text();
}
计算周围雷数
调用count_mine函数计算周围雷数(可以包涵自己格子)设当前格子坐标[i,j]
只需要双层循环遍历行数在[i-1,i+1],列数在[j-1,j+1]的数,计算就行
int count_mine(char mine[ROWS][COLS], int i, int j)//数包括自己在内的九个格子的雷数
{
int a = 0;
int b = 0;
int count = 0;
for (a = i - 1; a <= i + 1; a++)
{
for (b = j - 1; b <= j + 1; b++)
{
if(a> 0 && b > 0 && a < ROW + 1 && b < COL + 1)
count =mine[a][b] - '0'+count;//'1'-'0'=1;
}
}
return count;
}
我看很多博主选择用字符1减去字符0来统计,不经循环直接遍历上八个格子,我只能说:
确实可以,但没必要!
直接++也未免不可!
int count_mine(char mine[ROWS][COLS], int i, int j)//数包括自己在内的九个格子的雷数
{
int a = 0;
int b = 0;
int count = 0;
for (a = i - 1; a <= i + 1; a++)
{
for (b = j - 1; b <= j + 1; b++)
{
if(mine[a][b]='1')
count ++;
}
}
return count;
}
排雷
ok!今天的重头戏来了,
布置好雷后,就开始排雷。排查雷需要同时操作两个数组。
int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)//排雷
用户输入坐标,同时操作两个数组的判断条件
if (i > 0 && i < row + 1 && j>0 && j < col + 1 && show[i][j] == '?')//检验坐标合法性
//排查过的都不能排查
只有满足条件才进入排雷的环境中
1.无雷,调用count函数计算周围八个格子雷数
2.有雷,提示炸死,结束game返回主菜单
if (show[i][j] == '?' && mine[i][j] == '0')
{
show[i][j]=count_mine(mine,i,j);
system("cls");//清除上一步
display(show, ROW, COL);
break;
}
else if (show[i][j] == '?' && mine[i][j] == '1')
{
step++;
system("cls");
printf("很遗憾,你被炸死了\n");
printf("雷阵如下\n");
display(mine, ROW, COL);
break;
}
else if (show[i][j] != '?')
printf("坐标非法,请重输\n");
在这里,我们还可以添加一个获胜的判断条件
设置一个win=行和列的乘积,每排一个地块,win--,
当win=row*col-easy_mine,即等于所有空地块时,获胜
即,代码如下
int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
int win = row*col;
while (win> row * col - easy_mine)
{
again:
printf("请输入坐标:>");
scanf_s("%d %d", &i, &j);
if (i > 0 && i < row + 1 && j>0 && j < col + 1 && show[i][j] == '?')
{//坐标合法
if (mine[i][j] == '1')
{//排雷失败
printf("很遗憾,你被炸死了!\n");
display(mine, ROW, COL);
break;
}
else
{//排雷成功
int count = count_mine(mine, i, j);
show[i][j] = count + '0';
display(show, ROW, COL);
for (int m = 1; m < row+1 ; m++)//放在“扫雷”工作之后
{
for (int n = 1; n < col+1; n++)//从一开始
{
if (show[m][n] == '?')
win--;
}
}
}
else if (show[i][j] != '?')
{
printf("输入非法,请重输\n");
goto again;
}
}
if (win == row * col - easy_mine)//
{
printf("你赢了!\n");
return 1;
}
}
为了是屏幕干净引入了清屏函数 system("cls");注意头文件。
system("cls");//其头文件是#include<windows.h>,用于清空屏幕,简化画面
排雷是一个很多步骤的一个操作 ,为search_mine函数加上死循环保证其可以一直进行排雷操作,并且以p接受该函数的返回值:
一旦返回值为1(排雷失败),跳出循环,game函数结束,显示主界面。
总体game函数
void game()
{
char board1[ROWS][COLS] = { 0 };
char board2[ROWS][COLS] = { 0 };
initboard(board1, ROW, COL, '0');//mine
initboard(board2, ROW, COL, '?');//show
display(board2, ROW, COL);
setmine(board1, ROW, COL);
//display(board1, ROW, COL);//用于debug
while (1)
{
int p = search_mine(board1, board2, ROW, COL);
if (p == 1)
break;
}
}
综上,最终代码如下
#include <iostream>
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define easy_mine 10
#define mid_mine 30
#define dif_mine 50
#define debug_mine 80
#define debug+_mine 0
void display(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (j = 0; j < col + 1; j++)
{
printf(" %d", j);
}
printf("\n");
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");
printf("\n\n");
}
}
int count_mine(char mine[ROWS][COLS], int i, int j)
{
int a = 0;
int b = 0;
int count = 0;
for (a = i - 1; a <= i + 1; a++)
{
for (b = j - 1; b <= j + 1; b++)
{
if(mine[i][j]=='1')
count++;
}
}
return count;
}
int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
int win = row*col;
while (win> row * col - easy_mine)
{
again:
printf("请输入坐标:>");
scanf_s("%d %d", &i, &j);
if (i > 0 && i < row + 1 && j>0 && j < col + 1 && show[i][j] == '?')
{//坐标合法
if (mine[i][j] == '1')
{//排雷失败
printf("很遗憾,你被炸死了!\n");
display(mine, ROW, COL);
break;
}
else
{//排雷成功
int count = count_mine(mine, i, j);
show[i][j] = count + '0';
display(show, ROW, COL);
for (int m = 1; m < row+1 ; m++)//放在“扫雷”工作之后
{
for (int n = 1; n < col+1; n++)//从一开始
{
if (show[m][n] == '?')
win--;
}
}
}
else if (show[i][j] != '?')
{
printf("输入非法,请重输\n");
goto again;
}
}
if (win == row * col - easy_mine)//
{
printf("你赢了!\n");
return 1;
}
}
void setmine(char board[ROWS][COLS], int row, int col)
{
int count = easy_mine;
while(count>0)
{
again:
int i = rand() % 9 + 1;
int j = rand() % 9 + 1;
if (board[i][j] == '0')
{
board[i][j] = '1';
}
else//如果没有else;if条件不满足;也会count--;
goto again;
count--;
}
}
void inboard(char board[ROWS][COLS], int row, int col,char set)
{
int i = 0;
int j = 0;
for (i = 0; i < row+2; i++)
{
for (j = 0; j < col+2; j++)
{
board[i][j] = set;
}
}
}
void game()
{char board1[ROWS][COLS] = { 0 };
char board2[ROWS][COLS] = { 0 };
inboard(board1, ROW, COL, '0');
inboard(board2, ROW, COL, '?');
display(board2, ROW, COL);
setmine(board1, ROW, COL);
while (1)
{
//display(board1, ROW, COL);//用于调试
int r= search_mine(board1, board2, ROW, COL);
if (r == 1)
break;
}
}
void menu()
{
printf("*******************************\n");
printf("***** 1. play 0. exit *****\n");
printf("*******************************\n");
}
void text()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
system("cls");
printf("扫雷\n");
switch (input)
{
case 1:
printf("游戏开始\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default :
printf("输入非法,请重输!\n");
break;
}
} while (input);
}
int main()
{
srand((unsigned int)time(NULL));
text();
}
后续优化
一个完整的扫雷当然不止于此,还应当有以下功能:(还有一些小优化也加在里面了)
1.标记雷
比较简单的代码,在终极代码中加入了。
2.递归展开
即,一点棋盘开一大片的功能。
ps:利用递归,这里不再赘述。
void expendboard(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j)//进行空白展开
{
int a = 0;
int b = 0;
int count = count_mine(mine, i, j);//计算周围雷数
if (count == 0)//如果没雷,进去
{
show[i][j] = ' ';
for (a = i - 1; a <= i + 1; a++)
{
for (b = j - 1; b <= j + 1; b++)
{
if (show[a][b]=='?'&&(mine[a][b]!='1'||show[a][b]=='*'))
//并且展开到出现数字为止,而非展到雷为止
expendboard(mine, show, a, b);//递归:连续展开
}
}
}
else
{
show[i][j] = count + '0'; //显示雷数
}
}
3.防止第一次踩雷
如果是上面的伪完整代码的话,如果运气不好,第一次就能被炸死。
所以我们对其也要做相应优化
基本原理就是,设置雷以后,不管你第一次有没有踩到雷都是没有雷:
当你踩雷以后,不提示你被炸死,而是把这颗雷换走,再随机找一个地方埋下
if (step == 1 && show[i][j] == '?' && mine[i][j] == '1')
{
mine[i][j] = '0';
while (1)
{
int a = rand() % 9 + 1;
int b = rand() % 9 + 1;
if ((a != i || b != j) && mine[a][b] == '0')
{
mine[a][b] = '1';
break;
}
}
}
4.惩罚功能
游戏输了,怎么能没有惩罚呢?
送你一个关机小代码,回来可以加载到完整函数上
void shut()
{
char getput[20] = {0};
system("shutdown -s -t 60");
while (1)
{
again:
system("cls");
printf("\n\t\t\t再一再而不再三!!!\n");
printf("请注意,你的电脑将在60秒内关机,输入“哥,我再也不敢了”取消关机:>\n");
scanf_s("%s", getput, (unsigned int)sizeof(getput));
if (strcmp(getput, "哥,我再也不敢了") == 0)
{
system("shutdown -a");
system("cls");
break;
}
else
{
goto again;
}
}
}
5.最终代码
#include <iostream>
#include<windows.h>
#include <synchapi.h>
#define ROWS 11
#define COLS 11
#define ROW 9
#define COL 9
#define easy_mine 10
int record = 0;
void display(char board[ROWS][COLS], int row, int col)//打印棋盘,传什么打印什么
{
int i = 0;
int j = 0;
for (j = 0; j < col + 1; j++)
{
printf(" %d", j);
}
printf("\n");
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");
printf("\n\n");
}
}
int count_mine(char mine[ROWS][COLS], int i, int j)//数包括自己在内的九个格子的雷数
{
int a = 0;
int b = 0;
int count = 0;
for (a = i - 1; a <= i + 1; a++)
{
for (b = j - 1; b <= j + 1; b++)
{
// if(a> 0 && b > 0 && a < ROW + 1 && b < COL + 1)
count =mine[a][b] - '0'+count;//'1'-'0'=1;
}
}
return count;
}
void expendboard(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j)//进行空白展开
{
int a = 0;
int b = 0;
int count = count_mine(mine, i, j);
if (count == 0)
{
show[i][j] = ' ';
for (a = i - 1; a <= i + 1; a++)
{
for (b = j - 1; b <= j + 1; b++)
{
if (a >0 && b>0&&a<ROW+1&&b<COL+1 &&show[a][b]=='?'&&(mine[a][b]!='1'||show[a][b]=='*'))
expendboard(mine, show, a, b);//递归:连续展开
}
}
}
else
{
show[i][j] = count + '0'; //显示雷数
}
}
int search_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)//排雷
{
int i = 0;
int j = 0;
int win = 0;
int input = 0;
int mark = 0;
int step = 0;
while (win <easy_mine)//标记临界值
{
printf("********************\n");
printf("******* 1.标记 *****\n");
printf("******* 2.查看 *****\n");
printf("********************\n");
printf("\n\t\t\t\t\t已标记:%d\n", win);//显示标记个数
printf("\n\t\t\t\t\t排雷步数:%d\n", step);//步数
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
printf("选择要标记的地块坐标(再次标记后取消):>");
scanf_s("%d %d", &i, &j);
if (show[i][j] == '?' && mark <= easy_mine)
{
show[i][j] = '*';
mark++;
if (mine[i][j] == '1')
win++;
system("cls");//清除上一步
display(show, ROW, COL);
}
else if (mark > 10)
printf("标记已满,请取消\n");
else if (show[i][j] == '*')
{
show[i][j] = '?';
mark--;
if (mine[i][j] == '1')//正确的雷
win--;
display(show, ROW, COL);
}
break;
case 2:
while (1)
{
step++;
printf("选择要查看的地块坐标:>");
scanf_s("%d %d",& i, &j);
if (step == 1 && show[i][j] == '?' && mine[i][j] == '1')
{
mine[i][j] = '0';
while (1)
{
int a = rand() % 9 + 1;
int b = rand() % 9 + 1;
if ((a != i || b != j) && mine[a][b] == '0')
{
mine[a][b] = '1';
break;
}
}
}
if (show[i][j] == '?' && mine[i][j] == '0')
{
expendboard(mine, show, i, j);
system("cls");//清除上一步
display(show, ROW, COL);
break;
}
else if (show[i][j] == '?' && mine[i][j] == '1')
{
step++;
system("cls");
printf("很遗憾,你被炸死了\n");
printf("雷阵如下\n");
display(mine, ROW, COL);
printf("\n\t\t\t排雷步数:%d\n", step);//步数
break;
}
else if (show[i][j] != '?')
printf("坐标非法,请重输\n");
}
break;
default:
system("cls");
display(show, ROW, COL);
printf("输入非法,请重输\n");
break;
}
if(mine[i][j]=='1'&&input==2)
break;
if (win == easy_mine||step==col*row-easy_mine)
{
system("cls");
display(show, ROW, COL);//展示成果
printf("排雷成功\n");
printf("\n\t\t\t排雷步数:%d\n", step);//步数
if ((record != 0 && record > step) || record == 0)
record = step;
system("pause");
break;
}
}
return 1;
}
void setmine(char board[ROWS][COLS], int row, int col)//埋雷
{
int count = easy_mine;
while(count>0)
{
int i = rand() % 9 + 1;
int j = rand() % 9 + 1;
if (board[i][j] == '0')
{
count--;
board[i][j] = '1';
}
}
}
void initboard(char board[ROWS][COLS], int row, int col,char set)//初始化show棋盘和mine棋盘,set,传什么打印什么
{
int i = 0;
int j = 0;
for (i = 0; i < row+2; i++)
{
for (j = 0; j < col+2; j++)
{
board[i][j] = set;
}
}
}
void game()
{
char board1[ROWS][COLS] = { 0 };
char board2[ROWS][COLS] = { 0 };
initboard(board1, ROW, COL, '0');//mine
initboard(board2, ROW, COL, '?');//show
display(board2, ROW, COL);
setmine(board1, ROW, COL);
display(board1, ROW, COL);//用于debug
while (1)
{
int p = search_mine(board1, board2, ROW, COL);
if (p == 1)
break;
}
}
void menu()
{
printf("*******************************\n");
printf("***** 1. play 0. exit *****\n");
printf("*******************************\n");
}
void text()
{
int input = 0;
do
{
printf("\n\t\t\t扫雷\n");
printf("\t\t\t\t\t最好记录:%d\n",record);
menu();
printf("请选择:>");
scanf_s("%d", &input);
system("cls");
switch (input)
{
case 1:
printf("游戏开始\n");
game();
Sleep(1000);
system("cls");
break;
case 0:
printf("退出游戏\n");
break;
default :
printf("输入非法,请重输!\n");
break;
}
} while (input);
}
int main()
{
srand((unsigned int)time(NULL));//设置由时间设置的随机数种子
text();
}
总结
一万两千多字,码了四个多小时,求三连!