C语言——扫雷游戏

更多C语言实战项目,欢迎浏览C语言——三子棋游戏

扫雷游戏简介

扫雷游戏功能说明

  1. 使用控制台实现
  2. 游戏提供一个菜单界面,可以根据用户输入来决定开始游戏或者退出游戏
  3. 扫雷区域为9*9的棋盘
  4. 默认随机布置10个雷
  5. 开始游戏后,用户输入坐标,程序根据用户输入的合法坐标对该坐标进行判断:
    1.如果坐标位置不是雷,就显示该坐标周围雷的个数
    2.如果坐标位置是雷,则提示用户踩雷了,游戏结束,程序终止结束
  6. 游戏通关的条件:把除10个雷以外的其他位置全部找出来了,扫雷胜利,游戏结束

游戏的界面

  1. 游戏开始时的菜单选择界面
    在这里插入图片描述
  2. 游戏开始后初始界面
    在这里插入图片描述
  3. 排雷过程界面(输入坐标(5 5),显示出该坐标周围有1个雷)
    输入坐标5 5,显示出该坐标周围有1个雷
  4. 踩雷界面(输入坐标(5 4),坐标(5 4)为雷,游戏结束,打印雷的布置情况和选择菜单)
    在这里插入图片描述
  5. 扫雷胜利界面(这里为了胜利方便,设置了80个雷,只有一个位置不是雷,坐标2 2不是雷。把所有不是雷的位置都找完了,扫雷胜利,打印出雷的布置情况和选择菜单)
    在这里插入图片描述

C语言代码实现

一个项目多个文件共同实现

多文件写代码的方式可以让我们的写的代码的逻辑结构更加清晰,还有就是,代码多的话,全部写在一个 .c文件中会显得很冗杂;一个项目多个文件实现的形式同时也符合实际工作中一个项目的实现过程,有利于我们养成良好的编程习惯。
扫雷游戏可以用三个文件实现:

game.h

头文件,用来包含项目用到的所有头文件,一些宏定义也写在这个文件中,还有项目中用到的函数也在这个文件中声明。

func.c

这个 .c 文件是用来实现大部分基本函数的。(fun.c 中没有main()函数)

game.c

这个 .c 文件是扫雷游戏的主体,包含main() 函数。

扫雷游戏实现逻辑分析

在这里插入图片描述
有了以上的逻辑,就可以大概写出扫雷游戏的主体代码了,如下:

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请输入你的选择->");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出游戏!");
			break;
		case 1:
			printf("******扫雷游戏开始******\n");
			game();
			break;
		default:
			printf("输入错误!请按要求输入!");
			break;
		}
	} while (input);
	return 0;
}

实现menu() 函数

menu()的实现比较简单,printf()打印想要的效果就行。

//菜单
void menu()
{
	printf("*********************\n");
	printf("******* 1.PLAY ******\n");
	printf("******* 0.EXIT ******\n");
	printf("*********************\n");
}

因为程序一开始运行就要打印菜单,所以采用do_while() 循环。

实现game() 函数

首先,根据前文的对扫雷游戏的逻辑分析可知,game()函数要实现几个功能:

  1. 设置棋盘(至少能存储9*9个元素的二维数组)
  2. 初始化棋盘(对二维数组进行初始化)
  3. 布置雷(在9*9的二维数组中随机布置10个雷)
  4. 打印棋盘(在控制台端显示界面)
  5. 排查雷(提示用户输入想要排查的坐标,根据该坐标排查雷。是雷则游戏结束,不是雷则显示该坐标周围雷的个数。)

设置棋盘

这里很容易想到是用二维数组当作棋盘。确实,设置棋盘这一步骤,我用了两个字符类型的,大小为11*11的二维数组:mine[11][11]和show[11][11]。

mine数组是用来存储雷的布局的,正常情况下不给用户看,只有触发了游戏结束的条件的时候(踩雷了或者游戏胜利了)才打印mine数组给用户看;show数组是给用户看的,是游戏的主要界面,用户输入要排查的坐标,show数组更新一次,显示该坐标周围雷的个数。

这里对设置棋盘为什么用**两个字符类型的,大小为11*11的二维数组**解释一下:

为什么用两个二维数组

为了方便后续操作。两个数组,一个存储雷的信息(非必要不给用户看);一个存储排查的雷的信息(就是给用户看的);逻辑简单清晰易懂。试想一下,如果只用一个数组,这个数组要存储雷的布局信息,还要存储排查雷的信息,还要判断哪个是要给用户看的才能打印显示,这样逻辑就会很冗杂,后面代码的实现也很复杂。

所以,为了方便后续操作,用两个数组好些。

为什么用字符类型的数组

首先,这个show数组(程序运行从头到尾都给用户看),为了美观性,采用全部初始化为字符 ‘*’ 的形式。如果用数字类型的数组的话,在后面排雷过程中容易混淆。比如采用数字类型数组,把show数组全部初始化为数字8,当你排查的坐标周围也有8个雷的时候,也显示8,这就混淆,也不美观。

所以,以上这些因素决定了show数组定义成字符类型的二维数组比较好。

至于mine数组也定义为字符类型,是为了后面写打印数组的函数的时候,只用封装成一个函数。其实也不是必要的,只是为了后面写代码的时候比较方便。如果mine数组定义为数字类型的,后面封装打印数组的函数的时候,就需要两个函数,一个打印 char 类型,一个打印 int 类型。

为什么用11*11大小的数组

为了防止后面排查雷的过程中方便,访问数组的时候不会越界。

考虑到后面排查雷的过程中要计算给定的坐标周围八个位置的雷的个数:
在这里插入图片描述

如果是9*9大小的数组的话,排查位置在周围一圈的坐标的时候就只用计算周围5个位置雷的个数;位于四个角坐标的位置,只用排查周围3个位置雷的个数,这就要求要分很多种情况进行分析,否则如果还是按周围8个位置的方式统计周围雷的个数,就会因为数组访问越界而报错。

但是,采用11*11的二维数组就不用这样,只要多出来的位置不布置雷就好(全部为字符‘0’),在统计雷的个数的时候就采用同一个函数就好,不用分类,方便快捷高效。

初始化棋盘

这里我们在头文件game.h中对行和列进行宏定义,方便以后对游戏的升级或者优化。同时对后面要用到的头文件进行包含:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//宏定义行和列
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10

然后在func.c文件中对game()函数进行编写

void game()
{
	//创建两个二维数组作棋盘用,
	/*mine数组用来存储雷的位置信息,
		show数组用来记录排查出的雷的信息*/
	char mine[ROWS][COLS];
	char show[ROWS][COLS];

	//以下两个语句是对mine数组和show数组进行初始化处理
	InitBoard(mine, ROWS, COLS, '0');//把mine数组11*11个元素全部初始化为字符‘0’
	InitBoard(show, ROWS, COLS, '*');//把show数组11*11个元素全部初始化为字符‘*’
}

接着我们实现 InitBoard()函数:
InitBoard()函数的返回值是 void,有三个参数,可以用for循环遍历二维数组中的每个元素,把所有元素都初始化成想要的字符。

void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag);
//初始化棋盘的函数,有四个参数,棋盘数组的名称,行数,列数,初始化的字符
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag)
{
	int j;
	for (j = 0; j < rows; j++)
	{
		int k;
		for (k = 0; k < cols; k++)
		{
			board[j][k] = flag;
		}
	}
}

布置雷

对两个数组初始化结束后就该往mine数组里面随机布置雷了。

这里用要用到C语言生成随机数相关知识:设置随机数种子,包含相应的头文件等。这里我们利用一个循环实现布置雷的操作,while()循环,用count作为进入循环的条件,count初始化为10,每次循环都生成一对1~9之间的随机数作为雷的坐标,然后对该坐标进行判断,如果该坐标没有布置过雷,即mine数组中该坐标为字符‘0’的话,把‘0’改为‘1’,这样就成功在mine数组中布置了一个雷,同时count–,直到10个雷全部都布置完了,count=0,跳出while()循环。

void SetMine(char mine[ROWS][COLS], int row, int col);
//布置雷的函数,三个参数,数组名称,行数,列数
void SetMine(char mine[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT; //在头文件中宏定义了雷的个数为10
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

打印棋盘

和前文的初始化棋盘类似,用for循环遍历想要打印的二维数组,用printf()函数输出就好。这里为了界面的美观性,还顺带把行号和列号也打印了。
在这里插入图片描述

void Print(char board[ROWS][COLS], int row, int col);
//打印棋盘的函数。三个参数,数组名称,行数,列数
//打印数组
void Print(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("------扫雷游戏------\n");
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= ROW; i++)//只打印中间的九乘九的二维数组
	{
		int j = 0;
		printf("%d ", i);
		for (j = 1; j <= COL; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

排查雷

排查雷这里也用了一个循环实现,因为在没踩雷前,要一直循环输入坐标并判断位置是否为雷。循环的条件是win<ROWCOL-EASY_COUNT,这里提前定义了一个变量win用来表示找到的非雷坐标的个数,当win<ROWCOL-EASY_COUNT时,表明游戏还没结束,用户还可以继续输入坐标排雷,而一旦用户输入的坐标位置是雷的话,则用break跳出循环。

同时,在排雷的函数中,也考虑到了用户输入非法的问题,排雷的坐标要求必须在1到9之间,这里用if 语句进行判断,如果用户输入非法,则提示,并继续循环。

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//排查雷的函数,四个参数,布置雷的数组名称,用来展示的数组名称,行数,列数
//排查雷的函数
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)
	{
		printf("请输入你要排查的坐标->");
		scanf("%d %d", &x, &y);
		if (x > 0 && x <= ROW && y > 0 && y <= COL)
		{
			if (mine[x][y] == '1')
			{
				printf("游戏结束!你踩到雷了!\n");
				Print(mine, ROW, COL);
				break;
			}
			else
			{
				int num = GetMineCount(mine,x,y); //统计(x y)坐标周围的8个位置共有几个雷
				show[x][y] = num + '0';
				Print(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("输入坐标错误!请重新输入\n");
		}
	}
	if (win == ROW * COL - EASY_COUNT)
	{
		printf("恭喜你!游戏通关!\n");
		Print(mine, ROW, COL);
	}
}

排雷函数中还包含了计算坐标周围雷的个数的函数,GetMineCount(),该函数返回一个int类型的数据,表明坐标周围雷的个数。

这里说明一下,因为之前在初始化棋盘的时候,为了方便,把布置雷的mine数组设置成了一个字符类型的数组,这里计算周围8个坐标雷的个数就不能简单的相加,而是要转化成数字再相加。

数字1 + 字符‘0’ = 字符‘1’
字符‘1’ - 字符‘0’ = 数字1

所以,坐标(x y) 处周围雷的个数可以把周围8个位置的字符元素加起来再减去8个字符‘0’。

//数x,y位置周围雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	int count = (mine[x - 1][y] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x + 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x - 1][y - 1] + mine[x - 1][y + 1] - 8 * '0');
	return count;
}

在这里插入图片描述
以上就是一步步实现C语言控制台扫雷游戏的过程

扫雷C语言源代码

game.h

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//宏定义行和列
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2

#define EASY_COUNT 10

//函数的声明
void menu();//菜单打印

void game();//游戏运行体


void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag);
//初始化棋盘的函数,有四个参数,棋盘数组的名称,行数,列数,初始化的字符


void Print(char board[ROWS][COLS], int row, int col);
//打印棋盘的函数。三个参数,数组名称,行数,列数

void SetMine(char mine[ROWS][COLS], int row, int col);
//布置雷的函数,三个参数,数组名称,行数,列数

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//排查雷的函数,四个参数,布置雷的数组名称,用来展示的数组名称,行数,列数

func.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"game.h"

//函数的实现
// 
//菜单
void menu()
{
	printf("*********************\n");
	printf("******* 1.PLAY ******\n");
	printf("******* 0.EXIT ******\n");
	printf("*********************\n");
}

//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag)
{
	int j;
	for (j = 0; j < rows; j++)
	{
		int k;
		for (k = 0; k < cols; k++)
		{
			board[j][k] = flag;
		}
	}
}

//打印数组
void Print(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("------扫雷游戏------\n");
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= ROW; i++)//只打印中间的九乘九的二维数组
	{
		int j = 0;
		printf("%d ", i);
		for (j = 1; j <= COL; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

//数x,y位置周围雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	int count = (mine[x - 1][y] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x + 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x - 1][y - 1] + mine[x - 1][y + 1] - 8 * '0');
	return count;
}

//排查雷
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)
	{
		printf("请输入你要排查的坐标->");
		scanf("%d %d", &x, &y);
		if (x > 0 && x <= ROW && y > 0 && y <= COL)
		{
			if (mine[x][y] == '1')
			{
				printf("游戏结束!你踩到雷了!\n");
				Print(mine, ROW, COL);
				break;
			}
			else
			{
				int num = GetMineCount(mine,x,y);
				show[x][y] = num + '0';
				Print(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("输入坐标错误!请重新输入\n");
		}
	}
	if (win == ROW * COL - EASY_COUNT)
	{
		printf("恭喜你!游戏通关!\n");
		Print(mine, ROW, COL);

	}
}

//游戏主体
void game()
{
	//创建两个二维数组作棋盘用,
	/*mine数组用来存储雷的位置信息,
		show数组用来记录排查出的雷的信息*/
	char mine[ROWS][COLS];
	char show[ROWS][COLS];

	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	SetMine(mine,ROW,COL);//设置雷的位置
	Print(show, ROW, COL);//打印*界面给用户看
	Print(mine,ROW,COL);//打印雷的位置
	FindMine(mine, show, ROW, COL);//排查雷
}

game.c

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include "game.h"
#include<time.h>

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请输入你的选择->");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出游戏!");
			break;
		case 1:
			printf("******扫雷游戏开始******\n");
			game();
			break;
		default:
			printf("输入错误!请按要求输入!");
			break;
		}
	} while (input);
	return 0;
}

思考和总结

扫雷游布置雷的数组采用的是字符类型的数组,很妙的一点:“数字字符”和“数字”之间的转换。
写代码重要的是实现的思路和逻辑。

拓展延伸

• 是否可以选择游戏难度
• 如果排查位置不是雷,周围也没有雷,可以展开周围的一片
• 是否可以标记雷
• 是否可以加上排雷的时间显示

  • 13
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值