c语言期中项目实战二—简易扫雷,思路分析加代码详细注释

游戏介绍

扫雷这个经典游戏,直到现在仍有很多人在玩,可以说它是全世界最多人玩过的游戏之一,对很多人来说甚至是他们在电脑上接触的第一款游戏。记得我上小学的时候也经常玩,今天我就写这篇文章简单地介绍一下写这个游戏的思路和分享一下代码,本篇文章讲解的是一个16*16(中级)扫雷,有40个雷,这篇文章只是实现了简单的排雷版本,1表示雷,0表示不是雷,你排一个位置的雷,有雷会被炸死,没雷显示周围雷的情况,不会一片的展开没雷的位置,你也可以自己设计一个几行几列的雷区,自定义雷的个数,只需要更改#define定义的宏常量即可。(这里只是实现后端的算法,其他的没有,前段界面的东西没有涉及)

项目步骤

模块化编程

多文件(模块化)开发C程序的方法,就是把不同功能的函数封装到不同的文件中,多个.c文件和一个.h文件,c语言中主函数调用其他文件中的函数实现相应的功能。此次项目也将程序分为三个模块,各模块功能如下
test.c :扫雷游戏的测试
game.c:扫雷游戏的函数实现
game.h:游戏函数的声明

设置菜单

任何一个游戏你玩不玩之前都有一个菜单,你是要玩(play),还是不玩想退出(exit),使用do{ }while()循环,0为假,非0为真,即达到玩不玩都打印一次菜单,也达到了要么你输入0退出,要么输入1,输入其他的数字是错误的目的。

void menu()
{
	printf("******************************\n");
	printf("******    扫雷游戏     *******\n");
	printf("******    1. play     *******\n");
	printf("******    0. exit     *******\n");
	printf("*******  请选择数字    *******\n");
}

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));//使用rand函数之前调用srand函数

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

设置棋盘

其实设置雷区这里也简单的,只要理解几个关键步骤就行:

  1. 存放雷的棋盘比真实所放雷的格子行和列都多二

因为之后我们设置了雷,在排雷的时候,要检查我们所排位置周围的八个格子有几个雷,返回数字给玩家,让他们赢得游戏,但如果设置雷区没有比真正放雷的区域大的话,在排雷的时候,如果排到雷区边缘的话,就会产生数组越界的问题。下面以图形的方式大致了讲解一下。(这里把雷区大致比作棋盘,其实他们都有许多小格子的)

  1. 创建两个数组
    我们首先写这个代码时,用字符1表示雷,字符0表示非雷,我们首先想到的是创建一个二维数组,18行18列,然后放雷,之后放雷扫雷,遇到非雷的时候返回这个位置周围的雷的个数,如果是一个二维数组,假设我们扫雷了,那个无雷的位置返回了一个‘1’,本来是表示那个位置有一个雷的,但我们也会歧义的认为那是一个雷,所以不能只定义,使用一个数组。
    那有的老铁可能会说了,那我不使用字符1和0表示有雷和没雷,假如我使用$表示有雷,#表示没雷,这就不会产生歧义了吧,这也不行,你想,你初始化一个数组用#表示无雷,之后设置雷为 $,你把雷设置好,要不要打印雷区给人扫雷,扫雷时你得把雷藏起来啊,用个什么字符把它盖住啊,不让人发现,这时你又必须又要定义一个字符盖住雷,就像我们真实玩游戏时,一个小方格雷区被白色的区域隐藏一样。一个数组是无法实现设置了雷又把它隐藏的,所以我们设置两个数组,一个存放布置好雷的信息(暗地里的雷),另一个数组存放排雷的信息(就是让你看不见雷的障碍物的存放和排雷后标记出那个地方有几个雷的信息的存放)
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’,传过来放在数组的每一个元素里面,之后就初始化了成‘*’或者‘0’了
		}
	}
}

排雷的时候雷在数组边沿会产生数组越界问题如下图:
在这里插入图片描述

打印棋盘

定义和初始化了了棋盘,我们把棋盘打印出来,在打印的时候我们顺便打印行号和列号方便玩家排对应坐标的雷,行号和列号都是1—16

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	printf("---------扫雷游戏-----------\n");//为了好打印给的区分
	//打印列号
	for (i = 0; i <= col; i++)
	{
		printf("%2d  ", i);//因为行和列有两位数字所以设置为%2d,并且右对齐,使整个雷区棋盘整齐
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%2d  ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%2c  ", board[i][j]);//字符也占两位
		}
		printf("\n");
	}
	printf("---------扫雷游戏-----------\n");
}

布置雷

这里我们利用随即函数rand随机的产生40个数字,再利用取余产生有效的坐标,并在坐标处设置雷。

void SetMine(char mine[ROWS][COLS], int row, int col)//18*18的数组里随机得放上40个雷,但真实的是把雷放在里面16*16的数组里,
                                                     //所以要传过mine数组,形式上传过ROWS,COLS,但正真操作的是16*16的格子放置的雷,所以形参为row,col和要传递的实参对应也为row,col
{
	//布置10个雷
	int count = EASY_COUNT;//#define定义的宏常量,玩一个中级版本,设置40颗雷
	while (count)//count等于0的时候就结束循环
	{
		//生产随机的下标
		int x = rand() % row + 1;//产生1-row+1的随机数字
		int y = rand() % col + 1;
		if (mine[x][y] == '0')//如果是字符‘0’没放上雷,我们就放上雷
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

排查雷

在排查雷这里我们应用到一点c语言不同数据类型之间运算的一点小知识,就是int 型数据和char型数据相加,相减,以及字符和字符类型数据相加相减,这该怎么运算呢?经过我查阅资料阅读其他的文章明白了:
Char型与int型数据进行运算,就是把字符的ASCII码与整型数据进行运算,如果返回结果为整形就返回ASCLL码,返回结果为字符就返回ASCLL码对应的字符;字符与字符相运算也是字符对应的ASCLL相运算,看运算结果返回对应的值。
数字1+‘0’返回的如是字符‘1’,1加‘0’的ASCLL码,1+48=49,ASCLL,49对应的字符为‘1’,以此类推,一个数字加上‘0’为这个数字对应的字符
下面附上部分ASCLL码表:
在这里插入图片描述
下面是不同数据类型间混合运算的一篇博客,可以看看:https://blog.csdn.net/Fengjingdisan/article/details/76358642?utm_source=app&app_version=4.5.2

static int get_mine_count(char mine[ROWS][COLS], int x, int y)//get_mine_count 仅仅只为了支持Findmine函数而已,加了static让这个函数只能在自己所在的源文件使用,
{
	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';//这里的函数返回值是一个数字(ASCLL码值),之后在下面的FindMine函数里转换为字符数字,打印雷的个数
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	//1. 输入排查的坐标
	//2. 检查坐标处是不是雷
	   // (1) 是雷   - 很遗憾炸死了 - 游戏结束
	   // (2) 不是雷  - 统计坐标周围有几个雷 - 存储排查雷的信息到show数组,游戏继续

	int x = 0;
	int y = 0;
	int win = 0;

	while (win < row*col - EASY_COUNT)//所有的雷区减掉雷的个数是无雷区,当还有无雷区未排除时继续游戏
	{
		printf("请输入要排查的坐标:>");
		scanf("%d%d", &x, &y);//x--(1,16)  y--(1,16)

		//判断坐标的合法性
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, row, col);//打印一下你排的位置是雷,你是怎么死的
				break;//跳出循环,继续游戏
			}
			else
			{
				//不是雷情况下,统计x,y坐标周围有几个雷
				int count = get_mine_count(mine, x, y);
				show[x][y] = count + '0';
				//显示排查出的信息
				DisplayBoard(show, row, col);//显示该格子周围有几个雷
				win++;
			}
		}
		else
		{
			printf("坐标不合法,请重新输入\n");
		}
	}

	if (win == row * col - EASY_COUNT)//判断所有空白雷区排完,排雷成功
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, row, col);
	}
}

总结及总代码和详细注释

这篇文章只是实现了简单的排雷版本(16*16),你排一个位置的雷,有雷会被炸死,没雷显示周围雷的情况,不会一片的展开没雷的位置,之后我还会续上一个应用递归的展开一片的排雷的版本 的。
文章是我学习后的总结和分享,如有错误评论区留言指正,看了的xdm觉得还行的话,希望点赞鼓励,之后有时间我就会持续输出的。

递归版扫雷连接

下面是总的完整代码。
game.h

#pragma once

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

#define EASY_COUNT 40

#define ROW 16
#define COL 16

#define ROWS ROW+2
#define COLS COL+2

//函数的声明
//初始化棋盘的
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//三个形参

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);//(形参)row和col和实参呼应,虽然你打印中间的16行16列,但这个数组传过去任然是18行18列的数组

//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col);

//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

test.c

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"

void menu()
{
	printf("******************************\n");
	printf("******    扫雷游戏     *******\n");
	printf("******    1. play      *******\n");
	printf("******    0. exit      *******\n");
	printf("*******  请选择数字    *******\n");
}

void game()
{
	char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息
	char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
	//初始化棋盘
	InitBoard(mine, ROWS, COLS, '0');//'0','0'函数传参,首先传给set ,你想初始化什么你给我传什么
	InitBoard(show, ROWS, COLS, '*');//'*'

	//打印一下棋盘
	DisplayBoard(show, ROW, COL);//ROW,COL是实际参数

	//布置雷
	SetMine(mine, ROW, COL);//把雷放在18*18的数组里,所以传递mine 数组,但我们实际操作的是中间16*16的格子,所以传递ROW,COL,操作中间的格子
	//DisplayBoard(mine, ROW, COL);//注意这里不能让别人看见我们设置的雷,屏蔽调

	//排查雷
	FindMine(mine, show, ROW, COL);//
}

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));//传time函数的返回值,即时间戳。返回值强制类型转换为int

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






game.c

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"//包含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实参传递*或者‘1’,set放在数组的每一个元素里面,之后就初始化了成‘*’或者‘0’了
		}
	}
}

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	printf("---------扫雷游戏-----------\n");//为了好打印给的区分
	//打印列号
	for (i = 0; i <= col; i++)
	{
		printf("%2d  ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%2d  ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%2c  ", board[i][j]);
		}
		printf("\n");
	}
	printf("---------扫雷游戏-----------\n");
}


void SetMine(char mine[ROWS][COLS], int row, int col)//18*18的数组里随机得放上40个雷,但真实的是把雷放在里面16*16的数组里,
                                                     //传过mine数组,形式上传过ROWS,COLS,但正真操作的是16*16的格子放置雷,所以形参为row,col和要传递的实参对应也为row,col
{
	//布置10个雷
	int count = EASY_COUNT;//#define定义的宏常量,玩一个中级版本,设置40颗雷
	while (count)//count等于0的时候就结束循环
	{
		//生产随机的下标
		int x = rand() % row + 1;//产生1-row+1的随机数字
		int y = rand() % col + 1;
		if (mine[x][y] == '0')//如果是字符‘0’没放上雷,我们就放上雷
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

//static的三种用法
//1. 修饰局部变量
//2. 修饰全局变量
//3. 修饰函数

static int get_mine_count(char mine[ROWS][COLS], int x, int y)//get_mine_count 仅仅只为了支持Findmine函数而已,加了static让这个函数只能在自己所在的源文件使用,
{
	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';
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	//1. 输入排查的坐标
	//2. 检查坐标处是不是雷
	   // (1) 是雷   - 很遗憾炸死了 - 游戏结束
	   // (2) 不是雷  - 统计坐标周围有几个雷 - 存储排查雷的信息到show数组,游戏继续

	int x = 0;
	int y = 0;
	int win = 0;

	while (win < row*col - EASY_COUNT)//所有的雷区减掉雷的个数是无雷区,当还有无雷区未排除时继续游戏
	{
		printf("请输入要排查的坐标:>");
		scanf("%d%d", &x, &y);//x--(1,16)  y--(1,16)

		//判断坐标的合法性
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, row, col);//打印一下你排的位置是雷,你是怎么死的
				break;//跳出循环,继续游戏
			}
			else
			{
				//不是雷情况下,统计x,y坐标周围有几个雷
				int count = get_mine_count(mine, x, y);
				show[x][y] = count + '0';
				//显示排查出的信息
				DisplayBoard(show, row, col);//显示该格子周围有几个雷
				win++;
			}
		}
		else
		{
			printf("坐标不合法,请重新输入\n");
		}
	}

	if (win == row * col - EASY_COUNT)//判断但所有空白雷区排完,排雷成功
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, row, col);
	}
}
  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值