C语言扫雷小游戏

学习了数组和函数之后,我们来讲一个扫雷小游戏。

本次作者第一次使用画图软件,奇丑无比,还请见谅。

目录

1.扫雷游戏的设计分析

1.1扫雷的功能说明

2.扫雷的具体实现

2.1 扫雷的数据结构

2.2 扫雷的文件结构设计

2.3 数组的初始化函数

2.4 打印棋盘函数

2.5 布置雷

2.6 扫雷


1.扫雷游戏的设计分析
1.1扫雷的功能说明

大家在小时候肯定都玩过扫雷游戏,所以它的玩法这里不过多介绍。

首先,大家在玩游戏的时候一开始肯定是一个菜单界面,在菜单里可以选择难度,开始游戏或退出游戏,所以我们在写扫雷时候也可以从菜单开始入手。

其次,我们要做出扫雷的这样一个机制,尽可能去还原:

1.如果位置不是雷,就显⽰周围有⼏个雷
2.如果位置是雷,就炸死游戏结束
3.把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束
我们先写出菜单的代码:
//测试文件 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"game.h"

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

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

我们通过对input赋值来达到对菜单各功能的执行,input值为1时,开始游戏,进入game函数。input值为0时,退出游戏,而input被赋于其他值时,会执行default语句。当一次循环结束后,会根据input的值来判断是否再次进入循环,input不为0时,判断条件为真,进入循环,而input为0时,判断条件为假,离开循环,这也就达到了input输入0时退出游戏的目的。

2.扫雷的具体实现
2.1 扫雷的数据结构

因为在扫雷游戏中我们需要进行扫雷,标记一系列操作,所以需要数据结构对这些数据进行存储,因为我们需要在9*9的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个9*9的数组来存放信息。

对于 这样一个9*9的数组,我们可以让为雷的地方为1,不为雷的地方为0。

比如说我们在(2,5)这一点周围雷的个数为2,所以我们排查这一点后应将这一点赋值为2,

因为我们在扫雷的时候需要看这一点周围的雷的个数,所以边界点会产生越界问题,这一问题可以通过改变边界点扫雷的方式来解决,也可以将数组扩大一周,也就是创建一个11*11的数组,但最外层全赋值为0,这样不会对内层的扫雷产生影响。

且如果我们扫雷时直接将数据存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难。我们不妨重新创建一个数组专门用来打印每次扫雷后的结果。

对于放置雷的数组,在放置雷之前,我们要初始化;对于打印每次扫雷结果的数组,我们也要进行初始化,所以我们可以写一个初始化的代码来对这两个数组进行初始化。

2.2 扫雷的文件结构设计

在扫雷代码中,我们设置三个文件,分别是:

1. game.c作为扫雷游戏的源文件,用来写扫雷函数的实现

2. game.h作为扫雷游戏的头文件,来写扫雷需要的函数声明和数据类型

3. teat.c作为测试文件,用来测试扫雷

2.3 数组的初始化函数

这两个数组需要在测试文件test.c里创建,初始化函数的实现应写在game.c中,该函数声明应写在game.h中。当然对于一个扫雷游戏而言,它的棋盘不一定一直都是9*9的,如果我们在创建数组的时候,直接使用9来创建,那么在棋盘大小改变时是很难修改的,所以我们可以使用#define来定义常量,这样在修改时只需要修改常量值即可达到目的。

因为有一个数组是专门用来存放每次扫雷后的信息的,但为了一开始的神秘感,我们将其初始化为*,这也是棋盘数组类型为char的原因之一。

game.h

#pragma once
#include<stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2

game.c

#include"game.h"
initboard(char board[ROWS][COLS], int rows, int cols,char set)
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			board[i][j] = set;
		}
	}
}

test.c

#include"game.h"
game()
{
    char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };
    initboard(mine,ROWS,COLS,'0');           //对于初始化函数,起名为initboard
    initboard(show,ROWS,COLS,'*');
}

在test.c文件和game.c文件中,需要引用game.h文件才能使用其声明的函数。

在game.h中,对棋盘大小和应该创建的数组大小进行了常量定义,在创建数组时使用的行和列与在扫雷操作时使用的行和列不同。

在game.c中,函数的四个参数分别为:要初始化的数组,数组的行数,列数,初始化的值。

2.4 打印棋盘函数

我们需要打印棋盘函数来对我们每次扫雷后的数组进行打印,这样可以对现有的扫雷情况进行观察。具体代码如下:

//game.h

void displayboard(char board[ROWS][COLS], int row, int col);

//game.c

void displayboard(char board[ROWS][COLS], int row, int col)
{
	int i=0;
	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");
	}
	printf("\n");
}

//test.c

game()
{
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };
	initboard(mine,ROWS,COLS,'0');
	initboard(show,ROWS,COLS,'*');
	displayboard(show, ROW, COL);
    displayboard(mine, ROW, COL);
}

在game.c中,先打印列数,后将行数每行的数据一同打印出来

在test.c中,分别打印两个数组,也可以检验初始化是否正确

2.5 布置雷

打印棋盘也搞定了,接下来就是扫雷的一大难题,布置雷。

我们在布置雷时,每局游戏布置雷的位置肯定不能相同,所以这个雷不应该由人手动来布置。下面提到一个rand函数,它可以提供出随机的数字。

#include<stdio.h>
#include<stdlib.h>
int main()
{
    int i=0,j=0;
    for(i=0;i<5;i++)
    {
        j=rand();
        printf("%d",j);
    }
}

其中,rand()函数需要引用头文件<stdlib.h>,rand()的功能就是输出一个大小在0-32767的数字,但是我们执行时可以发现,我们每次执行时rand()输出的数字都是相同的,所以rand函数只是在程序单词执行时达到了随机的目的,但根本上还是没能解决我们的问题。

搜索后发现,rand()函数能输出随机值是因为它需要一个种子,由于在人为不改变的前提下,每次执行程序的时候这个种子是不会改变的,所以每次执行程序时才会输出相同的值。

现在,我们来介绍另一个函数,也就是rand()函数的兄弟srand(),当srand()的参数改变时,也就是改变了rand()的种子,rand()每次输出的值就会不同。可是我们为了输出随机值,在调整rand()的种子时也需要一个随机值,那么又该怎么办呢?

我们最后介绍一个在布置雷函数时使用到的函数time()函数。这个函数返回值是当前时间到1970 年 1 月 1 日 00:00 UTC 以来的秒数(即当前 unix 时间戳)。我们可以发现,时间是一直在改变的那么时间戳也就是一直在改变的,我们使用时间戳作为rand()的种子,不就可以解决输出随机值的问题了吗?

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{
	srand((unsigned int)time(NULL));
	int i = 0, j = 0;
	for (i = 0; i < 5; i++)
	{
		j = rand();
		printf("%d", j);
	}
	return 0;
}

这样每次代码执行的时候就会输出不同的值了。

那么现在进入正题:布置雷

在布置雷的数组中,我们希望将布置雷的数据由0改为1,使用rand()来输出放置雷的坐标,对于雷的坐标(x,y),对应的应该是数组的第x+1行和第y+1列,而对于9*9的棋盘来说,坐标最大为(9,9),在数组中对应第10行第10列。我们知道了rand()输出数的范围是0-32767,我们让这个随机数对0取余,会得到一个在0-9的数字,而在此基础上再+1,那么这个随机数的范围就对应0-10,也就是数组上棋盘的位置。

确定了雷的位置,还有一个问题就是:不能多次将同一个位置布置雷,这样可能会使雷的数量减少。所以我们在随机出一个坐标时,应先判断这个坐标的位置上是否已经布置了雷。

这一部分主要说明了在布置雷的时候的逻辑和注意事项,下面是布置雷的代码:

//game.h

#define Easy 10
void setmine(char board[ROWS][COLS], int row, int col);

//game.c

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

//test.c

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(mine, ROW, COL);
}
int main
{
    srand((unsigned int)time(NULL));
}

在game.h中,将easy定义为常量,代表简单模式下的雷的数量。

因为有打印棋盘的函数,我们可以将布置好雷的数组打印出来进行检验。

2.6 扫雷

在讲完布置雷后,讲解扫雷的最后一部分,也是最难的一部分,那就是排查雷。

我们知道,排除雷是将排除坐标周围的雷的个数打印到这个坐标上,所以我们要计算排除坐标点周围的雷的个数。

在我们排查的时候,首先要判断的就是我们排查的这个坐标是不是布置雷的坐标,如果是雷的话,则直接结束游戏,如果不是雷的话则进行下一步判断。

如果排查的坐标没有雷,那么只需要将该坐标点周围的雷的个数打印出来,于是我们可以专门写一个函数来计算某一坐标周围的雷的个数。因为数组是字符数组,所以数组中的‘0’和‘1’存放的都是其ASCII码值,0就是48,1就是49,那么我们在计算的时候就需要将该坐标周围8个坐标的值相加再减去8个字符‘0’的值,就是该坐标点周围的雷的个数。

//game.h

int getminecount(char mine[ROWS][COLS], int x, int y);

//game.c

int getminecount(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int j = 0;
	int sum = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		for (j = y - 1; j <= y + 1; j++)
		{
			sum += mine[i][j]-'0';
		}
	}
	return sum;
}

又因为我们需要将雷的个数打印出来,所以该函数将此数字作为返回值返回。

接下来来看排查雷的函数:
 

//game.h

void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

//game.c

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)
	{

		printf("请输入要排查的坐标\n");
		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
			{
				int minecount = getminecount(mine, x, y);
				show[x][y] = minecount + '0';
				displayboard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入\n");
		}
	}
}

//test.c

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);
	findmine(mine, show, ROW, COL);
}

在该函数中,创建了一个新的变量win,当成功排查一个坐标时,win的值就会+1,棋盘中未排查的坐标一共有row*col-easy个,win值小于这个值时,循环继续,win值等于这个值时,跳出循环,此时雷全部排查完,此次游戏也就结束了,退出game()函数,继续打印菜单进入下一次游戏。

到这里,整个扫雷的代码就全部介绍完了,整套代码并不难理解,但是对于雷的标记,以及当前所用的时间等等,都没有进行实现,也是一种遗憾,对现在的我来说。

下面是整套代码:

//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 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);
int getminecount(char mine[ROWS][COLS], int x, int y);

//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;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			board[i][j] = set;
		}
	}
}
void displayboard(char board[ROWS][COLS], int row, int col)
{
	int i=0;
	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");
	}
	printf("\n");
}
void setmine(char board[ROWS][COLS], int row, int col)
{
	int count = Easy;
	while (count)
	{
		int x = rand() % 9 + 1;
		int y = rand() % 9 + 1;
		if (board[x][y] != '1')
		{
			board[x][y] = '1';
			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)
	{

		printf("请输入要排查的坐标\n");
		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
			{
				int minecount = getminecount(mine, x, y);
				show[x][y] = minecount + '0';
				displayboard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入\n");
		}
	}
}
int getminecount(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int j = 0;
	int sum = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		for (j = y - 1; j <= y + 1; j++)
		{
			sum += mine[i][j]-'0';
		}
	}
	return sum;
}

//test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"game.h"
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);
	findmine(mine, show, ROW, COL);
}
void menu()
{
	printf("**************************\n");
	printf("******    1.play    ******\n");
	printf("******    0.exit    ******\n");
	printf("**************************\n");
}
int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误\n");
			break;
		}
	} while (input);
	return 0;
}

那么这次扫雷的代码就介绍到这里了,我们下次再见。

  • 25
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值