1. 前言
这次用C语言实现扫雷小游戏,下面来记录一下学习过程!
扫雷游戏因微软Windows操作系统的普及而广为人知,该游戏根据棋盘周围提示雷的数量,在不触发到雷的情况下,揭开所有非雷的位置,直到玩家赢。
本次游戏的编写由main.c、game.c、game.h三个文件编写组成。本次记录分为扫雷基础版和优化版两大部分。
扫雷基础版主要实现扫雷的最基本功能,没有展开一片、地雷标记、第一次排雷避免雷等功能,后面的优化版将这些功能全部加上。
2. 扫雷基础版
下面是扫雷基础版的演示:
2.1 main.c编写
2.1.1 打印游戏菜单
首先,在主函数上打印游戏菜单,代码如下:
void menu() {
printf("****************************\n");
printf("****** 1. play ******\n");
printf("****** 0. exit ******\n");
printf("****************************\n");
}
2.1.2 do...while()组成游戏主框架
打印好游戏菜单后,进入游戏开始环节,用do...while()判断语句组成游戏主框架。
int main() {
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;
}
2.2 game.c编写
完成了游戏菜单主框架的编写后,后面开始编写game()函数。
下面是game()函数的大概执行流程,游戏开始时,需要对棋盘初始化,需要埋多少个雷,打印出棋盘供玩家扫雷、最后玩家扫雷。
2.2.1 扫雷游戏构想思路
雷储存和显示独立分开
最初设想构建只构建一个棋盘,用来存放地雷和展示棋盘。即直接在地雷棋盘打印显示。但考虑到存放地雷用'0'和'1'表示非雷和有雷,而显示棋盘需要显示出周围雷的个数,会与‘0’和'1'冲突。
另一方面,如果后面增加了地雷标记功能,如果只用一个棋盘进行储存地雷和打印展示,会把原来存放地雷信息覆盖掉,所以干脆用两个分别独立的棋盘表示存放雷棋盘和显示棋盘。可以用mine表示存放雷,用show表示显示棋盘
构建ROW+2和COL+2棋盘
另一方面,考虑到后面需要统计非雷区周围有几个雷,为了方便计算,可以创建ROW+2行和COL+2列的棋盘,以简化计算周围雷数量。
以该方式创建棋盘后,当要计算边缘位置周围雷数量时,就不需要单独判断周围有几个格子,统一按8个格子处理。
综上所述,写出game()执行流程代码:
void game() {
char mine[ROWS][COLS] = {0}; // 用于存放布置好的雷信息
char show[ROWS][COLS] = {0}; // 存放排查出的雷信息
// 初始化mine数组,在没有排查雷的时候,都是'0'
InitBoard(mine,ROWS,COLS,'0');
// 初始化show数组,在没有排查雷的时候,都是'*'
InitBoard(show, ROWS, COLS, '*');
// 埋雷
SetMine(mine,ROW,COL);
// 显示棋盘
DisplayBoard(show, ROW, COL);
// 排查雷
FindMine(mine,show,ROW,COL);
}
2.2.2 初始化棋盘
创建存放雷棋盘和显示棋盘后,对存放雷的棋盘和显示的棋盘进行初始化,为埋雷和显示作准备。存放雷的棋盘全部初始化为字符'0',表示非雷状态,显示棋盘初始化为字符'*',表示非揭开状态。
棋盘初始化InitBoard()代码:
// 棋盘初始化
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; //当初始化的棋盘是存放雷棋盘,set内容为'0',当初始化棋盘是显示棋盘,set内容为'*'
}
}
}
下面为两者初始化结果
2.2.3 埋雷
棋盘初始化完成,接下来进行埋雷操作。埋雷时,只需将字符'0'更改为字符'1',即可完成埋雷。字符'1'表示该位置有雷。
对于埋雷的坐标,可以利用rand()函数生成随机坐标。rand()函数的使用,需要设置生成随机数的"种子",可以用当前系统的时间作为其“种子”。
设置生成随机数的“种子”,将其放在主函数mian()上,调用一次就好。
int main() {
//设置随机数的生成起点
srand((unsigned int)time(NULL));
... //游戏实现部分
return 0;
}
埋雷时可以根据需求设置埋雷的个数,可以将该值EASY_COUNT在头文件宏定义,方便后面统一修改,EASY_COUNT为埋雷个数。
//埋雷个数
#define EASY_COUNT 10
埋雷SetMine()代码如下:
// 埋雷
//'0'表示非雷状态
//'1'表示有雷状态
void SetMine(char board[ROWS][COLS], int row, int col) {
int count = EASY_COUNT; //EASY_COUNT为埋雷个数,在头文件定义
//横坐标1~9
//纵坐标1~9
while (count) {
int x = rand() % row + 1; //随机生成x坐标
int y = rand() % col + 1; //随机生成y坐标
if (board[x][y] == '0') { //非雷状态才可以设置成雷
board[x][y] = '1'; //设置成雷
count--; //标志设置雷的数量
}
}
}
埋雷5个雷,试试效果,把储存雷的棋盘打印出来看看,成功!
2.2.4 打印显示棋盘
设置好雷后,下面开始打印出显示棋盘,方便玩家下棋,顺便把显示棋盘的行列的索引打印出来。
显示棋盘DisplayBoard() 代码如下:
//打印棋盘
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++) { // 控制打印棋盘的列
printf("%d ",i); //打印纵坐标的索引
for (j = 1; j <= col; j++) {
printf("%c ",board[i][j]); //打印棋盘内容
}
printf("\n");
}
printf("--------扫雷--------\n");
}
显示棋盘打印效果如下:
2.2.5 排查雷
显示出棋盘,玩家开始排查雷,下面进行完善扫雷FindMine()代码。
一个完整的排查雷流程:玩家输入扫雷的坐标,然后判断对应的位置是否有雷,如果该位置有雷,就会被炸死,游戏结束;如果对应位置不是雷,就需要计算出周围雷的个数,然后继续排查雷,直到全部的雷扫出来,玩家赢家;还有就是在扫雷过程被炸死。
(1)玩家输入坐标的判断
排查雷开始时,玩家需要输入坐标,程序需要对输入坐标进行判断:判断坐标是否合法、判断是否之前排查过、判断是否为为雷区。然后对不同结果进行不同处理。
以玩家输入坐标(x,y)为例,对下面情况进行讨论。
判断坐标是否合法,只要判断判断横坐标和纵坐标取值范围在行数ROW和列数COL范围即可。
if(x >= 1 && x <= row && y >= 1 && y <= col}){ //坐标合法性判断
...
}else {
printf("输入坐标非法,请重新输入!\n");
}
输入坐标不合法时,需要重新输入,如下图。
判断某个位置是否排查过,可以判断该位置在show棋盘的值是否为'*',为'*'说明没有排查过,为非'*',说明已经被排查。
if (show[x][y] != '*') { //之前是否排查过
printf("该坐标被排查过了,不能重复排查\n");
}
如图,被排查过后,要求重新输入排雷坐标。
判断是否为为雷区。如果mine棋盘的值是为'1',说明该位置是雷,为'0'说明该位置为非雷。
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
}
当被炸死时,打印出地雷棋盘,让玩家了解地雷分布状态,游戏结束。
(2)统计非雷区周围雷数量
对于mine()棋盘上,想要找到坐标x,y)周围雷的数量,就将周围8个坐标雷的数量相加起来。
在实际上,假设某位置周围有8个地雷时,8个字符'1'相加时,得到的结果却是392,而不是对应雷数量8。为什么呢?
原来定义棋盘类型是char类型,而字符类型由ASCII编码。字符为'0'时,ASCII值为48,字符为'1'时,对应ASCII值为49。当8个字符'1'相加时,相当于为8*49=392,而不是对应雷数量8。
如果要想得到8,就需要在原来的基础上减去8*49,也可以减去8*'0',也是一样的效果。
get_mine_count()代码如下:
int get_mine_count(char board[ROWS][COLS],int x,int y) {
return (board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] -
8 * '0');
}
下面是统计非雷区周围雷数量。
(3)判断是否为赢
如果玩家赢了,则非雷区域意味着全部揭开,即满足:非雷区揭开数量=棋盘位置总数量-雷的数量。
按时上面的思路,每揭开一次非雷区域,进行iswin计数加1。实现代码如下:
if(mine[x][y] == '1') { //
printf("很遗憾,你被炸死了\n");
...
} else {
iswin++; //每揭开一次非雷区域,进行计数加1
...
}
排雷成功,打印出mine棋盘,让玩家了解地雷分布情况,如下:
综上所述,根据上面的分析,写出game()的完整代码:
//扫雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0; //定义玩家输入的横坐标
int y = 0; //定义玩家输入的纵坐标
int iswin = 0; //揭开未非雷的数量
while (iswin < row * col - EASY_COUNT) { //当揭开非雷数量小于棋盘全部网格减去雷数量时,说明没有赢,需要继续循环
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y); //玩家在屏幕输入x、y坐标
if (x >= 1 && x <= row && y >= 1 && y <= col) { //坐标合法性判断
if (show[x][y] != '*') { //之前是否排查过
printf("该坐标被排查过了,不能重复排查\n");
}
else {
//如果是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL); //被炸死时,为了让玩家死得明白,打印出雷的数量
break;
//如果不是雷
} else {
iswin++; //每揭开一次非雷区域,进行计数加1
//统计mine数组中x,y坐标周围有几个雷
int count = get_mine_count(mine, x, y); //获取非雷区周围雷的总数
show[x][y] = count + '0'; //转成数字字符
DisplayBoard(show, ROW, COL); //每扫一次雷,打印出显示棋盘
}
}
}
else {
printf("输入坐标非法,请重新输入!\n");
}
}
if (iswin == row * col -EASY_COUNT) { //iswin为未揭开雷的数量,当条件成立时,说明全部非雷都揭开
printf("恭喜你,排雷成功!\n");
DisplayBoard(mine, ROW, COL); //当玩家赢时,打印所有雷
}
}
2.3 附上扫雷基础版代码
扫雷基础版代码写到这里基本结束了,下面附上扫雷基础版的完整代码。包括game.h、main.c、game.c代码。
2.3.1 game.h文件
#define _CRT_SECURE_NO_WARNINGS
#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 40
//初始化
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);
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
2.3.2 main.c文件
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu() {
printf("****************************\n");
printf("****** 1. play ******\n");
printf("****** 0. exit ******\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, '*');
// 埋雷
SetMine(mine,ROW,COL);
// 显示棋盘
DisplayBoard(show, ROW, COL);
// 排查雷
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;
}
2.3.3 game.c文件
#define _CRT_SECURE_NO_WARNINGS
#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; //当初始化的棋盘是存放雷棋盘,set内容为'0',当初始化棋盘是显示棋盘,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++) { // 控制打印棋盘的列
printf("%d ",i); //打印纵坐标的索引
for (j = 1; j <= col; j++) {
printf("%c ",board[i][j]); //打印棋盘内容
}
printf("\n");
}
printf("--------扫雷--------\n");
}
// 埋雷
//'0'表示非雷状态
//'1'表示有雷状态
void SetMine(char board[ROWS][COLS], int row, int col) {
int count = EASY_COUNT; //EASY_COUNT为埋雷个数,在头文件定义
//横坐标1~9
//纵坐标1~9
while (count) {
int x = rand() % row + 1; //随机生成x坐标
int y = rand() % col + 1; //随机生成y坐标
if (board[x][y] == '0') { //非雷状态才可以设置成雷
board[x][y] = '1'; //设置成雷
count--; //标志设置雷的数量
}
}
}
// 统计非雷区周围雷数量
int get_mine_count(char board[ROWS][COLS],int x,int y) {
return (board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] -
8 * '0');
}
//扫雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0; //定义玩家输入的横坐标
int y = 0; //定义玩家输入的纵坐标
int iswin = 0; //揭开非雷的数量
while (iswin < row * col - EASY_COUNT) { //当揭开非雷数量小于显示棋盘全部网格减去雷数量时,说明没有赢,需要继续循环
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y); //玩家在屏幕输入x、y坐标
if (x >= 1 && x <= row && y >= 1 && y <= col) { //坐标合法性判断
if (show[x][y] != '*') { //之前是否排查过
printf("该坐标被排查过了,不能重复排查\n");
}
else {
//如果是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL); //被炸死时,为了让玩家死得明白,打印出雷的数量
break;
//如果不是雷
} else {
iswin++; //每揭开一次非雷区域,进行计数加1
//统计mine数组中x,y坐标周围有几个雷
int count = get_mine_count(mine, x, y); //获取非雷区周围雷的总数
show[x][y] = count + '0'; //转成数字字符
DisplayBoard(show, ROW, COL); //每扫一次雷,打印出显示棋盘
}
}
}
else {
printf("输入坐标非法,请重新输入!\n");
}
}
if (iswin == row * col -EASY_COUNT) { //iswin为未揭开雷的数量,当条件成立时,说明全部非雷都揭开
printf("恭喜你,排雷成功!\n");
DisplayBoard(mine, ROW, COL); //当玩家赢时,打印所有雷
}
}
3. 扫雷优化版
上面扫雷基础版只完成扫雷的最基本功能,没有第一次排雷避开雷、地雷标记、自动展开一片功能。下面对扫雷基础版进行增加功能。
下面是打雷基础版的的演示:
3.1 第一次排雷避开雷功能
当我们第一次排雷时,如果运气不好,可能第一次排雷时,就会踩到雷,然后导致游戏直接结束,浪费时间,影响体验。
对于上面的情况,我们最好能实现第一次排雷时,避免踩到雷。对于上面功能的实现有两种解决方法:方法一,在玩家输入第一个排雷坐标时,才进行埋雷,埋雷时避免对此坐标进行埋雷。方法二,当玩家第一次排雷踩到雷时,将该雷移到其它地方,以避免踩雷。
感觉第二方法比较好,于是采取第二种方法。当玩家第一次排雷踩时,将雷随机移到其它地方。代码如下:
//转移雷
void AvoidMine(char mine[ROWS][COLS], int row, int col,int a,int b) {
while (1) {
int x = rand() % row + 1; // 随机生成雷的新位置横坐标
int y = rand() % col + 1; // 随机生成雷的新位置纵坐标
if (mine[x][y] == '0' ) { // 保证新坐标没有雷,同时也意味着新生成坐标不会是原来坐标,因为原来坐标是有雷的
mine[x][y] = '1'; // 在新坐标设置雷
mine[a][b] = '0'; // 原坐标设置为非雷
break; //完成转移雷操作后,跳出循环
}
}
}
3.2 地雷标记与取消标记功能
在扫雷的过程中,如果确定某个位置有雷,需要对该位置进行标记,方便后续的扫雷。具体效果如下:
3.2.1 地雷标记
用字符'P'表示地雷标记,在show棋盘上,只需将'*'改为为'P'。
//地雷标记,用'P'表示
void MarkMine(char show[ROWS][COLS], int row, int col) {
int x = 0; //标记地雷横坐标
int y = 0; //标记地雷纵坐标
printf("标记中,输入坐标:>");
scanf("%d %d",&x,&y);
if (show[x][y] == '*') {
show[x][y] = 'P'; //标记地雷
DisplayBoard(show, ROW, COL); // 标记成功后,打印show棋盘
} else {
printf("无法标记!\n");
}
}
3.2.2 取消地雷标记
取消标记时,将show棋盘中的‘P’改为'*'
//取消标记,用'*'表示
void UnMarkMine(char show[ROWS][COLS], int row, int col) {
int x = 0; // 取消标记横坐标
int y = 0; // 取消标记纵坐标
printf("取消标记中,输入坐标:>");
scanf("%d %d",&x,&y);
if (show[x][y] == 'P') {
show[x][y] = '*'; //取消地雷标记
DisplayBoard(show, ROW, COL);// 取消标记成功后,打印show棋盘
} else {
printf("无需取消标记!\n");
}
}
3.3 排雷自动展开一片
完成排雷标记和取消标记后,下面编写排雷自动展开一片。
排雷自动展开一片,指的是当你对某个位置进行排雷时,当该位置周围显示为0个雷时,对其周围的8个位置的周围雷进行统计,当某个位置显示为0个雷时,按照第一次展开方式,进行周围雷统计、为0展开,直到展开的格子周围显示有雷才停止展开(如果出现展开越界、之前展开过等,展开数会少于8个)。
下图为周围雷数为0时,自动展开一片:
上面的代码可以使用 AutoExpand()递归来实现。当周围雷的数量为0时,对该位置周围8个位置进行递归调用。
对于递归AutoExpand()调用条件,我们规定如下:
1、递归位置坐标要合法,不能越界。
保证递归的位置始终在棋盘上。
2、该位置不能是雷;
展开一片功能,揭开的是非雷区,而不是雷区,即使该雷周围雷数为0。
3、该位置周围雷数量为0;
如果周围有雷,递归时会被炸死,所以不能为0;
4、该位置之前没有排查过;
防止重复递归,造成死循环。
根据上面的条件,编写AutoExpand()代码:
void AutoExpand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* iswin) {
if (1 <= x && x <= ROW && 1 <= y && y <= COL) { // 防止递归调用时位置越界
if (mine[x][y] == '0') { //位置不是雷才继续展开
if (show[x][y] == '*') { // 之前没有排查过,才继续展开(当该值为'*'时,表示之前没有排雷过)
show[x][y] = '0' + get_mine_count(mine, x, y); // 计算周围个数
(*iswin)++; //每计算周围数量时,iswin就加1
if (show[x][y] == '0') { //当统计为0时,进行递归展开
show[x][y] = ' ';
AutoExpand(mine, show, x - 1, y - 1, iswin);
AutoExpand(mine, show, x - 1, y, iswin);
AutoExpand(mine, show, x - 1, y + 1, iswin);
AutoExpand(mine, show, x, y - 1, iswin);
AutoExpand(mine, show, x, y + 1, iswin);
AutoExpand(mine, show, x + 1, y - 1, iswin);
AutoExpand(mine, show, x + 1, y, iswin);
AutoExpand(mine, show, x + 1, y + 1, iswin);
}
}
}
}
}
自动展开一片,效果如下:
可以将字符'0'替换成字符空格' ',会更加简洁。
3.4 扫雷优化版代码
下面附上扫雷基础版的完整代码。包括game.h、main.c、game.c代码。
3.4.1 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 5
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int row,int col, char set);
//显示棋盘
void DisplayBoard(char board[ROWS][ROWS],int row, int col);
//埋雷
void SetMine(char board[ROWS][COLS], int row, int col);
//扫雷
void StartGame(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
3.4.2 main.c文件
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu() {
printf("****************************\n");
printf("***** 1.play 0.exit *****\n");
printf("****************************\n");
}
void game() {
//创建棋盘
char mine[ROWS][COLS] = {0}; //存放雷的棋盘
char show[ROWS][COLS] = {0}; //展示雷的棋盘
//初始化棋盘
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');
//埋雷
SetMine(mine,ROW,COL);
//显示棋盘
DisplayBoard(show, ROW, COL);
//扫雷
StartGame(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;
}
3.4.3 game.c文件
#define _CRT_SECURE_NO_WARNINGS
#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][ROWS], int row, int col) {
printf("--------扫雷--------\n");
int i = 0;
//打印x坐标的索引
for (i = 0; i <= row; i++) {
printf("%d ",i);
}
printf("\n");
for (i = 1; i <= row; i++) {
int j = 0;
//打印y坐标的索引
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 = 1;
//设置雷的数量
while (count <= EASY_COUNT) {
//生成两个随机值
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0') {
board[x][y] = '1';
count++;
}
}
}
//计算周围雷的数量
int get_mine_count(char mine[ROWS][COLS],int x, int y) {
return (
mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
mine[x][y - 1] + mine[x][y + 1] +
mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] -
8 * '0');
}
//展开一遍
void AutoExpand(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* iswin) {
if (1 <= x && x <= ROW && 1 <= y && y <= COL) { // 位置坐标要合法
if (mine[x][y] == '0') { // 位置不是雷才继续展开
if (show[x][y] == '*') { // 之前没有排查过,才继续展开(当该值为'*'时,表示之前没有排雷过)
show[x][y] = '0' + get_mine_count(mine, x, y); // 计算周围个数
(*iswin)++; // 每计算周围数量时,iswin就加1,用来判断玩家是否赢的一个标记
if (show[x][y] == '0') { // 周围雷数量为0,进行递归展开
show[x][y] = ' ';
AutoExpand(mine, show, x - 1, y - 1, iswin);
AutoExpand(mine, show, x - 1, y, iswin);
AutoExpand(mine, show, x - 1, y + 1, iswin);
AutoExpand(mine, show, x, y - 1, iswin);
AutoExpand(mine, show, x, y + 1, iswin);
AutoExpand(mine, show, x + 1, y - 1, iswin);
AutoExpand(mine, show, x + 1, y, iswin);
AutoExpand(mine, show, x + 1, y + 1, iswin);
}
}
}
}
}
//转移雷
void AvoidMine(char mine[ROWS][COLS], int row, int col,int a,int b) {
while (1) {
int x = rand() % row + 1; // 随机生成雷的新位置横坐标
int y = rand() % col + 1; // 随机生成雷的新位置纵坐标
if (mine[x][y] == '0' ) { // 保证新坐标没有雷,同时也意味着新生成坐标不会是原来坐标,因为原来坐标是有雷的
mine[x][y] = '1'; // 在新坐标设置雷
mine[a][b] = '0'; // 原坐标设置为非雷
break; //完成转移雷操作后,跳出循环
}
}
}
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int *iswin) {
//定义两个排雷坐标变量
int x = 0;
int y = 0;
printf("排雷中,输入坐标:>");
scanf("%d %d", &x, &y);
//判断坐标合法性
if (1 <= x && x <= row && 1 <= y && y <= col) {
//判断此位置之前是否排查过
if (show[x][y] == '*') {
//判断是不是雷
if (mine[x][y] == '1') { //如果是雷
if ((*iswin) == 0) { // 判断是否是第一次扫雷
AvoidMine(mine,ROW,COL,x,y); // 转移雷
} else {
printf("你被炸死了,已输!\n");
//炸死时, 最好把所有雷的坐标打印出来
DisplayBoard(mine, ROW, COL);
(*iswin) = -1; //如果被炸死,直接把标志iswin设置-1
}
} else { //该位置不是雷
// 自动展开一片
AutoExpand(mine, show, x, y,iswin,1);
//显示出打印棋盘
DisplayBoard(show, ROW, COL);
}
}
else {
printf("此位置已经排雷过!\n");
}
}
else {
printf("坐标不合法!\n");
}
if (*iswin == row * col - EASY_COUNT) {
printf("你赢了,所有雷已排完!\n");
DisplayBoard(mine, ROW, COL);
}
}
//地雷标记,用'P'表示
void MarkMine(char show[ROWS][COLS], int row, int col) {
int x = 0; //标记地雷横坐标
int y = 0; //标记地雷纵坐标
printf("标记中,输入坐标:>");
scanf("%d %d",&x,&y);
if (show[x][y] == '*') {
show[x][y] = 'P'; //标记地雷
DisplayBoard(show, ROW, COL); // 标记成功后,打印show棋盘
} else {
printf("无法标记!\n");
}
}
//取消标记,用'*'表示
void UnMarkMine(char show[ROWS][COLS], int row, int col) {
int x = 0; // 取消标记横坐标
int y = 0; // 取消标记纵坐标
printf("取消标记中,输入坐标:>");
scanf("%d %d",&x,&y);
if (show[x][y] == 'P') {
show[x][y] = '*'; //取消地雷标记
DisplayBoard(show, ROW, COL);// 取消标记成功后,打印show棋盘
} else {
printf("无需取消标记!\n");
}
}
//开始游戏
void StartGame(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int input = 0;
//设置赢的标志
//iswin为row * col - EASY_COUNT为赢
//iswin为-1表示被炸死
int iswin = 0;
while (iswin != row * col - EASY_COUNT && iswin != -1) { //玩家赢或者被炸死,跳出循环
printf("1.扫雷 2.标记 3.取消标记\n");
printf("请选择:>");
scanf("%d", &input);
printf("-------------------------\n");
switch (input) {
case 1:
FindMine(mine,show,row,col,&iswin); // 打雷
break ;
case 2:
MarkMine(show,row,col); // 地雷标记
break;
case 3:
UnMarkMine(show, row, col); // 取消标记地雷
break;
default:
printf("输入错误!\n");
}
}
}
学习记录写到这里基本结束了,本次学习主要是对扫雷基础版和优化版的两个版本进行编写,让扫雷游戏功能逐渐完善。如果文章写不对的地方,欢迎提出讨论解决,谢谢。
附上gitee基础版和优化版的源码,一起讨论学习~,链接:https://gitee.com/yiqixuexiba/Minesweeper
参考资料:比特鹏哥C语言教学视频