一、扫雷介绍
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
本篇博客所实现的是9*9*10的简易扫雷游戏,采用多文件方式:
game.h → 包含头文件、宏定义、函数声明
game.c → 游戏功能的函数实现
test.c → 测试的游戏逻辑
二、如何实现
2.1 游戏应有效果
如果有接触过扫雷游戏,应该不难理解扫雷的玩法,一般我们玩到的扫雷都具有标记雷和点开非雷格子的功能(本文暂不涉及标记操作),其中最最基础的扫雷游戏应当具有的功能是,点击格子时,若该格子不是雷,则显示该格子周围的雷的个数,若是雷,则游戏结束,并在结束时显示棋盘中所有雷的位置;若玩家全程未踩雷,则在非雷格子全被翻开之后游戏结束。
若是进阶一些的话,我们可以让玩家点击的第一个格子非雷且该格子周围也没有雷,确保不会开局即结束,且在点击一个格子时,若该格子周围的雷个数为0,则以点击的格子为中心,周围3*3的格子全部打开。
再此基础上,我们还可以试着实现当点击的格子周围的格子也显示0时,它的周围格子也一并打开这样的效果,从而大幅缩短游戏进程。
2.2 游戏实现逻辑
要进行扫雷,首先就要在游戏开始之前布置好雷,雷的位置应当是随机的,为了显示方便,我们可以在开始的时候创建两个数组,一个用于游玩时显示给玩家,另一个用于放置雷,由于点击一个格子时需要排查的是以该格子为中心的3*3区域,当格子位于棋盘边缘时会出现越界情况。
所以,我们可以在创建数组时比实际显示的棋盘大一圈,我们要创建的棋盘是9*9大小,数组创建时就创建11*11大小的二维数组,从而避免数组越界的情况。
创建好棋盘后,我们就可以一步一步通过代码来实现扫雷游戏了。
三、代码实现
3.1 test.c 游戏逻辑 & game.h
要开始游戏首先需要打印一个菜单,为了调用方便可以写一个menu函数:
void menu()
{
printf("**************************\n");
printf("******** 1. start ********\n");
printf("******** 0. exit ********\n");
printf("**************************\n");
}
打印菜单后由玩家自行选择是否进行游戏,可以通过switch语句来实现选择,且存在玩家想要连续多次游玩的可能,我们可以用do while循环来实现这一功能,代码如下:
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("\n-----------END-----------\n");
break;
default:
printf("\nerror!请重新选择\n\n");
break;
}
} while (input);
其中game函数的运行逻辑很简单,创建棋盘、初始化、打印棋盘、布雷、排查 ,实现的具体函数稍后在game.c中实现,此处只做简单的调用
void game()
{
//创建棋盘,都创建为字符数组方便统一
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印
DisplayBoard(show, ROW, COL);
//布雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//↑此条语句可用于确认布雷情况,实际运行时不需要
//排查
FindMine(mine, show, ROW, COL);
}
接触C语言的朋友们应该知道,库函数的调用需要声明,由于我们采用的是多文件形式,为了方便我们可以将声明及宏定义统一放到game.h头文件中,在源文件中直接声明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 mine[ROWS][COLS], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
3.2 game.c 游戏功能函数
从test.c中的game函数,不难知道我们首先要实现的就是棋盘初始化函数,话不多说,直接上代码!
//初始化
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0, 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, j = 0;
printf("\n-------Minesweeper-------\n\n");
for (i = 0; i <= row; 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");
}
printf("\n");
}
需要注意的是,打印棋盘只需要打印9*9的棋盘部分,然后为了确定坐标可以打印出来行数及列数。
布雷需要注意的是随机性,我们可以通过srand及rand函数来实现,因为srand函数只需要写一次就行,所以我们可以把它写在test.c的main函数中
srand((unsigned int)time(NULL));
布雷函数如下:
//布雷
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--;
}
}
}
接下来要面对的就是扫雷实现中最大的坎——排查了,我们可以先从基础的排查逻辑开始,即“点击格子时,若该格子不是雷,则显示该格子周围的雷的个数,若是雷,则游戏结束,并在结束时显示棋盘中所有雷的位置;若玩家全程未踩雷,则在非雷格子全被翻开之后游戏结束”这一部分的实现。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = ROW * COL;
do
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
if (win == ROW * COL)
{
FirstSave(mine, x, y);
//确保第一个格子不是雷
}
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
printf("该坐标已排查过,请重新输入\n");
else if (mine[x][y] == '1')
{
printf("很遗憾,你踩到雷了\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
GetMine(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标不在范围内,请重新输入\n");
}
win = MineCount(show);
} while (win > EASY_COUNT);
if (win == EASY_COUNT)
{
printf("恭喜!你通关了!\n");
DisplayBoard(mine, ROW, COL);
}
}
然后我们具体来看其中的功能要如何实现,首先是确保玩家排查的第一个格子非雷且周围也无雷,落实到代码上,就是若排查到的格子是雷,则把它变成非雷并在棋盘内其它随机位置创建新的雷,对该坐标周围3*3的格子也一样
void FirstSave(char mine[ROWS][COLS], int x, int y)
{
if (mine[x][y] == '1')
{
mine[x][y] = '0';
int m = rand() % ROW + 1;
int n = rand() % COL + 1;
if (m != x && n != y && mine[m][n] == '0')
mine[m][n] = '1';
}
int i = 0, j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '1')
{
mine[i][j] = '0';
int m = rand() % ROW + 1;
int n = rand() % COL + 1;
if ((m < x - 1 || m>x + 1) && (n<y - 1 || n>y + 1))
if (mine[m][n] == '0')
mine[m][n] = '1';
}
}
}
}
若排查的格子不是雷,则需要显示该格子周围有几颗雷,也就是说我们需要一个数雷的个数的函数,由于只在game.c中使用,我们可以加上static作为限制
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1]
+ mine[x][y - 1]
+ mine[x + 1][y - 1]
+ mine[x - 1][y]
+ mine[x + 1][y]
+ mine[x - 1][y + 1]
+ mine[x][y + 1]
+ mine[x + 1][y + 1] - 8 * '0');
}
然后,我们想要实现当排查的坐标显示为0时打开周围的格子,且周围格子中也为0时该格子的周围格子也打开的功能,实现这一步时需要注意的是,由于我们创建的数组大于实际棋盘,棋盘以外用于防止数组越界的部分都没有雷,可能导致看起来不相连的部分同时被打开的情况,所以需要适当加上限制
void GetMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
if (show[x][y] == '0' && x > 0 && x <= ROW && y > 0 && y <= COL)
{
//(show)中心坐标为0时进入该函数,排查输入坐标周围的八个坐标
int i, j;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
while (show[i][j] == '*')
{
int count = GetMineCount(mine, i, j);
show[i][j] = count + '0';
if (show[i][j] == '0')
GetMine(mine, show, i, j);
}
}
}
}
}
打开格子的工作做完了,但是游戏还需要判断结束的条件,由于雷的数量是固定的,所以我们可以创建一个函数,用来数还未打开的格子数量,当前创建的9*9*10的棋盘,也就是说当还未打开的格子数量为10时,玩家顺利通关
static int MineCount(char show[ROWS][COLS])
{
int i = 0, j = 0, win = 0;
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (show[i][j] == '*')
win++;
}
}
return win;
}
3.3 完整代码
头文件 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 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 1
#include "game.h"
void menu()
{
printf("**************************\n");
printf("******** 1. start ********\n");
printf("******** 0. exit ********\n");
printf("**************************\n");
}
void game()
{
//创建棋盘
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印
DisplayBoard(show, ROW, COL);
//布雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//↑此条语句可用于确认布雷情况,实际运行时不需要
//排查
FindMine(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-----------END-----------\n");
break;
default:
printf("\nerror!请重新选择\n\n");
break;
}
} while (input);
return 0;
}
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, 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, j = 0;
printf("\n-------Minesweeper-------\n\n");
for (i = 0; i <= row; 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");
}
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--;
}
}
}
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1]
+ mine[x][y - 1]
+ mine[x + 1][y - 1]
+ mine[x - 1][y]
+ mine[x + 1][y]
+ mine[x - 1][y + 1]
+ mine[x][y + 1]
+ mine[x + 1][y + 1] - 8 * '0');
}
void FirstSave(char mine[ROWS][COLS], int x, int y)
{
if (mine[x][y] == '1')
{
mine[x][y] = '0';
int m = rand() % ROW + 1;
int n = rand() % COL + 1;
if (m != x && n != y && mine[m][n] == '0')
mine[m][n] = '1';
}
int i = 0, j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '1')
{
mine[i][j] = '0';
int m = rand() % ROW + 1;
int n = rand() % COL + 1;
if ((m < x - 1 || m>x + 1) && (n<y - 1 || n>y + 1))
if (mine[m][n] == '0')
mine[m][n] = '1';
}
}
}
}
void GetMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
if (show[x][y] == '0' && x > 0 && x <= ROW && y > 0 && y <= COL)
{
//(show)中心坐标为0时进入该函数,排查输入坐标周围的八个坐标
int i, j;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
while (show[i][j] == '*')
{
int count = GetMineCount(mine, i, j);
show[i][j] = count + '0';
if (show[i][j] == '0')
GetMine(mine, show, i, j);
}
}
}
}
}
static int MineCount(char show[ROWS][COLS])
{
int i = 0, j = 0, win = 0;
for (i = 1; i <= ROW; i++)
{
for (j = 1; j <= COL; j++)
{
if (show[i][j] == '*')
win++;
}
}
return win;
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = ROW * COL;
do
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
if (win == ROW * COL)
{
FirstSave(mine, x, y);
}
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
printf("该坐标已排查过,请重新输入\n");
else if (mine[x][y] == '1')
{
printf("很遗憾,你踩到雷了\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
GetMine(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标不在范围内,请重新输入\n");
}
win = MineCount(show);
} while (win > EASY_COUNT);
if (win == EASY_COUNT)
{
printf("恭喜!你通关了!\n");
DisplayBoard(mine, ROW, COL);
}
}
至此,我们已经完成了这个简单的扫雷游戏可以实际的游玩它了,虽然还有待进步,但是希望对看到这里的你有所帮助!