C语言实现扫雷游戏学习记录

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语言教学视频

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值