文章目录
简介
扫雷可以说是一款我们从小玩到大的游戏,他的游戏规则也很简单。
在一块16×30的网格中,点开所有没有雷的网格(雷数默认为99颗)。(其中简单难度为9×9的方格,共10颗雷;中级难度为16×16的方格,共40颗雷) 所有操作依靠鼠标即可完成。左键点开网格,若该网格为雷则游戏结束;若该网格周边八个网格中有雷,则显示雷数,若无雷则直接开启周边网格。可通过鼠标右键标记网格。点击一次右键插旗,表明你确定该网格有雷,在改变插旗状态前该网格无法被点开;在旗上右键将插旗状态改变为问号标记(仅具有标记作用,与普通网格无异);在问号上右键回复初始状态。左右键同时点击,为快速点开周围网格。若插旗数不等于雷数则无法点开;若插旗数等于雷数则直接点开所有未插旗网格(旗插错了游戏结束)。
由于c语言本身的局限性,而且只能够用键盘输入数据以确定要进行操作的坐标,因此在本篇文章中,我们实现了点开数字为零的板块时自动展开的功能;标记雷的功能;以及当非雷板块周围已经确定的雷的数量与自身数字相等时自动标雷等功能,以下为实现代码。
主函数部分
菜单函数
主函数前包括两个菜单函数,第一个为游戏开始时的菜单,决定进行游戏or退出游戏,第二个为决定是否进行标记的菜单,因为实现了标记功能,在进行之前先进行询问。
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu1(void)
{
printf("\n**************扫雷*******************\n");
printf("******** 1 . play ********\n");
printf("******** 0 . exit ********\n");
printf("*************************************\n");
}
void menu2(void)
{
printf("******** 1 . yes *********\n");
printf("******** 0 . no **********\n");
}
随机数函数
主函数中我们先调用srand函数进行随机设雷,为了使游戏能够重复体验,采用do-while语句进行设计,在一局游戏结束后可选择继续或退出游戏。
int main(void)
{
srand((unsigned)time(NULL));
int input = 0;
do {
menu1();
scanf("%d",&input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入有误,请重新输入\n");
break;
}
} while (input);
return 0;
}
游戏函数
然后便是game函数的设计。首先要创建两个二维数组,其中一个用来设置地雷,另一个则用来展现给玩家。这里我们考虑到在计算非雷板块的数字时需要遍历其周围的八个区域,而在棋盘边上的时候这样计算会出现越界访问的问题,因此我们将实际的数组比游戏中的棋盘的长宽多出来两行,也就是ROWS = ROW + 2,COLS = COL + 2,在后续的头文件中也会体现出来。
void game(void)
{
char mineboard[ROWS][COLS] = { 0 };
char playerboard[ROWS][COLS] = { 0 };
initialization(mineboard, ROWS, COLS, '0');
initialization(playerboard, ROWS, COLS, '*');
setmine(mineboard, ROWS, COLS);
displayboard(playerboard, ROWS, COLS);
while (1)
{
if ((move(mineboard, playerboard, ROWS, COLS)) == 0)
{
break;
}
system("cls");
mark_mine(playerboard, ROWS, COLS);
displayboard(playerboard, ROWS, COLS);
if ((is_win(mineboard, playerboard, ROWS, COLS)) == 0)
{
break;
}
system("cls");
if_mark(playerboard, ROWS, COLS);
displayboard(playerboard, ROWS, COLS);
}
}
初始化函数
随后是两个初始化函数,可以看到初始化函数在传参时,除了数组和行列外,还传了一个字符,这是为了能让两个数组共用一个函数的办法,传参时通过最后一个参数来区别两个数组,省掉了分别初始化的麻烦。
initialization(mineboard, ROWS, COLS, '0');
initialization(playerboard, ROWS, COLS, '*');
展示棋盘函数
然后对于地雷棋盘进行初始化并进行棋盘展示,然后便进入了操作循环(while(1))。分别为操作(也就是鼠标的点击)函数->清屏->自动标记雷区函数->展示棋盘函数->判断是否游戏结束函数->清屏->判断是否进行手动标记函数->展示棋盘函数。
displayboard(playerboard, ROWS, COLS);
while (1)
{
if ((move(mineboard, playerboard, ROWS, COLS)) == 0)
{
break;
}
system("cls");
mark_mine(playerboard, ROWS, COLS);
displayboard(playerboard, ROWS, COLS);
if ((is_win(mineboard, playerboard, ROWS, COLS)) == 0)
{
break;
}
system("cls");
if_mark(playerboard, ROWS, COLS);
displayboard(playerboard, ROWS, COLS);
}
}
到此为止,该游戏的基本逻辑就已经交代清楚,下面介绍头文件以及具体功能的实现。
头文件
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>
#define ROW 10
#define COL 10
#define ROWS ROW+2
#define COLS COL+2
#define NUMBER_OF_MINES 6
void initialization(char board[ROWS][COLS],int row,int col,char sign);
void displayboard(char board[ROWS][COLS],int row,int col);
void setmine(char board[ROWS][COLS], int row, int col);
int move(char mineboard[ROWS][COLS], char playerboard[ROWS][COLS], int row, int col);
int is_win(char mineboard[ROWS][COLS], char playerboard[ROWS][COLS], int row, int col);
void unfold_zero_area(char mineboard[ROWS][COLS], char playerboard[ROWS][COLS], int row, int col);
char get_mine_count(char board[ROWS][COLS], int row, int col);
void mark_mine(char board[ROWS][COLS], int row, int col);
void if_mark(char board[ROWS][COLS], int row, int col);
可以看出,在头文件中我们主要设置游戏难度,即棋盘的大小以及雷数,以及所有的函数声明,这里面包括在文章前面未出现的函数,如unfold_zero_area函数,也就是实现点0自动展开功能的函数;get_mine_count函数,计算非雷区域数字的函数。
下面介绍各函数的实现细节。
游戏函数及其实现
初始化函数实现
名曰初始化,其实就是遍历数组并赋初值。前面有讲,最后一个字符类型参数决定了用什么符号进行初始化,其中地雷棋盘用‘0’来初始化,玩家棋盘用‘*’来初始化。
void initialization(char board[ROWS][COLS], int row, int col, char sign)
{
int i = 0;
for (i = 0; i < ROWS; i++)
{
int j = 0;
for (j = 0; j < COLS; j++)
{
if (sign == '*')
board[i][j] = '*';
else
board[i][j] = '0';
}
}
}
展示棋盘函数实现
遍历并打印。为了方便玩家使其快速找到坐标,我们在第一行上方&第一列左边加入了坐标显示。如果棋盘变大的话,可以在打印时在%c中间加入数字来使坐标数与棋盘对齐。
void displayboard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (int m = 0; m <= ROW; m++)
{
printf(" %d",m);
}
printf("\n");
for (i = 1; i < ROWS - 1; i++)
{
int j = 0;
printf("%2d ", i);
for (j = 1; j < COLS - 1; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
计算非雷板块数字函数实现
在地雷棋盘上的某个‘0’区算出其周围‘1’的个数,由于字符数组中存储的是字符数字,比如字符‘0’的ASCII码为48,因此在返回时我们要减去8个字符‘0’以保证返回值是数字。
char get_mine_count(char board[ROWS][COLS], int row, int col)
{
return ( board[row - 1][col]
+ board[row - 1][col - 1]
+ board[row][col - 1]
+ board[row + 1][col - 1]
+ board[row + 1][col]
+ board[row + 1][col + 1]
+ board[row][col + 1]
+ board[row - 1][col + 1] - 8 * '0');
}
设雷函数实现
先对预设雷数进行拷贝,借助随机数函数在未设有雷的地方进行埋雷,然后预设雷数减一,循环此操作直至预设雷数为零。
void setmine(char board[ROWS][COLS], int row, int col)
{
int count = NUMBER_OF_MINES;
do
{
int i = rand() % ROW + 1;
int j = rand() % COL + 1;
if (board[i][j] == '0')
{
board[i][j] = '1';
count--;
}
} while (count);
}
操作(点击)函数实现
首先输入坐标,然后进行两次判断:①坐标是否越界 ②坐标是否已经排查。在不符合以上两个条件之后判断所点击的坐标产生的影响。若点到雷区,则游戏结束、展示棋盘并返回0。若未踩雷,则进入展开函数。我们知道如果点开的板块对应数字不是0的话是不会展开的,所以这里笔者是将两种情况都加入了展开函数,以方便书写。
int move(char mineboard[ROWS][COLS], char playerboard[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
while (1)
{
printf("请输入坐标:>\n");
scanf("%d %d", &i, &j);
if ((i < 1) || (j < 1) || (i > row - 1) || (j > col - 1))
{
printf("输入有误,请重新输入:>\n");
continue;
}
else if (playerboard[i][j] != '*')
{
printf("此处已排查,请更换坐标\n");
continue;
}
else
{
if (mineboard[i][j] == '1')
{
printf("很遗憾,游戏结束(>_<)\n");
displayboard(mineboard, ROWS, COLS);
printf("所踩雷的坐标为:%d %d",i,j);
return 0;
}
else
{
unfold_zero_area(mineboard, playerboard, i, j);
//displayboard(playerboard, ROWS, COLS);
return 1;
}
}
}
}
展开函数实现
由于此函数在点0时要进行成片展开,那就需要借助递归来完成,因此一上来我们就对于递归的范围进行了界定,以保证能够顺利跳出递归,也就是将范围控制在向玩家展示的棋盘之内。而后算出传进来的板块周围雷数,如果不为零,标出数字后函数结束;如果等于零且不等于空格(不等于空格是为了防止死递归,因为不进行判断的话,就会在相邻的空白之间无限递归,最终导致栈溢出),我们就将其赋值为空格,并且遍历其周围的八个格子每个格子都再调用一次展开函数,即可实现空白展开功能。(这里我们对于为0的区域进行赋空格处理,就像我们玩的扫雷一样,为0的区域是空白的)
void unfold_zero_area(char mineboard[ROWS][COLS], char playerboard[ROWS][COLS], int row, int col)
{
if (row > 0 && row <= ROW && col > 0 && col <= COL)
{
int count = get_mine_count(mineboard, row, col);
if (count != 0)
{
playerboard[row][col] = '0' + count;
}
else if (playerboard[row][col] != ' ')
{
playerboard[row][col] = ' ';
int i = 0;
for (int i = row - 1; i <= row + 1; i++)
{
int j = 0;
for (j = col - 1; j <= col + 1; j++)
{
unfold_zero_area(mineboard, playerboard, i, j);
}
}
}
else
{
;//结束递归
}
}
}
自动标记地雷函数实现
这里我们用’!‘来表示已经必然是地雷的区域。然后对于传进来的板块的周围进行雷数判断,如果未点开的板块(’*‘)加上已经必然是雷的板块(’!‘)的数量已经与传进来的板块上标记出的数字一致的话,则将全部未点开的板块设置为’!'。其实这里的代码存在一定程度上冗余,存在一定的改定空间,欢迎大家提出自己的理解,笔者会在第一时间进行改进。
void mark_mine(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i < ROW; i++)
{
int j = 0;
for (j = 1; j < COL; j++)
{
if (board[i][j] >= '1' && board[i][j] <= '8')
{
int count = 0;
if (board[i - 1][j] == '*' || board[i - 1][j] == '!')
{
count++;
}
if (board[i - 1][j - 1] == '*' || board[i - 1][j - 1] == '!')
{
count++;
}
if (board[i][j - 1] == '*' || board[i][j - 1] == '!')
{
count++;
}
if (board[i + 1][j - 1] == '*' || board[i + 1][j - 1] == '!')
{
count++;
}
if (board[i + 1][j] == '*' || board[i + 1][j] == '!')
{
count++;
}
if (board[i + 1][j + 1] == '*' || board[i + 1][j + 1] == '!')
{
count++;
}
if (board[i][j + 1] == '*' || board[i][j + 1] == '!')
{
count++;
}
if (board[i - 1][j + 1] == '*' || board[i - 1][j + 1] == '!')
{
count++;
}
if (count == board[i][j] - '0')
{
if (board[i - 1][j] == '*')
{
board[i - 1][j] = '!';
}
if (board[i - 1][j - 1] == '*')
{
board[i - 1][j - 1] = '!';
}
if (board[i][j - 1] == '*')
{
board[i][j - 1] = '!';
}
if (board[i + 1][j - 1] == '*')
{
board[i + 1][j - 1] = '!';
}
if (board[i + 1][j] == '*')
{
board[i + 1][j] = '!';
}
if (board[i + 1][j + 1] == '*')
{
board[i + 1][j + 1] = '!';
}
if (board[i][j + 1] == '*')
{
board[i][j + 1] = '!';
}
if (board[i - 1][j + 1] == '*')
{
board[i - 1][j + 1] = '!';
}
}
}
}
}
}
手动标记地雷函数实现
由于自动标雷函数的局限性,它并不能标记出所有的已知雷区。就比如我们所知道的“121”、“1221”等规律仍然需要我们来自行标记和点开,这是我们就可以用这个函数对已知雷进行标记(用’!'进行标记),以加快排雷效率,而且这里我们也利用的do-while函数,可在点开板块的间隙进行多次标记。
void if_mark(char board[ROWS][COLS], int row, int col)
{
int input = 0;
do {
displayboard(board, ROWS, COLS);
printf("是否进行标记?\n");
menu2();
scanf("%d", &input);
getchar();
switch (input)
{
case 1:
{
int x = 0;
int y = 0;
printf("请输入坐标:>\n");
scanf("%d %d", &x, &y);
if (board[x][y] != '*')
{
printf("输入坐标有误,请重新输入:>");
break;
}
else
{
board[x][y] = '!';
}
break;
}
case 0:
break;
default:
printf("输入有误,请重新输入\n");
break;
}
} while (input);
}
判断胜利函数实现
这里我们利用雷数进行判断,如果没点开的板块(‘*’)和已经确定是雷的板块(‘!’)数量已经等于预设雷数,即非雷区域已经全部打开,则游戏胜利并返回0;否则返回1,游戏继续。
int is_win(char mineboard[ROWS][COLS], char playerboard[ROWS][COLS], int row, int col)
{
int i = 0;
int count = 0;
for (i = 1; i < row - 1; i++)
{
int j = 0;
for (j = 1; j < col - 1; j++)
{
if (playerboard[i][j] == '*' || playerboard[i][j] == '!')
{
count++;
}
}
}
if (count == NUMBER_OF_MINES)
{
printf("游戏胜利\\^o^/\n");
displayboard(mineboard, ROWS, COLS);
return 0;
}
else
{
return 1;
}
}
游戏实际体验画面
结束语
以上便是笔者所完成的扫雷游戏,尽管实现了部分拓展功能,但仍有很多不足之处,希望能看到这篇文章的读者能够留下宝贵的意见和建议,使其能够不断地完善,笔者感激不尽。