在控制台里实现的扫雷(mine clearance)
今天为兄弟们带来扫雷游戏,emmm… 依然是在控制台里实现的扫雷,哈哈哈 ^ ^,当然后续会为兄弟们带来可以使用鼠标点击的窗口化扫雷游戏。这次呢,依然使用最基础的C语言语法来实现扫雷的逻辑。还是那句话,开发游戏带来的成就感是无与伦比的,希望兄弟们喜欢 ^ ^
搭建游戏框架
习惯性的,在玩游戏之前总是会询问玩家是否开始游戏,这或许就是我们熟知的菜单,可以选择玩或不玩,菜单可以华丽也可以朴素,我就写成以下代码咯
void menu()
{
printf("*******************************************\n");
printf("**************** 0、Exit ****************\n");
printf("**************** 1、Play ****************\n");
printf("*******************************************\n");
}
我们可以将小游戏的菜单直接写在main()
函数里。对于好玩的单机游戏来说,只玩一次显然是不过瘾的,我们可以通过循环来实现。从我写的菜单不难看出是选择数字控制是否开始游戏,也就是来控制循环。搭配使用while
循环和switch--case
语句就是很好的游戏框架,可以像如下代码实现main()
函数
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
system("cls");//清屏
menu();
printf("\n请输入对应的数字:> ");
scanf("%d", &input);
switch (input)
{
case 1:
Game();
system("pause");//程序暂停
break;
case 0:
printf("\n退出游戏!\n");
system("pause");
break;
default:
printf("\n输入错误,请重新输入!\n");
system("pause");
break;
}
} while (input);
return 0;
}
把1
放进变量input
就可以进入 实现游戏的根本函数Game()
。不过main()
函数里语句srand((unsigned int)time(NULL));
后面会解释;语句system("cls");
用于清屏,语句system("pause");
用于程序暂停,均需要引头文件<stdlib.h>
,可以优化玩家的游戏体验 ^ ^
为了便于管理,我会采用多文件实现游戏,我会把main()
函数、菜单menu()
函数和Game()
“地基”函数放在test.cpp
文件当中,把剩余用来实现游戏功能的函数单独放在Han.cpp
文件里,通过Han.h
头文件把test.cpp
和Han.cpp
联系起来,在自己写的头文件里进行 剩余游戏功能函数 的声明。注意引用自己头文件的时候要用双引号,eg: #include "Han.h"
例如我将main()
函数、菜单menu()
函数和Game()
函数放在test.cpp
文件里,将其他函数放在Han.cpp
文件里,自己写的头文件则是Han.h
,最后会奉上代码 ^ ^
游戏思路
相信兄弟们应该都玩过扫雷游戏 ^ ^ 给出一个雷盘,玩家随机点击坐标,然后会拨开一片区域,这片区域的边界会标有数字。数字的意义就是 以数字为中心的九宫格内雷的个数,利用此特点可以扫除所有的雷。那么就会有许多实现游戏的思路
- 只定义一个整形的雷盘,咱可以用-1表示雷,每个非雷坐标会根据其九宫格内雷的个数进行赋值,范围是(-1)到(+8)。雷盘内布置好雷也赋值好后,利用加减数字对雷盘加密,比如所有坐标都加上20,此时范围是19–28,这个范围内全部都打印*。玩家选择坐标后就解除该坐标的加密。这样选择后是玩家就可以区分游戏雷盘进行雷位置的推断,此方法较为简单
- 而我要写的是这个思路:定义两个字符雷盘,一个用于数据的保存
mine
(类似数据库),一个用于展示给玩家show
(类似图形化界面)。我们可以在mine
里对雷进行布置,1表示雷,0表示非雷,请注意:这里的1和0都是字符,执意存这两个进去是有原因的,咱后面再说。而show
刚开始就只需要将雷盘全部打印为*。当玩家对雷盘选择坐标时,会有三种情况:1、坐标是雷,游戏结束。2、该坐标的九宫格内无雷,拨开一片区域。 3、该坐标的九宫格内有雷,在此坐标上打印雷的个数。被炸死了咱就不说了哈 ^ ^ 没死的话根据逻辑可以继续排下去
判定游戏是否结束很简单,扫雷嘛,把所有非雷地区点开就好了,如果雷盘只剩下雷没有被点开,那就可以说扫雷成功游戏结束了 ^ ^
请注意:我们知道扫雷可以鼠标右击进行雷位置的标记,但这毕竟不是图形化的窗口编程可以使用鼠标进行点击,而在控制台里如果每一步都询问是否进行标记会使得游戏进程变得非常拖沓,所以标记功能我就不实现了,而标记带来的算法也不是难。后续为兄弟们带来可以使用鼠标点击的窗口化扫雷游戏时会为兄弟们实现标记功能 ^ ^
游戏的“地基”函数Game()
Game()
函数就是 用于实现游戏的函数 的大杂烩,扫雷游戏的步骤并不多,无非就是做一个雷盘出来,让玩家排除雷。由于我们采用第二个思路,所以首先我们要定义两个雷盘(二维数组)出来,然后给两个雷盘进行初始化,做完这一切就可以打印出show
雷盘,咱就可以在mine
里布置雷。接下来就可以让玩家进行排雷了,排雷过程中再顺便判定输赢就可以了,代码如下:
void Game()
{
printf("\n扫雷游戏!\n");
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);
//排查雷
FindMine(mine, show, ROW, COL);
}
封装 功能函数 实现游戏
单机游戏嘛,对于雷盘来说当然是我想玩多大就玩多大,想排多少雷就排多少,我们就可以利用宏定义来规定。可是你想啊,边界坐标在处理九宫格时是不是要考虑越界问题?为了省去些许麻烦,我们不如将雷盘定义大一点,比展示给玩家的大个一圈。这样我们就可以先定义玩家的雷盘大小,再定义我们需要处理的雷盘时只需要在其基础上+2
#define ROW 9 //玩家雷盘
#define COL 9
#define ROWS ROW+2 //代码处理雷盘
#define COLS COL+2
#define EASY_COUNT 10 //雷的个数
雷盘初始化
我们定义了雷盘,就需要对其进行初始化,本身二维数组可以用memset
函数进行初始化,考虑到不知道此函数的新手小白,亲自敲一遍也是可以的 ^ ^
我们可以达到如下效果
那么对于mine
来说,全初始化为0
,而show
来说是*
,所以传参时需要二维数组和其对应的初始化字符,当然行列传不传都可以,毕竟是宏定义void InitBoard(char board[ROWS][COLS], int rows, int cols, char ch)
像这样,函数内嵌套两个for
循环即可
//初始化扫雷平面
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ch)
{
for (int i = 0;i < rows;i++)
{
for (int j = 0;j < cols;j++)
{
board[i][j] = ch; //ch是对应的雷盘初始化字符
}
}
}
打印雷盘
很显然,雷盘的打印至关重要,雷盘较大,需要方便玩家看到行和列。那么排列美观性很影响游戏体验。
打印雷盘自然不用多说,嵌套两个for循环,但是数字打印呢?第一行数字(各自列号)自然是最先打印,紧接着是横线分割行,(但是前几列是占有空间的,可以看到是3格,横线分割行要少一格)。在每一行打印前需要打印各自的行号,放在两个循环中间就好,最后注意格式,如下是代码:
//打印扫雷的平面
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("\n ");
for (int i = 1;i <= row;i++) //列号
{
printf("%d ", i);
}
printf("\n "); //注意换行
for (int i = 1;i <= row;i++)
{
printf("--"); //横线分割行
}
printf("\n");
for (int i = 1;i <= row;i++)
{
printf("%d|", i); //行号
for (int j = 1;j <= col;j++)
{
printf(" %c", board[i][j]);
}
printf("\n");
}
printf("\n");
}
雷的布置
不可能每次都人为的去布置雷的坐标,所以随机布置就需要随机数的加持,随机函数rand()
用于产生随机数,而之前在 main()
函数里的语句 srand((unsigned int)time(NULL));
就是随机数的配置函数,种子就是时间,毕竟在一天内,时间是不一样的。需要引头文件<stdlib.h>
和 <time.h>
。可以使用随机数后就可以布置雷了,这里需要注意:雷的布置不能重复,否则不足宏定义的 EASY_COUNT
个,代码如下:
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0') //防止雷的坐标重复
{
board[x][y] = '1';
count--;
}
}
}
玩家排查雷
现在的情况就是,玩家面对一个雷盘可以进行扫雷了。那么我们需要两个变量 x
,y
来接收玩家输入的坐标。玩家只要还没被炸死或者没有排出所有的雷,那么游戏就会继续,显然while
循环会更加有利游戏的实现,在游戏思路里说到 如果雷盘只剩下雷没有被点开,那就可以说扫雷成功游戏结束了,如果定义一个全局变量win
用于计数被点开的坐标,那while
循环的控制条件呼之欲出,就是win < row * col - EASY_COUNT
。(在while
循环里需要注意x
,y
坐标的合法性)那么拿到玩家的坐标后,根据游戏思路所述,如果mine[x][y] == '1'
,玩家踩到了雷,游戏结束,没有的话就要计算这个坐标的九宫格内雷的个数,计算雷个数这个函数我们重新封装,请注意:不论是游戏以何种方式结束,总要玩家明白雷的位置,让他心服口服。而以上代码实现如下
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0, k = 0;
while (win < row * col - EASY_COUNT)
{
printf("\n请输入要排查的坐标:> ");
scanf("%d,%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) //坐标合法性判断
{
if (mine[x][y] == '1')
{
//如果是雷,则被炸死!
printf("很遗憾,您被炸死了!\n");
system("pause");
printf("\n以下是雷的位置......\n");
for (int i = 1;i <= row;i++)
{
for (int j = 1;j <= col;j++)
{
if (mine[i][j] == '0')
mine[i][j] = ' ';
}
}
DisplayBoard(mine, ROW, COL);
break;
}
else
{
MyGet(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标非法,请重新输入!\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
system("pause");
printf("\n以下是雷的位置......\n");
DisplayBoard(mine, ROW, COL);
}
}
递归实现所谓的扫雷机制:拨开一块区域
拨开一片区域的条件就是被点击的坐标的九宫格内没有雷,为了优化玩家游戏体验,就会拨开一片区域,区域的边缘会有数字,就像这样 ^ ^
具体思路就是:进入这个非雷坐标后,计算九宫格内雷个数。如果是0,就说明该坐标九宫格内没有雷,为了优化玩家游戏体验,我们需要拨开旁边的非雷坐标,就像上面这样。玩家选择的坐标被传进函数void MyGet(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
里,计算该坐标的九宫格内的雷的个数。有雷咱就返回,没雷就会依次进入该坐标旁边的八个坐标,再次进行计算这八个坐标九宫格内的雷个数,这八个坐标有雷就返回,没有雷继续依次进入这八个坐标旁边的八个坐标,此思想为递归!!!
为了防止无限递归下去,我们就把已经进入的坐标标为空格,空格坐标不会被再次进入且不会参加雷的计数!
注意:标记空格时两个雷盘都标,且加入win
的计数,同时递归时可能会越界,而越界会带来内存溢出警告。
代码如下:
void MyGet(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int c = GetMineCount(mine, x, y);
if (c != 0)
{
show[x][y] = (char)c + '0';
mine[x][y] = ' ';
win++;
}
else
{
win++; //计数
mine[x][y] = ' ';
show[x][y] = ' ';
for (int i = -1;i < 2;i++)
{
for (int j = -1;j < 2;j++)
{
if ((x + i < 1 || x + i > ROW || y + j < 1 || y + j > COL) || mine[x + i][y + j] == ' ')
continue;
else
{
MyGet(mine, show, x + i, y + j);
}
}
}
}
}
计数函数
在mine
雷盘里坚持使用字符0
和1
的原因就是好计数呀 ^ ^ ,因为1
表示的是雷呐,周围坐标内的数字加起来就好了。注意:0
和1
都是字符,字符0
的ASCLL码值是48,所以还要减去字符0
才是数字,eg: 0 = '0' - '0'
,1 = '1' - '0'
,以此类推 ^ ^ ,代码如下:
//获取该坐标九宫格内雷的个数
int GetMineCount(char board[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = -1;i < 2;i++)
{
for (int j = -1;j < 2;j++)
{
if (board[x + i][y + j] != ' ')
{
count += board[x + i][y + j] - '0';
}
}
}
return count;
}
完整代码
兄弟们,我使用的是vs2022平台,不知道如何使用多文件来实现游戏的兄弟可以私信我,或者评论区问我
test.cpp
#include "Han.h"
void menu()
{
printf("*******************************************\n");
printf("**************** 0、Exit ****************\n");
printf("**************** 1、Play ****************\n");
printf("*******************************************\n");
}
void Game()
{
printf("\n扫雷游戏!\n");
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);
//排查雷
FindMine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
system("cls");
menu();
printf("\n请输入对应的数字:> ");
scanf("%d", &input);
switch (input)
{
case 1:
Game();
system("pause");
break;
case 0:
printf("\n退出游戏!\n");
system("pause");
break;
default:
printf("\n输入错误,请重新输入!\n");
system("pause");
break;
}
} while (input);
return 0;
}
Han.cpp
#include "Han.h"
int win = 0;
//初始化扫雷平面
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ch)
{
for (int i = 0;i < rows;i++)
{
for (int j = 0;j < cols;j++)
{
board[i][j] = ch;
}
}
}
//打印扫雷的平面
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("\n ");
for (int i = 1;i <= row;i++)
{
printf("%d ", i);
}
printf("\n ");
for (int i = 1;i <= row;i++)
{
printf("--");
}
printf("\n");
for (int i = 1;i <= row;i++)
{
printf("%d|", i);
for (int 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_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
//获取该坐标九宫格内雷的个数
int GetMineCount(char board[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = -1;i < 2;i++)
{
for (int j = -1;j < 2;j++)
{
if (board[x + i][y + j] != ' ')
{
count += board[x + i][y + j] - '0';
}
}
}
return count;
}
//递归算法
void MyGet(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int c = GetMineCount(mine, x, y);
if (c != 0)
{
show[x][y] = (char)c + '0';
mine[x][y] = ' ';
win++;
}
else
{
win++;
mine[x][y] = ' ';
show[x][y] = ' ';
for (int i = -1;i < 2;i++)
{
for (int j = -1;j < 2;j++)
{
if ((x + i < 1 || x + i > ROW || y + j < 1 || y + j > COL) || mine[x + i][y + j] == ' ')
continue;
else
{
MyGet(mine, show, x + i, y + j);
}
}
}
}
}
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0, k = 0;
while (win < row * col - EASY_COUNT)
{
printf("\n请输入要排查的坐标:> ");
scanf("%d,%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
//如果是雷,则被炸死!
printf("很遗憾,您被炸死了!\n");
system("pause");
printf("\n以下是雷的位置......\n");
for (int i = 1;i <= row;i++)
{
for (int j = 1;j <= col;j++)
{
if (mine[i][j] == '0')
mine[i][j] = ' ';
}
}
DisplayBoard(mine, ROW, COL);
break;
}
else
{
MyGet(mine, show, x, y);
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标非法,请重新输入!\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
system("pause");
printf("\n以下是雷的位置......\n");
DisplayBoard(mine, ROW, COL);
}
}
Han.h
#pragma once
#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 InitBoard(char board[ROWS][COLS], int rows, int cols, char ch);
//打印扫雷的平面
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);