Bootstrap

【C语言小项目】扫雷游戏 ——二维数组的应用

前言

  《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。(来自 百度搜索)

 当你学完二维数组之后,你可以拿这个项目来练手,不到300行的代码就可以加深你对这块知识的理解。作为一个游戏小项目,在编程的时候也会涉及到一部分的项目知识,可谓一举多得。那么话不多说,让我们开始完成扫雷游戏吧!

一、扫雷游戏规则

扫雷链接: minesweeper

1.功能分析

如下图所示为扫雷的初始界面:
在这里插入图片描述
功能:

  1. 难度选择区,分为基础、中级和专家三个级别,对应不同的棋盘格数和雷数;
    【初级】:雷区范围是9*9的方格,部署地雷10个

    【中级】:雷区范围是16*16的方格,部署地雷40个

    【高级】:雷区范围是16*30的方格,部署地雷99个雷

  2. 满屏按钮,可以让界面铺满全屏;

  3. 自定义按钮,可以自己设置棋盘大小和雷的数量;
    【自定义】:自己设定雷区大小和雷数,但是雷区大小不能超过24*30

  4. 雷数量的统计,倒数,每排掉一个雷减去1;

  5. 计时器,显示游戏耗时;

  6. 雷区显示区,显示排雷的情况;

  7. 成绩榜,可以查看成绩排名。

2.玩法分析

游戏的基本操作包括【左键单击】、【右键单击】、【双击】三种。

【左键单击】:在判断出不是雷的方块上按下左键,可以打开该方块并在方块上显示数字,该数字表示其周围3×3区域中的地雷数(一般为8个格子,对于边块为5个格子,对于角块为3个格子,所以扫雷中最大的数字为8);如果方块上为空(相当于0),则可以递归地打开与空相邻的方块。

【右键单击】:在判断出地雷的方块上按下右键,可以出现小红旗以此标记地雷。

【双击】:同时按下左键和右键即可完成双击操作。该操作仅仅在双击位置周围已标记地雷的数量等于该位置数字时才有效,相当于对该数字周围未打开的方块均进行一次左键单击操作,有效快速的打开安全方格。但是要注意的是,如果数字周围有标记错误的地雷,那么你打开的方格内必有地雷,游戏就此结束。
在这里插入图片描述

3.胜负判定条件

成功标记所有地雷,并且安全打开其余方格视为胜利,更高级的玩家会挑战同难度下游戏通关的耗时记录;如果误翻了地雷,则游戏结束。

二、游戏实现思路

  1. 不同难度级别,通过设置不同的棋盘大小和雷数量,它们应当是可变的;(分为基础、中级和专家三个级别)
  2. 使用坐标输入代替鼠标点击,坐标应该符合人们使用习惯从1开始;(【鼠标左击】功能)
  3. 随机在棋盘上生成雷,并记录下来,但是展示给玩家的是另一个界面,需要两张棋盘;
  4. 若踩到雷,提示失败,并打印出雷的布置;若没有踩到雷,显示出排雷坐标周围有几个雷,并继续输入坐标,需要使用循环;(玩法)
  5. 排雷显示区,打印出合适大小的棋盘;(雷区显示区)
  6. 无雷区的扩展,如果方块上为空(相当于0),则可以递归地打开与空相邻的方块。(玩法)
  7. 设置变量X,记录已经排的空格数量,如果排的空格数量等于棋盘格数减去雷的数量,则显示游戏胜利。(胜负判定条件)

可以看出现在我们只能实现小游戏的部分功能,在之后学习别的部分可以再继续改进小游戏,我们本次是在命令行窗口做的小游戏,所以需要用坐标输入代替鼠标点击。

二、代码实现与函数封装

1.项目文件创建

将源代码分为三个文件,一个头文件(.h),用于声明函数,两个源文件(.c),一个用于写函数体,一个为主函数,调用函数。
在这里插入图片描述
其中,test.c为主函数,game.c写函数体。

2.头文件说明

  1. 为了防止在项目中多次申明同一个头文件,需要加入防重复判断语句
#ifndef __GAME_H__
#define __GAME_H__

// ...
#endif

或者使用现在的新写法

#pragma once
  1. 头文件中可以直接引用需要的标准库,然后在源文件中只需要引用自己写的头文件即可;
#include "game.h"

3.函数封装

我们先写9*9的初级棋盘,然后更改棋盘大小即可。

1)棋盘设置

为了之后记录坐标点周围八格的数量,在角落时只有两格或三格,不好统计,所以棋盘可以设置为1111,最后打印时只打印99即可,也同时解决了人们习惯输入坐标从1开始的问题。
图示如下:
在这里插入图片描述

//设置显示棋盘的大小
#define ROW 9
#define COL 9
//存储棋盘的大小
#define ROWS ROW+2
#define COLS COL+2

//1.布置好的雷的信息
char mine[ROWS][COLS] = {0};//11*11
//2.排查出的雷的信息
char show[ROWS][COLS] = {0};

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;
		}
	}
}

其中char set为初始化的字符,可以直接为两个棋盘初始化。

InitBoard(mine , ROWS, COLS, '0');//初始化设置成不同字符,加入一个参数
InitBoard(show , ROWS, COLS, '*');

3)棋盘打印

打印时只打印9*9的棋盘

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	//打印列号
	for(i =0; i<= col; i++)
	{
		printf("%d ", i);		
	}
	printf("\n");
	
	for(i = 1; i<=row; i++)//行号
	{
		printf("%d ", i);//打印行号
		for(j =1; j<=col; j++)//列号
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

函数传参,调用:

DisplayBoard(show, ROW, COL);

4) 雷的布置

雷的数量引入#define 定义标识符,可以随时更改
随机数的生成需要时间种子

//设置雷的个数
#define EASY_COUNT 10
srand((unsigned int)time(NULL));//时间种子,位于主函数中


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

5) 坐标周围八个位置雷的数量

int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	//'3' - '0' = 3
	//'1' - '0' = 1
	return mine[x-1][y] + 
	mine[x-1][y-1] + 
	mine[x][y-1] + 
	mine[x+1][y-1] + 
	mine[x+1][y] + 
	mine[x+1][y+1] + 
	mine[x][y+1] + 
	mine[x-1][y+1] - 8*'0'; //字符0,可以乘,计算的是ascii码值
	
}

6)无雷区的扩展

这个函数是为下一个排雷函数做准备。如果说我们输入一个坐标,这个坐>标没有雷的话,我们希望判断它周围的八个位置,有没有周围八个格都没有雷的,都没有雷的话就将它们直接排掉,优化游戏、更具可玩性。

这里需要注意,我们在排雷时需要加入胜利判断条件:
WIN < ROW*COL - EASY_COUNT

//设置雷的个数
#define EASY_COUNT 10
WIN = 0;
while(WIN <row*col - EASY_COUNT)

在进入这个函数体时如果WIN是局部变量那么,函数体内部的扩展区无法排掉,所以应该将WIN设置成全局变量,并且在每一次进入扫雷函数时置为0.

int WIN = 0;
void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], >int x, int y)
{
	//计算x, y周围有几个雷
	int num = GetMineCount(mine, x, y);

	if (num != 0)
	{
		show[x][y] = num + '0';//转化为字符		
	}
	else if(show[x][y] == '*')
	{
		//周围八个格都为空,则将它置空
		show[x][y] = ' ';
		WIN++; //全局变量记录已排的格数

		//递归
		for (int i = x - 1; i <= x + 1; i++)
		{
			for (int j = y - 1; j <= y + 1; j++)
			{
				ExpandBlank(mine, show, i, j);
			}
		}

	}
}

6)扫雷

int WIN = 0;

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;	
	
	WIN = 0;
	while(WIN <row*col - EASY_COUNT)
	{
		again:
		printf("请输入排查雷的坐标(中间用空格隔开):> ");
		scanf("%d%d", &x, &y);


		//输入坐标已被排查
		if (show[x][y] != '*')
		{
			printf("此坐标已被排查,请重新输入!\n");//已被排查时提示
			goto again;
		}

		if(x>=1 && x<= row && y>=1 && y<=col)
		{
			//坐标合法
			//1.踩雷
			if(mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了!\n");
				DisplayBoard(mine, row, col);
				break;//跳出
			}
			else//不是雷
			{
				//先判断是否无雷,需要扩展
				ExpandBlank(mine, show, x, y);
				
				//打印棋盘
				DisplayBoard(show, row, col);//显示扩展后的棋盘
				WIN++;
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入!\n");	
		}
	}
	if(WIN == row*col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功!\n");
		DisplayBoard(mine, row, col);
	}
}

三、源码分享

1.test.c

#include "game.h"

void menu()
{
	printf("*************************\n");
	printf("****     1. play     ****\n");
	printf("****     0. exit     ****\n");
	printf("*************************\n");
}

void game()
{
	//雷的信息存储
	
	//1.布置好的雷的信息
	char mine[ROWS][COLS] = {0};//11*11
	//2.排查出的雷的信息
	char show[ROWS][COLS] = {0};
	//初始化
	InitBoard(mine , ROWS, COLS, '0');//初始化设置成不同字符,加入一个参数
	InitBoard(show , ROWS, COLS, '*');

	//打印棋盘
	DisplayBoard(show, ROW, COL);
	
	
	//布置雷
	SetMine(mine, ROW, COL);
	/*printf("\n");
	DisplayBoard(mine, ROW, COL);*/

	//扫雷
	// 
//	DisplayBoard(mine, ROW, COL);
	FindMine(mine, show, ROW, COL);
}

void test()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch(input)
		{
		case 1:
			printf("      扫雷\n");
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,请重新选择!\n");
			break;
		}
	}
	while(input);
}


int main()
{
	test();
	return 0;
}

2.game.c

#define _CRT_SECURE_NO_WARNINGS 1

#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;
		}
	}
}

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	//打印列号
	for(i =0; i<= col; i++)
	{
		printf("%d ", i);		
	}
	printf("\n");
	
	for(i = 1; i<=row; i++)//行号
	{
		printf("%d ", i);//打印行号
		for(j =1; j<=col; j++)//列号
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	//'3' - '0' = 3
	//'1' - '0' = 1
	return mine[x-1][y] + 
	mine[x-1][y-1] + 
	mine[x][y-1] + 
	mine[x+1][y-1] + 
	mine[x+1][y] + 
	mine[x+1][y+1] + 
	mine[x][y+1] + 
	mine[x-1][y+1] - 8*'0'; //字符0,可以乘,计算的是ascii码值
	
}

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

int WIN = 0;

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;	
	
	WIN = 0;
	while(WIN <row*col - EASY_COUNT)
	{
		again:
		printf("请输入排查雷的坐标(中间用空格隔开):> ");
		scanf("%d%d", &x, &y);


		//输入坐标已被排查
		if (show[x][y] != '*')
		{
			printf("此坐标已被排查,请重新输入!\n");
			goto again;
		}

		if(x>=1 && x<= row && y>=1 && y<=col)
		{
			//坐标合法
			//1.踩雷
			if(mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了!\n");
				DisplayBoard(mine, row, col);
				break;//跳出
			}
			else//不是雷
			{
				//先判断是否无雷,需要扩展
				ExpandBlank(mine, show, x, y);
				
				//打印棋盘
				DisplayBoard(show, row, col);//显示扩展后的棋盘
				WIN++;
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入!\n");	
		}
	}
	if(WIN == row*col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功!\n");
		DisplayBoard(mine, row, col);
	}
}


void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
	//计算x, y周围有几个雷
	int num = GetMineCount(mine, x, y);

	if (num != 0)
	{
		show[x][y] = num + '0';//转化为字符		
	}
	else if(show[x][y] == '*')
	{
		//周围八个格都为空,则将它置空
		show[x][y] = ' ';
		WIN++;

		//递归
		for (int i = x - 1; i <= x + 1; i++)
		{
			for (int j = y - 1; j <= y + 1; j++)
			{
				ExpandBlank(mine, show, i, j);
			}
		}

	}
}

3.game.h

#ifndef __GAME_H__
#define __GAME_H__

//#pragma once

#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 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);
//判断周围是否有无雷区
void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);

#endif

总结

怎么样,学完了这个项目,你对数组有没有更好的认识呢?在后续的过程中,会慢慢的学习到枚举类型、图形化界面等等,到时候你可以再继续优化这个项目,让它变得更完美。

 有什么不懂的,也可以在评论区提出问题哟,对于里面出现的错误也欢迎各位伙伴积极指出,谢谢大家的支持!

;