1、扫雷游戏简介
如上所示是一个九成九的初阶版扫雷棋盘,棋盘上八十一个格子共有十处埋有地雷。当在(x,y)处有地雷,玩家被炸死,游戏结束,(x,y)处没有地雷,(x,y)处会显示其周围八个坐标雷的信息,假设周围八处共有三处埋有地雷,则在(x,y)处显示3。
2、游戏实现流程
- 扫雷游戏棋盘的设计
- 棋盘的初始化
- 生成雷埋于棋盘中
- 打印棋盘
- 玩家找雷且输赢判定
3、游戏棋盘的设计
1、在九成九的棋盘中下棋,无非就是在二维数组中进行存放元素。char board [9][9] = { 0 };
2、设定将一块棋盘上埋雷的信息存放字符1,无雷的信息存放字符0(就是在字符数组中存放字符1与字符0)。
3、根据游戏规则,当一个坐标处无雷时,会在该坐标上显示周围八个坐标的信息,假设当周围有一个雷时,该坐标显示1,如何区分这个1是此坐标埋的雷的信息,还是此处坐标无雷,显示了周围八个坐标雷的信息。为了解决此问题,我们可以创建两个数组,一个数组用于存放埋雷的信息,一个用于存放展示给玩家看某坐标周围八个坐标的信息。
char mine[9][9] = { 0 };此数组用于存放埋雷的信息。
char show[9][9] = { 0 };此数组用于存放排查出雷的信息。
4、当我们遍历棋盘最外围处坐标周围坐标的信息时,棋盘最外围坐标的周围没有八个坐标,如四个对角坐标,他们的周围只有三个坐标。对于数组的遍历来说,就会造成越界。为了避免这种情况,我们给最边缘处再加上一行一列,此时二维数组的大小由[9][9]变成了[11][11]。不过我们使用的部分的大小依然是九成九,数组行和列在下标上的范围是1~9。如下图所示:
4、游戏棋盘的初始化
InitBoard(mine, ROWS, COLS,'0'); InitBoard(show, ROWS, COLS,'*'); 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; } } }
1、将mine数组全部初始化为字符0,show数组初始化为字符*。
2、为了将数组初始化函数的普遍性提高,能将我们传过去的任意字符数组初始化为我们想要的结果,在设计初始化函数的时候,创建一个形参将要初始化的内容也一并接收。这样将要初始化的内容一并传过去,传的是什么,初始化的就是什么。
3、ROWS与COLS是我们设定的行与列,已经在一个专门的头文件(.h文件)中用#define的标识符常量定义好为11行11列,如下:#define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+2
5、设置雷的信息
我们将mine数组中存放雷的信息
SetMine(mine, ROW, COL);//将mine数组传过去布置雷 void SetMine(char board[ROWS][COLS], int row, int col) { int count = EASY_COUNT; //布置雷的个数 while (count) { int x = rand() % row + 1; //随机数模9,得到0~8的随机数,加上1,得到1~9的随机数 int y = rand() % col + 1; if (board[x][y] == '0') { board[x][y] = '1';//将雷的信息布置为1。 count--; } } }
1、行和列只在1~9的范围上布置雷,所以传参传的是ROW与COL。
2、EASY_COUNT是我们布置雷的个数,已经在头文件(.h文件)中用#define的标识符常量定义为10,#define EASY_COUNT 10。
3、利用srand与rand函数生成1 ~ 9的随机数,随机数组成棋盘行和列的坐标,在棋盘1 ~ 9的行和列上存放字符1,即埋雷。
4、埋雷的时候,为了不再一个地方重复埋雷,导致埋雷的个数不足十个,先判断次数坐标是否已经有雷,有雷即这个坐标已经存放了字符1,没有雷,即这个坐标依然是初始化的字符0。
5、循环条件count,布置好一个雷,count - 1,直到十个雷全部埋好,循环条件不满足,循环结束。
6、打印棋盘
我们展示的棋盘是show数组,所以将show数组打印出来。当然后面遇到雷炸了,或者玩家赢了等游戏结束的情形,我们也要通过打印函数,将mine数组打印出来,告诉玩家为什么输或者为什么赢。
DisplayBoard(show, ROW, COL);//打印的时候只打印我们要展示的数组下标1~9的范围,所以传参ROW与COL void DisplayBoard(char board[ROWS][COLS], int row, int col) { int i = 0; int j = 0; printf("-------扫雷--------\n"); for (j = 0; j <= col; j++) { printf("%d ", j);//打印列号,为了用户能够在棋盘上识别出棋子的坐标 } printf("\n"); for (i = 1; i <= row; i++)//我们需要用的部分的是ROW与COL这么大的空间。在这里为1~9。 { printf("%d ", i);//打印行号,为了用户能够在棋盘上识别出棋子的坐标 for (j = 1; j <= col; j++) { printf("%c ", board[i][j]); } printf("\n"); } printf("-------扫雷--------\n"); }
打印结果
- 虽然我们要打印数组行和列的范围是1 ~ 9,不过函数形参接收的数组设计依然要写成[ROWS][COLS],因为我们设计的数组本身就是这么大,必须要用相应大的数组来作为形参来接收,只不过我们只是用其中1 ~ 9的范围,而不是全部的范围0 ~ 10。
- 为了玩家便于知道棋盘上的坐标,我们在棋盘周围将行号与列号同时打印出来,为了对仗工整,列号的打印为0 ~ 9,行号的打印为1 ~ 9。
7、玩家找雷且输赢判定
排查雷,在mine数组中找雷,找出雷的信息显示在show数组中,所以要将两个数组都作为参数传过去。
FindMine(mine, show, ROW, COL); int GetMine_count(char board[ROWS][COLS],int x, int y) { return (board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + board[x][y - 1] + board[x][y + 1] + board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0'); //字符1的ASCII值是49,字符的ASCII值是48,'1'-'0'=49-48=1,所以将board[x][y] //周围八个坐标相加减去八个字符0,就能得到周围board[x][y]八个坐标有几个雷的信息 //不能妄想将周围八个坐标相加得到几就有几个雷,因为这里是字符1,不是数字1。 } void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int win = 0;//找到非雷的个数 while (win < row * col - EASY_COUNT)//9*9-10=71 { printf("请输入要排查的坐标:>"); scanf("%d%d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入坐标的合法性 { if (show[x][y] != '*') { printf("该坐标被排查过了,不能重复排查\n"); } else { if (mine[x][y] == '1')//等于1,说明踩到雷了 { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, ROW, COL);//告诉玩家为什么被炸死了,即这里雷的信息打印出来 break; } else//如果不是雷,显示这个坐标周围雷的信息(周围有几个雷) { win++; int count = GetMine_count(mine, x, y); show[x][y] = count + '0';//将周围雷的信息传递个这个坐标,因为传递回来的是数字,例如将数字3转化为字符3,加上字符0 //3+'0'=3+48=51,数字3对应的ASCII值就是51,将51存进字符数组,存进去的就是字符3。 DisplayBoard(show, ROW, COL);//此时将棋盘上的信息打印一遍 } } } else { printf("输入坐标非法,请重新输入"); } } if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); DisplayBoard(mine, ROW, COL); } }
- GetMine_count(mine, x, y); 函数是为了探寻坐标(x,y)处周围八个坐标的雷的信息。
- 字符1的ASCII值是49,字符的ASCII值是48,‘1’-‘0’=49-48=1,所以将board[x][y]周围八个坐标相加减去八个字符0,就能得到周围board[x][y]八个坐标有几个雷的信息。不能妄想将周围八个坐标相加得到几就有几个雷,因为这里是字符1,不是数字1。这个案例提示我们要注意内存的含义,即1. 内存中存的是什么,2. 怎么理解这块内存
- 玩家在输入某个坐标(x,y)后,我们会将此坐标雷的信息标记在数组show中,但是我们是在mine数组中找的雷,并且对坐标(x,y)找寻后,没有在数组mine中进行标记,所以在下次重复输入相同坐标(x,y)后,会使win++重复,导致判断赢的循环条件在没有排除雷的情况下进行了修正。使得游戏出现bug,为了避免这种情况,对输入的坐标判断其是否已经被排查过了,排查过的坐标在数组show上是已经被标记过周围雷的信息的,即show数组上该坐标的元素不是初始化的 ’ * '。------> if (show[x][y] != ‘*’)
- 玩家什么时候赢?
在给一个9×9的棋盘上布置10个雷,当无雷的9×9-10=71个坐标数都被排查了,说明已经取得胜利。所以给循环条件设计为 win<71,没成功排查一个坐标数win+1。循环中止后,再判断是否 win=71(因为被炸死也会中止循环),等于,说明赢了。- 被炸死,即玩家输入的该坐标,对应的mine数组中存放的是字符1。
8、游戏完整代码
game.h 头文件:包含了库函数的头文件以及自定义函数的声明
#pragma once #include <stdio.h> #include <time.h> #include <stdlib.h> #define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+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);//数组部分依然是ROWS与COLS,因为我们定义的数组就是这么大。虽然我们只用ROW与COL部分的空间。 void SetMine(char board[ROWS][COLS], int row, int col); void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
game.c文件:实现扫雷的主要功能,如:初始化棋盘、设计雷的信息、打印棋盘、玩家找雷、判断输赢
#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" 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; } } } void DisplayBoard(char board[ROWS][COLS], int row, int col) { int i = 0; int j = 0; printf("-------扫雷--------\n"); for (j = 0; j <= col; j++) { printf("%d ", j);//打印列号,为了用户能够在棋盘上识别出棋子的坐标 } printf("\n"); for (i = 1; i <= row; i++)//我们需要用的部分的是ROW与COL这么大的空间。在这里为1~9。 { printf("%d ", i);//打印行号,为了用户能够在棋盘上识别出棋子的坐标 for (j = 1; j <= col; j++) { printf("%c ", board[i][j]); } printf("\n"); } printf("-------扫雷--------\n"); } void SetMine(char board[ROWS][COLS], int row, int col) { int count = EASY_COUNT; //布置雷的个数 while (count) { int x = rand() % row + 1; //随机数模9,得到0~8的随机数,加上1,得到1~9的随机数 int y = rand() % col + 1; if (board[x][y] == '0') { board[x][y] = '1';//将雷的信息布置为1。 count--; } } } int GetMine_count(char board[ROWS][COLS],int x, int y) { return (board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + board[x][y - 1] + board[x][y + 1] + board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0'); //字符1的ASCII值是49,字符的ASCII值是48,'1'-'0'=49-48=1,所以将board[x][y] //周围八个坐标相加减去八个字符0,就能得到周围board[x][y]八个坐标有几个雷的信息 //不能妄想将周围八个坐标相加得到几就有几个雷,因为这里是字符1,不是数字1。 } void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) { int x = 0; int y = 0; int win = 0;//找到非雷的个数 while (win < row * col - EASY_COUNT)//9*9-10=71 { printf("请输入要排查的坐标:>"); scanf("%d%d", &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入坐标的合法性 { if (show[x][y] != '*') { printf("该坐标被排查过了,不能重复排查\n"); } else { if (mine[x][y] == '1')//等于1,说明踩到雷了 { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, ROW, COL);//告诉玩家为什么被炸死了,即这里雷的信息打印出来 break; } else//如果不是雷,显示这个坐标周围雷的信息(周围有几个雷) { win++; int count = GetMine_count(mine, x, y); show[x][y] = count + '0';//将周围雷的信息传递个这个坐标,因为传递回来的是数字,例如将数字3转化为字符3,加上字符0 //3+'0'=3+48=51,数字3对应的ASCII值就是51,将51存进字符数组,存进去的就是字符3。 DisplayBoard(show, ROW, COL);//此时将棋盘上的信息打印一遍 } } } else { printf("输入坐标非法,请重新输入"); } } if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); DisplayBoard(mine, ROW, COL); } }
test.c文件:将主函数放在此文件中,整合实现扫雷游戏功能的函数的顺序,什么时候实现什么函数的功能。
#define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" void menu() { printf("****************************\n"); printf("****** 1. play ******\n"); printf("****** 0. play ******\n"); printf("****************************\n"); } void game() { char mine[ROWS][COLS] = { 0 };//存放布置好的的雷的信息 char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息 //初始化数组的内容为指定的内容 //mine 数组在没有布置雷的时候,都是'0' InitBoard(mine, ROWS, COLS,'0'); //将要初始化的内容一并传过去,传的是什么,初始化的就是什么 //show 数组在没有排查雷的时候,都是'*' InitBoard(show, ROWS, COLS,'*'); //DisplayBoard(mine, ROW, COL);//一般不打印埋雷的信息,不能让人看见埋雷的信息 //设置雷 SetMine(mine, ROW, COL);//将mine数组传过去布置雷 DisplayBoard(show, ROW, COL);//打印的时候只打印我们要展示的内容,所以传参ROW与COL //排查雷,在mine数组中找雷,找出雷的信息显示在show数组中,所以要将两个数组都作为参数传过去。 FindMine(mine, show, ROW, COL); } int main() { srand((unsigned int)time(NULL)); 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); return 0; }
- 选择do while循环结构,程序一运行先打印游戏菜单,选择是否玩游戏。将输入决定玩游戏的 input来作为循环条件,input = 1,玩游戏 ;input =0 ,游戏结束,循环结束;input = 其他数字,循环条件非 0 循环依然在继续,重新选择是否玩游戏。
- 如何测试这个游戏代码的正确性?
我们先在头文件中将雷的个数改为80个,在游戏开始时将show数组与mine数组同时打印出来,这样就能知道剩下的唯一那个不是雷的具体坐标。此时想测试玩家被炸死或则玩家赢了游戏都是十分方便快捷的。