前言:本篇文章基于三子棋的基础,会在部分三子棋中运用过的技术或方法进行简述。详情可见
目录
FindMine()排查雷函数和 eapand()展开函数(重难点!!!!)
重点难点:1.实现布置雷和排查雷的功能
解决办法--》设置两个二维数组,一个数组存放雷的信息,一个数组存放棋盘的信息(界面)
大致逻辑:先初始化,电脑随机设置雷的信息,之后玩家访问存放雷的信息的数组,对玩家输入的值进行判断,更改存放棋盘信息的数组,从而在界面上看出改变。
2.实现展开一大片的操作
运用到递归的操作。递归的推出条件为“1.所排查位置不是雷 2. 所排查位置没有被排查过3 .同时输入的横纵坐标不能越界
Ⅰ游戏表现和玩家体验(程序设计目标--->确定整体方向)
1.整体游戏运行---> 玩家玩完一局后可以选择继续游玩,退出游戏要主动选择
2.展现游戏菜单---> 玩家可选择进入或退出游戏
3.游戏界面初始化
4.玩家选择位置排雷--->有雷爆炸,显示所有雷位置,退出游戏
--->无雷显示周边雷的个数/周边也无雷时展开一片,继续游戏
5.当排出图上所有雷时结束游戏
Ⅱ代码运用与编程逻辑(程序设计实现)
首先我们依旧要用到多文件进行模块化编程
game.h文件完成函数的声明和相关头文件引用,test.c文件放游戏的启动代码
game.c文件放游戏的实现代码
game.h
#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 game();
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 row, int col, int count);
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col);
void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
重点:
1.将二维数组的横纵坐标进行宏定义常量
2.因为排查雷的操作需要访问周边八个空格的位置,当排查的位置是在棋盘边缘的时候,存在越界访问数组的危险,所以在最开始初始化数组的时候就将数组的范围上下左右各增加一行。需要访问的是9*9的二维数组,而初始化的是11*11的二维数组
注释:
宏定义常量
#define ROW 9 //行
#define COL 9 //列
#define ROWS ROW + 2 //加长的行
#define COLS COL + 2 //加长的列
#define EASY_COUNT 101.EASY_COUNT 10 为雷的个数 ,可以用来设置难度系数。当雷的数量越多的时候越困难。
2.void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
初始化棋盘函数。因为要初始化两个数组,为了让函数的可利用性增加,我们将需要初始化的数组的元素(set) 也作为参数传给函数。横纵坐标的参数传的是11行,11列。
3.void DisplayBoard(char board[ROWS][COLS], int row, int col);
打印棋盘函数。要访问的二维数组的行列都是11,但是实际要打印出来给玩家看到的只需要9行9列。
4.void SetMine(char board[ROWS][COLS], int row, int col, int count);
布置雷函数。要接受难度系数(即雷的个数)作为参数。理论设计为用循环实现雷的布置,当一个雷布置完成,需要布置的雷的个数就减少一个,直到最后全部雷布置完成,退出循环。
5.void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col);
找雷函数。实现玩家排查雷,判断所排查位置是否为雷,是雷引爆,非雷显示周边八个空格雷的个数。
6.void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
展开函数。当所排查的雷的位置不是雷,且周边八个空格都不是雷,就进入这个函数。利用递归的方法,实现展开一片的功能。
test.c文件
#include "game.h"
void menu()
{
printf("**************************\n");
printf("******** 1.play *********\n");
printf("******** 0.exit *********\n");
printf("**************************\n");
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL)); //初始化随机数起点
do
{
menu();
printf("请选择->\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("exit\n");
break;
default:
printf("wrong try again\n");
}
} while (input);
return 0;
}
test.c文件放游戏的启动代码
do while 循环实现
do while 循环特点 :会直接执行循环体一次,当一次执行完后再判断是否继续。
即实现玩完一局可选择继续游玩或退出游戏
可用玩家输入值作为进入或退出循环判断。输入为真(非0)进入游戏
为假(0)退出游戏
在main()函数中初始化随机数起点
因为我们后面要布置雷,需要用随机数来生成雷的位置,而调用随机数函数rand()前,根据规定要先调用srand()函数进行随机数起点设置。
game.c文件
void game游戏运行逻辑
void game()
{
//初始化数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL, EASY_COUNT);
//排查雷
FindMine(show ,mine, ROW, COL);
}
初始化数组mine放雷的信息
show放棋盘的信息(给玩家看的)
定义字符数组,所以放置元素全设为字符类型
先将mine数组中的所有元素初始化为'0'
我们预先假定雷的表示是'1'
这么设置的好处后面会讲
将show数组的元素全初始化为'*'
InitBoard()初始化数组函数
void InitBoard(char board[ROWS][COLS], int row, int col, char set)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
初始化棋盘函数。因为要初始化两个数组,为了让函数的可利用性增加,我们将需要初始化的数组的元素(set) 也作为参数传给函数。横纵坐标的参数传的是11行,11列。
DisplayBoard()打印棋盘函数
void DisplayBoard(char board[ROWS][COLS], int rows, int cols)
{
int i = 0;
printf("------扫雷游戏-----\n");
for (i = 0; i <= cols; i++)//打印列号
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= rows; i++)
{
int j = 0;
printf("%d ", i); //打印行号 要让列对齐,所以在打印格式中也多加个空格
for (j = 1; j <= cols; j++)
{
printf("%c ", board[i][j]); //打印格式中多个空格
}
printf("\n");
}
}
为了让玩家更方便地确定所排查的位置的坐标,要在棋盘的周边放上行和列的信息
因为棋盘初始化的时候,为了防止排查边缘的位置时进行越界访问,我们将棋盘初始化的行和列设成了11行,11列。所以在打印棋盘的时候,我们要将打印的循环的起点,i 初始化为1
即从二维数组下标为1行1列开始打印
最后不要忘记换行
出来的效果就是这样
因为是十一列,所以在打印列的时候多设置了个空格
为了列号和棋盘中的列对齐,我们可以从0开始循环
SetMine()布置雷函数
void SetMine(char board[ROWS][COLS], int row, int col, int count)
{
while (count)
{
int x = rand() % row + 1; //布置是从一行一列开始的
int y = rand() % col + 1; //布置雷的时候也要判断所布置的地方是否已经布置过了
if (board[x][y] == '0') //因为随机生成的数是可能在同一个位置的
{
board[x][y] = '1';
count--;
}
}
}
我们要访问的是被打印出来的 '*'上的雷的位置
在打印棋盘的时候,我们的循环就是从二维数组元素下标为一行一列开始的
所以生成的雷的位置信息应该也在这个范围内
rand() & row得到 0~ (row-1) 的数,再+1就得到1~row 的数 。
可以发现,此时我们的坐标数和数组的元素下标号重合了,棋盘上的一行一列就是数组中的一行一列。
这里用while函数来实现雷的布置,将count(临时变量,是EASY_COUNT即雷的数量的一份临时拷贝)设置为进入循环的条件。
为什么不用for?因为生成的随机坐标不一定有效,当生成的雷的坐标和上一次生成的坐标重合时,如果是for循环,可能会造成最终生成的雷的数量比预期要少。而while循环就解决了这个弊端。
FindMine()排查雷函数和 eapand()展开函数(重难点!!!!)
因为涉及嵌套调用,我们先将两个函数放在一起,之后再逐一详解
int win = 0; //win代表排查出的空格数,即安全格子数
void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)//遍历周围9空格计算周围雷个数
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '1')
count++;
}
}
if (count == 0)
{
show[x][y] = ' ';
win++; //这个位置没有雷,排查成功+1
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if ((mine[i][j] != '1') && (show[i][j] == '*') && (i > 0) && (i <= ROW) && (j > 0) && (j <= COL))
{
expand(mine, show, i, j);
}
}
}
}
else
{
show[x][y] = count + '0'; //整型数字转化为对应的字符数字
win++; //这个位置没有雷,排查成功+1
}
}
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (win < row * col - EASY_COUNT)
{
printf("请选择要排查的位置-->\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
if (mine[x][y] == '1') //输入位置有雷
{
printf("你被雷炸死咯\n");
DisplayBoard(mine, ROW, COL);
break;
}
else if (mine[x][y] == '0') //输入位置无雷
{
expand(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
printf("输入错误,请重试\n");
}
if (win == row * col - EASY_COUNT)
{
printf("you win!\n");
win = 0;
}
}
首先是对判断游戏输赢的计数器win的设置
因为Findmine()函数和expand()函数都要使用到它,而考虑到如果用传参的方法设置太麻烦,我们直接将其设置为全局变量。
Finemine()函数详解
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (win < row * col - EASY_COUNT) //EASY_COUNT代表雷的个数
{
printf("请选择要排查的位置-->\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
if (mine[x][y] == '1') //输入位置有雷
{
printf("你被雷炸死咯\n");
DisplayBoard(mine, ROW, COL);
break;
}
else if (mine[x][y] == '0') //输入位置无雷
{
expand(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
printf("输入错误,请重试\n");
}
if (win == row * col - EASY_COUNT)
{
printf("you win!\n");
win = 0;
}
}
整体排查代码我们放进while循环里面,当win小于打印出的棋盘中的空格数时,就进入循环,进行排查。
最外层if else语句用来判断输入的合法性
里层if ese语句判断所排查位置是否有雷
有雷给出相应提示,并打印雷的信息,让玩家死得瞑目
并break跳出while循环
无雷则调用expand()函数
最后跳出while循环有两种情况
一是踩雷炸死,break出来的。二是扫完了所有雷,win 不满足进入while循环条件,出来的。
所以这里要额外加一句 if 判断 是否是真赢了
真赢了就打印祝贺信息,并且重置win的值,因为win是全局函数。而一把游戏结束,直接进入下一把时,win如果在这里没被重置的话,会在下一次进入游戏时直接判断成玩家胜利。
详解expand()函数
void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)//遍历周围9空格计算周围雷个数
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '1')
count++;
}
}
if (count == 0)
{
show[x][y] = ' ';
win++; //这个位置没有雷,排查成功+1
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if ((mine[i][j] != '1') && (show[i][j] == '*') && (i > 0) && (i <= ROW) && (j > 0) && (j <= COL))
{
expand(mine, show, i, j);
}
}
}
}
else
{
show[x][y] = count + '0'; //整型数字转化为对应的字符数字
win++; //这个位置没有雷,排查成功+1
}
}
expand()函数要实现
1.将周边雷的信息打印到当前格子上
2.若当前格子及周边八个格子都无雷,扩展一片
首先设置计数器count计算雷的数量 ,遍历周边加上本身九个格子的元素,当有雷时,count自增
接下来的if else语句判断周边雷的数量和是否为0
1.不是0,打印周边雷的数量,并让win自增。这是当前已打印周边雷的信息的这个格子排查完的结束
2.是0 将当前格子打印为空格,并让win自增。这是已排查完这个格子的结束标志。
随后再遍历周边的八个空格,当满足所遍历的格子
a.不是雷,即mine数组上存放的不是'1'
b.没有被排查过即show数组上存放的是'*'
c,这个格子是在玩家看到的棋盘中,即不越界
这三个条件全满足时再次调用函数expand()这样就可以达到展开一片的效果
而且为了让玩家直接看到展开后的结果,所以我们的DisplayBoard()函数是放在
FindMine()函数调用完后的。(可以翻上去看看代码)
这里还要格外注意的一个点是给expand()传的横纵坐标的实参是 i 和 j
因为是对本身已经是空格的这个格子周边的格子的展开,如果传的是x 和 y(玩家最开始输入的坐标) 那就成bug了,会进入无限的循环中。
因为我们设置的计算雷的数量的计数器count是int型的数字,而在棋盘上的数字应该是字符char型的。所以这里涉及整型数字转为字符型数字。
方法是 直接用这个整型数字 + '0' (字符0) 就可以得到对应的字符数字
具体是为什么呢?
首先我们要直到字符都有一个对应的ASCii码值,而这个ASCii码值其实就是个整数
所以字符类型和整型之间是可以直接进行运算的
在ASCii码表中,字符数字是连续的
我们举个例子
'0'--48 '1'--49' '2'--50 '3'--51
所以如果要让数字3转化为字符3 只要加上一个'0'即可 '0'+3 = '3' (48+3=51)
下面是截取的一张ASCii码表
十进制 字符
这是展开一片及游戏胜利的效果
为了便于测试,这里暂时将宏定义常量的EASY_COUNT 设置为3
大家在测试自己代码的时候,也可以这么进行设置
至此我们将扫雷整个游戏编写完成,恭喜你,又向繁星更近了一步!
整个游戏的代码
test,c
#include "game.h"
void menu()
{
printf("**************************\n");
printf("******** 1.play *********\n");
printf("******** 0.exit *********\n");
printf("**************************\n");
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择->\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("exit\n");
break;
default:
printf("wrong try again\n");
}
} while (input);
return 0;
}
game.h文件
#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 game();
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 row, int col, int count);
void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col);
game.c文件
#include "game.h"
void InitBoard(char board[ROWS][COLS], int row, int col, char set)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
void DisplayBoard(char board[ROWS][COLS], int rows, int cols)
{
int i = 0;
printf("------扫雷游戏-----\n");
for (i = 0; i <= cols; i++)//打印列号
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= rows; i++)
{
int j = 0;
printf("%d ", i); //打印行号
for (j = 1; j <= cols; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
void SetMine(char board[ROWS][COLS], int row, int col, int count)
{
while (count)
{
int x = rand() % row + 1; //布置是从一行一列开始的
int y = rand() % col + 1; //布置雷的时候也要判断所布置的地方是否已经布置过了
if (board[x][y] == '0') //因为随机生成的数是可能在同一个位置的
{
board[x][y] = '1';
count--;
}
}
}
int win = 0; //win排查出的空格数,即安全格子数
void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)//遍历周围9空格计算周围雷个数
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '1')
count++;
}
}
if (count == 0)
{
show[x][y] = ' ';
win++; //这个位置没有雷,排查成功+1
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if ((mine[i][j] != '1') && (show[i][j] == '*') && (i > 0) && (i <= ROW) && (j > 0) && (j <= COL))
{
expand(mine, show, i, j);
}
}
}
}
else
{
show[x][y] = count + '0'; //整型数字转化为对应的字符数字
win++; //这个位置没有雷,排查成功+1
}
}
void FindMine(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (win < row * col - EASY_COUNT)
{
printf("请选择要排查的位置-->\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
if (mine[x][y] == '1') //输入位置有雷
{
printf("你被雷炸死咯\n");
DisplayBoard(mine, ROW, COL);
break;
}
else if (mine[x][y] == '0') //输入位置无雷
{
expand(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
printf("输入错误,请重试\n");
}
if (win == row * col - EASY_COUNT)
{
printf("you win!\n");
win = 0;
}
}
void game()
{
//初始化数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL, EASY_COUNT);
//排查雷
FindMine(show ,mine, ROW, COL);
}