C语言实现扫雷(含展开,附源码)

我相信在做的各位都是玩过扫雷的,游戏玩法就不赘述了。


直接进入主题:先思考后敲代码!!

首先,我将扫雷分为两个棋盘,一个放雷,另一个为玩家猜测盘。

这就有同学问了,设置一个棋盘不就完了,这样不就搞复杂了吗?

先简短的回答这位同学的问题:

因为我的考虑是这样的,我用‘1’代表有雷,‘0’代表没有雷,如果放在一个盘中,出现多个1的时候,无法确定这是雷还是代表周围8个格子中有一个雷。

进一步解释

  图片参考:                      

当雷是1,还有记录数字也为1时,以下黄色标记位置为例:         

                                         

此时点击黄色位置,那么它显示的数字是2而不是1,我们会发现雷‘’变多了‘’。

但又有同学要问了,为啥非要用1代表雷,0代表没有雷呢,我用¥代表雷,@代表非雷就不会出现这种情况了。

其实这样安排是没问题的,我也鼓励大家去尝试一下,但分双棋盘带来的好处,经过后面的代码分析会体现出来。(之后可以出一个棋盘的版本)

需要注意的是我们采用‘1’和‘0’,即字符1和字符0代表有雷和没有雷。为什么要这样安排呢?这利于我们后续设计数组和函数,现在暂时解释到这,后面会让大家有一个更清楚,更系统的认知。

初步设计思路到此结束!现在开始发车了,请关好门窗,系好安全带!!


首先我们分文件设计游戏,分一个test.c来管理游戏的执行流程,一个game.c来实现游戏需要的自定义函数,一个game.h来封装函数声明,常数的定义,头文件的包含等。

test.c的初步游戏执行流程设计,参考以下代码:

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

int main()
{
	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;
}

因为游戏可能需要玩很多次,为了不重新打开游戏,此处采用循环的形式,又因为游戏至少需要执行 一次,此处采用do while循环,用while循环其实问题也不大。好了,do while循环首先执行的是打印菜单,然后我们根据菜单来选择接下来要执行的分支,当我们选1的时候代表我们接下来要玩游戏,而选择0的时候我们要退出游戏,在这里我们选择用整形变量input来接受我们的选择,用switch来执行我们的选择。同时我们会发现安排退出数字为0时有一个好处,当input为0的时候直接可以退出循环,而非0可以继续循环,这完美符合我们的选择设计!!!


接下来就是函数game()的实现了。

我们刚刚讨论了,我们需要两个棋盘,一个雷盘,另一个为玩家猜测盘。

因此我们创建两个二维数组来代表这两个盘。

即(雷盘) char mine [ ] [ ] = { 0 }; (玩家界面盘)char show [ ] [ ] = { 0 };但这两个盘的大小分别为多大呢?这是我们要考虑的问题,根据初级扫雷难度,我们暂且安排为9X9的棋盘。为了更长远的考虑,我们不可能直接这样写char mine [ 9 ] [ 9 ] = { 0 };  char show [ 9 ] [ 9 ] = { 0 };为了以后的维护和花小代价的控制难度,我们用#define分别定义ROW为9,COL为9.但是我们真的想的够全面了吗?如果要统计处于边界位置周围的8个雷怎么算呢?这样无疑会造成数组越界。因此我们需要将棋盘扩展成11X11的棋盘。因此我们这样创建两个棋盘char mine [ ROWS ] [ COLS ] = { 0 };  char mine [ ROWS ] [ COLS ] = { 0 };

这里需要简单说一下:

#define ROWS ROW+2

#define COLS COL+2.

好了,我们终于有了可操作的对象了,现在我们需要给它们‘’洗个澡‘’,让它更优雅。

先对其进行初始化,按照传统取个函数名吧,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;
		}
	}
}

在这里,我们用0初始化雷盘,代表所有位置都没有雷,用 * 来保持对该位置的神秘感,用来玩家猜测。即  initboard(mine, ROWS, COLS, '0');
                initboard(show, ROWS, COLS, '*');

虽然没暂时没完成打印函数,但并不妨碍我们欣赏初始化结果(参考下图)

    

其实这里就可以体现为什么用字符‘1’和字符‘0’代表有雷和没有雷了,而不是数字1和0了,因为我们只要一个函数就可以两个盘都进行初始化。而如果一个棋盘是整数数组,另一个是字符数组,你就需要两个初始化函数了。


 初始化棋盘完了之后,我们需要完成剩下的打印函数,因为我们要检查我们的初始化函数是否按照我们的想法实现了对棋盘的初始化。代码如下:

void displayboard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i <= col; i++)
	{
		if (i == 0)
		{
			printf("  %d     ", i);
		}
		else
		{
			printf("%d     ", i);
		}
	}
	printf("n------------------------------------------------------------n");
	for (i = 1; i <= row; i++)
	{
		printf("  %d  |", i);
		for (j = 1; j <= col; j++)
		{
			printf("  %c  |", board[i][j]);
		}
		printf("n------------------------------------------------------------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--;
		}
	}
}

此处EASY_COUNT是#define EASY_COUNT 9定义的常函数,意思是雷的简单数量。此处用rand()库函数对坐标X和Y分别设计随机数,%row+1和%col+1确保X和Y在 1~row 范围内,需要注意的是在给mine[ x ] [ y ]赋值的时候,要确保mine[ x ] [ y ]的位置没有埋雷,如果缺少上图if条件判断,这设置的雷可能变少,因为重复赋值,会覆盖上次的值。


哈哈哈,终于来激动人心的排雷环节了,此处应有掌声!!!先看图

首先我们需要输入排查雷的坐标,因此我们创建两个整形变量X和Y,在我们输入X和Y的值时候,可能输入的X和Y并不符合要求,此时该 if 语句登场了。当我们输入的值符合要求的适合,我们先要判断是否mine [ X ] [ Y ] == '1',如果条件成立,则可以宣布游戏结束了,如果mine [ X ] [ Y ] == '1',不成立,则我们需要对坐标(X,Y)位置赋值,要统计它周围8个位置有多少个雷。统计坐标(X,Y)位置周围8个位置雷的个数我们用 int get_mine_count();代码如下:

void foundmine(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);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了n");
				displayboard(mine, row, col);
				break;
			}
			else
			{
				iswin++;
				expand(mine, show, row, col, x, y);
				displayboard(show, row, col);
			}
		}
		else
		{
			printf("输入坐标不合法,请重新输入n");
		}
	}
	if (iswin == row * col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功n");
	}
}

排雷过程是整个游戏的核心,需要玩家排查出所有的雷的位置或者踩到雷,然后游戏才能结束,所以排雷函数一定是一个循环过程,这样才能取到多次排雷的效果。如何判断胜利?我们采取这样的一种设计思想:排查出所有非雷的位置,其他位置自然是雷,这样达到一个扫雷的效果。此处我采用整形变量 iswin 来当计数器,当输入一个非雷的坐标位置的时候,iswin+=1,当 iswin等于ROW*COL-EASY_COUNT的时候,即可判断游戏胜利。


重点来了,expand()函数的详细解析

这里重申以下get_mine_count()函数的功能:计算输入坐标(X,Y)位置周围8个位置有多少个雷。

expand()是一个扩展函数,功能是:

当排查的坐标位置get_mine_count()!=  0时候,将该位置的值改为get_mine_count()的返回值。

当排查的坐标位置周围为0个雷的时候,把该位置置为空,并检查周围8个位置是否它的周围也是0个雷,如果周围坐标位置有满足条件get_mine_count == 0 ,这将这个位置也置为空,如果周围周围的位置也满足条件get_mine_count == 0,这也将该位置置为0,如果周围的周围的周围也满足条件et_mine_count == 0.........。

说人话就是:以你输入的位置为起点,只要该位置get_mine_count == 0,就把它置空,同时把周围满足get_mine_count == 0也置空,同时也把周围这个位置也看做起点。显然满足递归思想,用递归能够很舒服的解决。

上代码!!!

void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	if (get_mine_count(mine, x, y)==0)
	{
		show[x][y] = ' ';
		int i = 0;
		int j = 0;
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if (show[i][j] == '*' && i > 0 && i <= row && j > 0 && j <= col)
				{
					expand(mine, show, row, col , i, j);
				}
			}
		}
	}
	else
	{
		show[x][y] = get_mine_count(mine, x, y) + '0';
	}
}

唉,果然放不太下,只能麻烦看客滑动看了。

为了表示出输入位置周围8个位置,我们用 i,j 变量以for循环的方式体现,即以下8个位置

        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]

需要注意的是:当边界 get_mine_count == 0 时不能将它周围位置置空,因为如果把它周围位置置空,计算get_mine_count会出问题,具体可看get_mine_count的实现原理。因此以上递归实现需要一个解决这个问题的限制条件,具体可看上述代码。

以下补充get_mine_count的实现原理:

static 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 * 48;
}

重申get_mine_count()函数的功能:计算输入坐标(X,Y)位置周围8个位置有多少个雷。

因为有雷我用的是字符 ‘1’设置的,因此将这8个位置的值相加起来,就能知道雷的个数了。但不碰巧的是这不是数字1,而是字符 ‘1’,因为它的ASCII的值为49,所有它实际代表的是数字49,但这并妨碍我们相加,因为字符 ‘1’==字符 ‘0’+1的,所以我们可以把它们相加后减去字符‘0’即可,因为字符 ‘0’实际代表的是数字48.(这里多说一下,当字符 ‘0’ 以字符形式打印时,它打印的结果是字符 ‘0’ ,显示的是0,和数字0一样。当它以整形打印时,它打印的结果是48.),这里也体现了用字符‘1’和字符‘0’代表有雷和没有雷的好处。

分享整个源码

test.c:

#include "game.h"
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);
	displayboard(mine, ROW, COL);
	foundmine(mine, show, ROW, COL);
}

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	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:

#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 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++)
	{
		if (i == 0)
		{
			printf("  %d     ", i);
		}
		else
		{
			printf("%d     ", i);
		}
	}
	printf("n------------------------------------------------------------n");
	for (i = 1; i <= row; i++)
	{
		printf("  %d  |", i);
		for (j = 1; j <= col; j++)
		{
			printf("  %c  |", board[i][j]);
		}
		printf("n------------------------------------------------------------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--;
		}
	}
}

static 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 * 48;
}

void foundmine(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);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了n");
				displayboard(mine, row, col);
				break;
			}
			else
			{
				iswin++;
				expand(mine, show, row, col, x, y);
				displayboard(show, row, col);
			}
		}
		else
		{
			printf("输入坐标不合法,请重新输入n");
		}
	}
	if (iswin == row * col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功n");
	}
}

void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	if (get_mine_count(mine, x, y)==0)
	{
		show[x][y] = ' ';
		int i = 0;
		int j = 0;
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if (show[i][j] == '*' && i > 0 && i <= row && j > 0 && j <= col)
				{
					expand(mine, show, row, col , i, j);
				}
			}
		}
	}
	else
	{
		show[x][y] = get_mine_count(mine, x, y) + '0';
	}
}

game.h:

#define _CRT_SECURE_NO_WARNINGS 1
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
void menu();
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 mine[ROWS][COLS], int row, int col);
void foundmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
void expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);


好了,以上就是扫雷的简单实现了,看到这里还不点个赞吗,哈哈。以后会出更多的系列,比如C语言所有关键字的解析与应用,或者更多的游戏实现。这是我的第一篇真正意义上的博客,希望大家多多关照。

上述如有错误,还请各位看官不吝赐教,在下感激不尽。

望与大家共同进步!天道酬勤!!人生值得,未来可期!!!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>