目录
EX-简要引入:
扫雷游戏这款简单易上手的小游戏想必大家都玩过,这里是某个扫雷游戏的网页版网址扫雷游戏网页版 - Minesweeper,可自行点进去体验。
一、游戏规则
首先先得知道游戏规则,假设你选的是9*9的简单模式,即9*9的棋盘会显现在屏幕上,并且81个小格子里有布置好的10个雷,当你点击某个位置时,如果这个位置刚好有雷,那么屏幕就会显示你被炸死;反之,这个格子会计算好周围八个格子的雷的个数并显示在屏幕上(现在不考虑若是附近都没有雷则将全部不是雷的全展开的拓展),直到将所有的雷都排查完毕,即71个格子,中间如果被雷炸死则要重来。
二、游戏整体的框架实现——test.c
Okay,根据游戏规则,我们就可以来思考,要怎么用C语言来实现这个游戏,达到我们可以在控制台上玩这个小游戏的目的。刚开始有一个游戏菜单是较为合适的,因为有指引,玩家才会知道下一步要干什么。接着我们会考虑用以下的代码来实现菜单功能,单独封装成一个函数会使主函数没有那么累赘。
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**********************\n");
}
之所以会用void来封装函数,是因为,该函数不需要返回任何值,此外,为了简洁美观,考虑在写菜单的时候尽量对称。
接着用户需要输入一个值来选择是要进入游戏还是要退出游戏,那么用户就有可能输入1、0或者其他数。这时候可以用switch-case来进行条件判断。
int input=0;
scanf("%d",&input);
switch(input)
{
case 1:
game();
break;
case 0;
printf("退出游戏");
break;
default:
printf("输入错误,请重新输入。");
break;
}
想要让游戏可以进行多次,就需要写个循环,这时候由于没有可以条件可以用来判断,并且在一开始就打印菜单,所以可以考虑用do-while来写,循环的次数比条件判断的次数要多一次,然而我们仍然需要设置一个条件来判断循环的进入。因为只有当用户输入的是0时,才会退出游戏界面,所以不妨将条件设置为input,即当非0时都会进入循环。即下面的代码
do
{
int input=0;
scanf("%d",&input);
switch(input)
{
case 1:
game();
break;
case 0;
printf("退出游戏");
break;
default:
printf("输入错误,请重新输入。");
break;
}
}while(input);
接下来才是大头(big challenge)。即游戏函数的实现。这里我们可以单独用一个源文件来写函数的实现逻辑,命名为game.c(源文件test.c可以单独用来写游戏逻辑的测试,而源文件game.c可以用来单独写游戏逻辑的实现,头文件game.h可以用来写游戏的函数声明)
三、游戏逻辑的声明与实现——game.h、game.c
1、棋盘的创建及初始化
(1)棋盘是来干嘛的?
在用户进行选择进入游戏之后,我们需要写一个游戏函数来实现扫雷游戏。前面说了游戏界面会有9*9的棋盘,于是我们会考虑用二维数组来构建该棋盘。为了便于拓展游戏后续的功能,可以用define来定义棋盘的行和列,而不是直接在定义数组的时候直接写数组的行和列有多少。我们创建棋盘就会忍不住思考这个棋盘是用来干什么的。棋盘应该是有如下几个功能,一是当游戏开始的时候,展现给用户的是“空白棋盘”,可以理解为即任何关于雷的信息都没有,而只有一个棋盘,待用户去点击,于是我们可以想到用*来表示棋盘的每一个格子(元素),等到用户点击(进行操作了)才会显现接下来是什么样的操作。这也就引出了第二个功能——计算并显示该位置周围有几个雷,那么前提是棋盘在此之前就有存放着关于雷的信息。但是要实现第二个功能也需要存放排查出来的雷的信息,这两个棋盘不能是同个棋盘,只能再创建另一个棋盘。于是我们需要创建两个数组,一个是用来存放所布置的雷的信息,一个是用来存放所排查出来的雷的信息(之后会呈现给用户,告诉用户此位置周围存在几个雷)。不妨一个称为mine,一个称为show(根据功能而定的名称)
(2)创建怎么样的棋盘
游戏界面会有9*9的棋盘,那么我们应创建9*9的二维数组吗?不见得,因为在排查雷的时候,我们是在排查所排查位置的周围八个位置(即以所排查位置为中心组成的九宫格除自己之外的位置)那么就有一个问题了,当所排查位置是边界线的话,在访问周围的位置时就会出现越界的情况。根据需要,便会想到用11*11的二维数组。这样在访问的时候就不会出现越界的情况。那么两个棋盘都需要创建11*11的数组吗,是的,为了之后将排查出来的雷的信息存放进show数组,最好是两个大小相同的数组,易于访问和存放数据。
创建好数组之后,我们自然会想到要怎么初始化数组,由于后期需要进行布置雷的操作,我们将mine数组全部初始化为数字0,表示还没进行布置雷,而show数组可以全部初始化为字符*,表示还未进行排查雷(排查的信息还未存进数组)。为了便于访问所以我们也可以考虑将mine数组全部初始化为字符0,后期存雷用字符1来表示此处有雷。而我们可以封装同个函数来初始化这两个数组。
我们便可以写下如下的代码:
#define ROW 9
#define COL 9
#define ROWS row+2
#define COLS col+2
//初始化棋盘
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 game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化棋盘
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*);
}
2、打印棋盘
要怎么使棋盘在操作台上显示?也就是要怎么打印二维数组?为了方便用户查看每个位置所处的行和列以此来进行后面的排雷操作,我们可以在棋盘周围有意地打印出行和列的序号。先打印列,后打印行,每打印行的序号后就开始打印棋盘的内容。于是可以有下面的代码
//打印棋盘
void DisplayBoard(char board[ROWS][COLS]int row,int col)
{
int i=0;
int j=0;
for(i=0;i<=col;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");
}
}
可以打印棋盘之后,就可以和前面的代码进行联系
//game函数逻辑的实现
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化棋盘
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*);
//打印棋盘
//DisplayBoard(mine,ROW,COL)可以打印出来看看效果
DisplayBoard(show,ROW,COL)
}
3、布置雷
用户看到操作题台上有棋盘之后(全是字符*的棋盘),就会进行扫雷,但扫雷的前提是得有雷。于是这一步我们开始布置雷。9*9的简单模式是有十处雷,于是我们需要随机地布置雷。这时候就需要用到随机函数了。先有个随机数种子,后面随机生成位置。然后对这个位置进行赋值,照前面所说我们可以赋值为字符‘1’。于是就有了下面的代码
#include<stdlib.h>
#include<time.h>
#define EASY_COUNT 10
int main()
{
//生成随机数种子
srand((unsigned)time(NULL);
}
//布置雷
void SetMine(char mine[ROWS][COLS],int row,int col)
{
while(EASY_COUNT)
{
int x=rand()%row+1;//row是9,对row进行取余得到0~8,后面再加1,得到1~9即棋盘横坐标
int y=rand()%col+1;
mine[i][j]='1';
EASY_COUNT--;
}
}
4、排查雷
(1)FindMine函数
布置好雷之后我们就可以进行排查雷的操作。在操作台给用户一个show棋盘之后。用户需要输入一个坐标表示所需要排查的对象。首先应该考虑输入的坐标是否合理(合法)即是否在棋盘范围内(9*9),其次再继续进行下面的判断。一是要判断该位置是否有雷,如果有雷,则显示“很遗憾,你被炸死”反之计算该位置周围的所有雷,该操作可以再封装成一个函数GetMine。由于排雷过程中需要访问两个数组,一个是来排查雷的mine数组,一个是用来存放排查雷的信息的数组即show数组。但这样写显然还是不够的,只排查一次雷吗?那什么时候游戏会结束呢?由于我们设置的是简单模式,所以可以考虑当用户没有触及那十个雷之后便宣告用户赢得游戏。即用户需要输入71(9*9-10)个不是雷的坐标才能赢得游戏。那我们就需要设置个循环,然后在必要的时候比如踩到雷或者已经排除掉所有雷的时候退出循环。于是就有下面的代码。
void FindMine(char mine[ROWS][COLS],int rows,int cols)
{
int win=0;
while(win<ROW*COL-EASY_COUNT)
{ int x,y;
printf(“请输入你要排查的位置:\n”);
scanf("%d %d",x,y);
if(x>0&&x<rows-1&&y>0&&y<cols-1)
{
if(mine[x][y]=='1')
{
printf("很遗憾,你被炸死了。");
DispayBoard(mine,ROW,COL);
break;
}
else
{
GetMineCount(mine,rows.cols);//数雷有几个,将排查得到的雷个数记为count
show[x][y]=count+'0';//count是数字,要转化成字符需要再加'0'的ASCII码值
Dispaly(show,ROW,COL);
printf("\n");
win++;
}
}
else
{
printf("你输入的坐标非法,请再次输入。\n");
break;
}
}
if(win==ROW*COL-EASY_COUNT)
{
printf("恭喜你,排雷成功!);
}
}
(2)GetMineCount函数
上面代码中需要再进行写一个计算雷个数并存进show数组的函数。先明确目标我们要排查的是以该位置为中心周围的八个位置。那么我们就需要对mine数组进行访问。这里需要特别注意的是mine数组中的元素是字符1或者字符0,不能通过简单的加减来计算,而是要将所有八个位置统计加在一块后再减去8*‘0’,因为计算机是将字符转换成对应的ASCII码值再进行计算的。有两个方式写该函数,一个是直接写,一个是通过循环写,如下。
//计算雷的函数
int GetMineCount(char mine[ROWS][COLS],char show[ROWS][COLS],int rows,int cols)
{
//方式一:直接法
//int count=mine[x][y-1]+mine[x][y+1]+mine[x-1][y-1]+mine[x-1][y]+mine[x-1][y+1]+mine[x+1][y-1]+mine[x+1][y]+mine[x+1][y+1]-8*'0';
//retrun count;
//方式二:循环法
int i,j;
int count=0;
for(i=x-1;i<=x+1;i++)
{
for(j=y-1;j<=y+1;j++)
{
if(mine[i][j]=='1')
{
count++;
}
}
return count;
}
至此,基本上游戏的实现逻辑已经写完了,接下来是进行整合和拓展。
四、整合和拓展
(1)整合
下面将所有代码进行整理,放进不同的文件中。
//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);
void DisplayBoard(char board[ROWS][COLS], int row,int col);
void SetMine(char board[ROW][COL], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS],int row ,int col);
//game.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
//初始化棋盘
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 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--;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
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");
}
}
static int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
//直接写
/*return mine[x - 1][y] +
mine[x - 1][y + 1] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x + 1][y + 1] +
mine[x + 1][y - 1] +
mine[x+1][y]-8 * '0';*/
//或者写个for循环
int count = 0;
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '1')
{
count ++;
}
}
}
return count;
}
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS],int row, int col)
{
int x, y;
int win = 0;
while (win<row*col-EASY_COUNT)
{
//DisplayBoard(mine, ROW, COL);可写来检查排查函数FindMine和GetMine函数是否写对了
//printf("\n");
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//如果不是雷的话,就数数该位置附近有多少雷
{
int count=GetMineCount(mine,x,y);
show[x][y] = count+'0';
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
printf("\n");
win++;
}
}
else
{
printf("坐标非法,请再次输入坐标");
break;
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功");
}
}
//test.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include"game.h"
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**********************\n");
}
void game()
{
printf("——扫雷游戏开始——\n");
char mine[ROWS][COLS];//存放布置好雷的信息
char show[ROWS][COLS];//存放排查出来的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');//初始化所有的棋盘为字符零
InitBoard(show, ROWS, COLS, '*');//初始化所有的棋盘为字符*
//布置雷
SetMine(mine, ROW, COL);
//打印棋盘
//DisplayBoard(mine, ROW, COL);
//printf("\n");
DisplayBoard(show, ROW, COL);
printf("\n");
//DisplayBoard(mine, ROW, COL);
//排查雷
FindMine(mine,show, ROW, COL);
}
int main()
{
int input=0;
srand((unsigned)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("扫雷游戏结束,退出游戏");
break;
default:
printf("输入错误,请再次输入");
break;
}
} while (input);
return 0;
}
(2)拓展
上面写的是简单模式, 下面我们可以试着朝中等模式和困难模式写。
简单模式:9*9 10个雷
中等模式:16*16 40个雷
困难模式:30*16 99个雷
模式的更改只需要改动行列和雷个数的数值以及打印格式的调整。
此外还可以进行的拓展有,①假如排查位置没有雷,而且周围也没有雷,可不可以将该位置周围的没有雷的部分全部展开。②是否可以标记雷③是否可以加上排雷的倒计时显示?
让我们在下一篇文章见!