文章目录
前言
扫雷是一个难度适中的C语言编程实践案例,通过对扫雷的编程可以强化我们对数组的认识。
一、运行画面展示
(1/5)菜单与启动
(2/5)游戏过程
(3/5)游戏胜利与重新开始
(4/5)游戏失败与重新开始
(5/5)输入错误坐标与重复坐标
二、代码
代码如下:
1.头文件 game.h (1/1)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//我们实际下棋的雷场参数
#define ROW 3
#define COL 3
//我们系统内部实际的雷场参数
#define ROWS ROW+2
#define COLS COL+2
//雷场一共有多少雷
#define MINE_COUNT 4
void init_board(char board[ROWS][COLS], int rows, int cols);//初始化系统雷场
void print_board(char board[ROWS][COLS], int rows, int cols);//打印初始化系统棋盘
void set_mine(char board[ROWS][COLS], int rows, int cols,int num);//布置地雷
void init_player_board(char player_board[ROWS][COLS], int rows, int cols);//初始化玩家雷场
void printf_player_board(char player_board[ROWS][COLS], int rows, int cols);//打印给玩家观看的雷场
void found_mine(char board[ROWS][COLS],char player_board[ROWS][COLS]/*, int auto_board[ROWS][COLS] */, int rows, int cols,int mine_count,int m);//寻找地雷
//int auto_found_mine(char board[ROWS][COLS], char player_board[ROWS][COLS], int auto_board[ROWS][COLS], int rows, int cols, int mine_count, int x, int y, int m);//自动排空
2.源文件 text.c(1/2)
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void mune()
{
printf("\n");
printf("************************\n");
printf("*******扫雷复习版*******\n");
printf("******* 菜单************\n");
printf("******1.开始游戏********\n");
printf("******0.结束游戏********\n");
printf("************************\n");
}
void game()
{
//游戏内由系统雷场和玩家雷场构成。
// 两个雷场由两个二维数组实现。
//系统雷场,所有信息完全暴露,用于存储地雷布置信息,计算某点周围的地雷个数
//玩家雷场,地雷信息隐藏,用于给玩家游玩猜测地雷所在位置。
//系统雷场
char board[ROWS][COLS];
init_board(board,ROWS,COLS);//初始化系统雷场
set_mine(board,ROWS,COLS, MINE_COUNT);//在系统雷场布置地雷
//玩家雷场
//系统雷场与玩家雷场不是同一个数组实现,两个雷场由两个不同的数组存储信息
//玩家雷场打印9*9个雷场格子,我们可以用11*11的数组实现
char player_board[ROWS][COLS];
init_player_board(player_board, ROWS, COLS);//初始化玩家雷场
printf_player_board(player_board , ROWS, COLS);//打印给玩家观看的雷场
//雷场布置完毕,接下来是扫雷
//步骤一是玩家输入一个坐标
//步骤二是判断它是不是地雷,如果是地雷则游戏结束,如果不是,则显示这个坐标周围的地雷数
//一个函数,要系统地图,要使用打印玩家地图
int m = (ROWS - 2) * (COLS - 2) - MINE_COUNT;
//int auto_board[ROWS][COLS];//为实现自动排空而设置的信息媒介数组
//for(int i=0;i<ROWS;i++)
// for (int j = 0; j < COLS; j++)
// {
// auto_board[i][j] = 0;
// }
found_mine(board,player_board/*,auto_board*/, ROWS, COLS, MINE_COUNT,m);//寻找地雷
}
int main()
{
srand((unsigned int)time(NULL));
mune();
int input = 0;
do
{
printf("请选择:\n");
scanf("%d", &input);
switch (input)
{
case 1: //开始游戏
printf("游戏开始\n");
game();
printf("\n是否再次“扫雷,启动!”\n");
mune();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新选择。\n");
}
} while (input);
return 0;
}
3.源文件 game.c(2/2)
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//初始化雷场
void init_board(char board[ROWS][COLS], int rows, int cols)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
board[i][j] = '0';
}
}
//打印初始化棋盘
void print_board(char board[ROWS][COLS], int rows, int cols)
{
printf("\n地雷信息如下:\n");
//行标
for (int j = 0; j < cols - 1; j++)
{
printf("%d ", j);
}
printf("\n");
for (int i = 1; i < rows-1; i++)
{
printf("%d ", i); //提供列标
for (int j = 1; j < cols - 1; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//布置地雷
void set_mine(char board[ROWS][COLS], int rows, int cols,int num)
{
//随机布雷,要在9*9的范围内布雷
//srand((unsigned int)time(NULL));
int i = num;
while(i)
{
int x = rand() % rows;
int y = rand() % cols;
if ((x > 0 && x < rows - 1) && (y > 0 && y < cols - 1))
{
if (board[x][y] != '1') //避免同一位置重复布雷
{
board[x][y] = '1';
i--;
}
}
}
}
//初始化玩家雷场
void init_player_board(char player_board[ROWS][COLS], int rows, int cols)
{
//玩家看到的是一个由*构成的地图
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
player_board[i][j] ='*';
}
}
}
//打印给玩家雷场
void printf_player_board(char player_board[ROWS][COLS], int rows, int cols)
{
printf("请扫雷:\n");
//首先为玩家提供行标
for (int j = 0; j < cols - 1; j++)
{
printf("%d|", j);
}
printf("\n");
//打印棋盘
for (int i = 1; i < rows-1; i++)
{
printf("%d|", i); //提供列标
for (int j = 1; j < cols-1; j++) //注意,这里的j要考虑到“提供列标”把列顶出了一格所以要多减去一格
{
printf("%c ", player_board[i][j]);
}
printf("\n");
}
}
//寻找地雷
void found_mine(char board[ROWS][COLS], char player_board[ROWS][COLS],/* int auto_board[ROWS][COLS], */int rows, int cols, int mine_count, int m)
{
while (1)
{
printf("请输入查找坐标:\n");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y); //根据玩家输入的坐标找对应的位置
if (x<1 || x>=rows-1|| y<1 || y>=cols-1)
{
printf("坐标越界,请重新输入坐标。");
continue;
}
else
{
if (player_board[x][y] != '*')
{
printf("此处排查过了\n");
continue;
}
else
{
//判断该坐标是否有地雷,若有则游戏结束,若没有则根据系统雷场标明该位置周围有几个地雷,并将地雷数标识打印在新地图上
if (board[x][y] == '1')//判断地雷
{
printf("遇到地雷,游戏结束\n");
print_board(board, rows, cols);//打印初始化系统棋盘
break;
}
else
{
//把周围一圈的数组内容作整数加起来
char ret =
(board[x - 1][y + 1] +
board[x][y + 1] +
board[x + 1][y + 1] +
board[x - 1][y] +
board[x + 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1]) - 7 * '0';
if (ret == '0')//如果坐标周围没有地雷就换成空格
{
player_board[x][y] = ' ';
m--;
//在自动排雷表上填充数字
//auto_board[x][y] = 1;
//如果被检查的坐标为空格,则调用自动排空函数
//m = auto_found_mine(board[ROWS][COLS], player_board[ROWS][COLS], auto_board[ROWS][COLS], rows, cols, mine_count, x, y, m);
}
else
{
player_board[x][y] = ret;
m--;
//auto_board[x][y] = 1;
}
if (m == 0)
{
printf("\n游戏胜利,你排除了%d颗地雷。\n", mine_count);
print_board(board, rows, cols);//打印初始化系统棋盘
break;
}
printf_player_board(player_board, rows, cols);//打印给玩家观看的雷场
//print_board(board, rows, cols);//打印初始化系统棋盘
}
}
}
}
}
//
//
自动排空---目前还未实现
问题在于我们怎样才能让目标坐标周围一圈的坐标被排空。上下左右,四个45角度,一共八个方向上的坐标被排空,
还要被排除地方自己也排空自己周围的地方,而且还要不对m重复减去,即同一个坐标不能被反复排除,
猜想:莫非再开一个数组,用新开的数组做一个信息存储中介,之前被检查过一次的坐标在此存储,每次剩余空格数m要减少前都要和这个数组比对判断,只有该位置没有被存储过m才能减少,防止同一个坐标被反复检查
我们让这个二维数组大小和系统雷场数组一样,每个位置存储整形0,如果被检查过则存储整形1。
若猜想实现,那各个方向的坐标要怎么实现,(注意,不能让自动排空里面有递归自己否则会有自己检查自己的无限循环,错,可以有递归,只需要对xy递归方向做严格限制即可)
我们可以在调用自动排空的时候拒绝与我们想要的方向相反方向上的排空检查发生,如此可以规避无限循环发生,注意要限制一下排空不要搞出访问出了数组范围
//
关于即将越界问题,不需要担心,因为函数主体开始前我们可以凭借if语句判断来规避读取越界,并且无尽递归的问题也同时解决了。
//
//
//int auto_found_mine(char board[ROWS][COLS], char player_board[ROWS][COLS], int auto_board[ROWS][COLS] , int rows, int cols, int mine_count, int x, int y,int m)
//{
//
// //这是个要不断递归使用的函数,在开始跑动前应该检查原坐标是否即将越界
// if ((x < cols && x > 0) && (y > 0 && y < rows))
// {
// if ((board[++x][++y] == '0') && auto_board[x][y] != 1) //原坐标右上方的元素是'0'的情况
// {
// int ret =
// (board[x - 1][y + 1] +
// board[x][y + 1] +
// board[x + 1][y + 1] +
// board[x - 1][y] +
// board[x + 1][y] +
// board[x - 1][y - 1] +
// board[x][y - 1] +
// board[x + 1][y - 1]) - 8 * '0';
// player_board[x][y] = ret;
// auto_board[x][y] = 1;
// m--;
// }
// auto_found_mine(board[ROWS][COLS], player_board[ROWS][COLS], auto_board[ROWS][COLS], rows, cols, mine_count, x, y, m);
// }
//
//}
//
三.思路
1.整体思路
我们要实现扫雷游戏就是要实现过程:
菜单选择——打印雷场——输入坐标——扫雷判断——游戏胜利或失败——菜单选择
其中的关键在于实现打印雷场与扫雷判断。
打印雷场与扫雷判断需要两个数组储存信息,一个储存地图的地雷坐标信息,另一个储存玩家已排查坐标信息。
我们将储存地雷信息的数组称为系统雷场,储存玩家已排雷坐标的数组称为玩家雷场。
2.详细设计
打印雷场:从玩家雷场调取信息,打印雷场地图,表明哪些是玩家已排查的坐标。
扫雷判断:从系统雷场调取信息,根据玩家输入的坐标在系统雷场里面找对应的坐标,进行是否踩中地雷的判断,如果踩中则游戏结束,如果没有则把该坐标周围一圈的坐标内有多少雷的信息传递给玩家雷场,令其该坐标的’ * '被改成一个数字,让玩家了解到该坐标周围有多少地雷。
3.功能实现(1/3)——菜单与重新开始
游戏开始需要打印菜单,游戏重新开始也需要打印菜单。
分析可得:在一场游戏的开始前和游戏结束后都需要反复打印菜单,并通过菜单进行游戏选择,我们可以通过switch结构实现菜单游戏选择功能,通过循环结构实现重新选择。
void mune()
{
printf("\n");
printf("************************\n");
printf("*******扫雷复习版*******\n");
printf("******* 菜单************\n");
printf("******1.开始游戏********\n");
printf("******0.结束游戏********\n");
printf("************************\n");
}
int main()
{
srand((unsigned int)time(NULL));
mune();
int input = 0;
do
{
printf("请选择:\n");
scanf("%d", &input);
switch (input)
{
case 1: //开始游戏
printf("游戏开始\n");
game();
printf("\n是否再次“扫雷,启动!”\n");
mune();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新选择。\n");
}
} while (input);
return 0;
}
4.功能实现(2/3)——两个雷场
游戏内由系统雷场和玩家雷场构成。
两个雷场由两个独立的二维数组实现。
a)系统雷场,所有信息完全暴露,用于存储地雷布置信息,计算某点周围的地雷个数
b)玩家雷场,地雷信息隐藏,用于给玩家游玩猜测地雷所在位置。
细节:我们在设置两个雷场时应在上下左右额外预留两行两列
因为当玩家输入扫雷的坐标无雷时程序会自动将周围地雷数标明在玩家雷场对应的坐标位置里,而自动标明的动作发生在系统雷场里,此时如果被扫描的是系统雷场边缘或角落,则自动扫描地雷数时会扫到雷场外面(即访问越界),为规避这一情况我们应该在两个雷场的上下左右都预留一行/列。
对系统雷场而言,预留的行列只会在计算扫描坐标周围地雷数时充当填充物的功能,而其对地雷生成的影响,我们可以在地雷生成时限制一下地雷生成的坐标范围即可消除其影响 。
对玩家雷场而言,多出的雷场只是为了在接受系统雷场传递的坐标时可以直接接收,我们在打印玩家雷场的时候限制打印的坐标范围即可消除其影响。
//我们实际下棋的雷场参数
#define ROW 3
#define COL 3
//我们系统内部实际的雷场参数
#define ROWS ROW+2
#define COLS COL+2
系统雷场与玩家雷场使用相同的关键字[ROWS][COLS]是为了让系统雷场直接传递玩家排查的坐标给玩家雷场,以便打印排查过的雷场地图。
//系统雷场
char board[ROWS][COLS]; //定义系统雷场,使其被字符' 0 '填满。
init_board(board,ROWS,COLS); //初始化系统雷场。
set_mine(board,ROWS,COLS, MINE_COUNT); //在系统雷场布置地雷,随机使系统雷场里的一些' 0 ',被替换成字符' 1 ' ,
//字符' 0 '代表没有地雷,字符' 1 '代表有地雷。
//玩家雷场
//玩家雷场打印雷场地图格子,我们可以用打印一个字符数组实现。
char player_board[ROWS][COLS]; //定义玩家的雷场大小。
init_player_board(player_board, ROWS, COLS);//初始化玩家雷场,使其被“ * ”填满。
printf_player_board(player_board , ROWS, COLS);//打印给玩家观看的雷场。
5.功能实现(3/3)——扫雷与判断
//雷场布置完毕,接下来是扫雷,扫雷分三步:
//步骤一:玩家输入一个坐标。
//步骤二:判断它是不是地雷,如果是地雷则游戏结束,如果不是,则显示这个坐标周围的地雷数。
//步骤三:判断扫完这个坐标后是否游戏结束。
以上步骤在found_mine()中按顺序实现
细节:我们应该对玩家输入的扫雷坐标做严格的限制
在玩家输入坐标后应该先确定输入的坐标在雷场内之后再进行踩雷判断
(1)坐标是否在雷场内,我们用if()语句判断坐标范围即可;
(2)坐标是否被检查过,我们调用玩家雷场,看对应坐标元素是否为’ '空格或数字,如果时则说明该坐标已经被检查过,那么提示玩家此处被扫描过;
(3)坐标没有地雷时判断周围有多少地雷,我们通过系统雷场对应位置周围的元素相加来判断,相加值用ret储存。
char ret =
(board[x - 1][y + 1] +
board[x][y + 1] +
board[x + 1][y + 1] +
board[x - 1][y] +
board[x + 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1]) - 7 * '0';
found_mine()实现步骤一二三如下:
//寻找地雷
void found_mine(char board[ROWS][COLS], char player_board[ROWS][COLS],/* int auto_board[ROWS][COLS], */int rows, int cols, int mine_count, int m)
{
while (1)
{
printf("请输入查找坐标:\n");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y); //根据玩家输入的坐标找对应的位置
if (x<1 || x>=rows-1|| y<1 || y>=cols-1)
{
printf("坐标越界,请重新输入坐标。");
continue;
}
else
{
if (player_board[x][y] != '*')
{
printf("此处排查过了\n");
continue;
}
else
{
//判断该坐标是否有地雷,若有则游戏结束,若没有则根据系统雷场标明该位置周围有几个地雷,并将地雷数标识打印在新地图上
if (board[x][y] == '1')//判断地雷
{
printf("遇到地雷,游戏结束\n");
print_board(board, rows, cols);//打印初始化系统棋盘
break;
}
else
{
//把周围一圈的数组内容作整数加起来
char ret =
(board[x - 1][y + 1] +
board[x][y + 1] +
board[x + 1][y + 1] +
board[x - 1][y] +
board[x + 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1]) - 7 * '0';
if (ret == '0')//如果坐标周围没有地雷就换成空格
{
player_board[x][y] = ' ';
m--;
//在自动排雷表上填充数字
//auto_board[x][y] = 1;
//如果被检查的坐标为空格,则调用自动排空函数
//m = auto_found_mine(board[ROWS][COLS], player_board[ROWS][COLS], auto_board[ROWS][COLS], rows, cols, mine_count, x, y, m);
}
else
{
player_board[x][y] = ret;
m--;
//auto_board[x][y] = 1;
}
if (m == 0)
{
printf("\n游戏胜利,你排除了%d颗地雷。\n", mine_count);
print_board(board, rows, cols);//打印初始化系统棋盘
break;
}
printf_player_board(player_board, rows, cols);//打印给玩家观看的雷场
//print_board(board, rows, cols);//打印初始化系统棋盘
}
}
}
}
}
总结
以上就是今天要讲的内容,谢谢各位读者的观看。