在上一章中,我们对三子棋进行了细致的分析与实现,这一章中,我们将完成另一个小游戏---就是我们耳熟能详的扫雷,那么就让我们开始设计这个小时候的游戏吧
扫雷小游戏
基本逻辑
扫雷小游戏我们同样利用键盘来进行操作,这里先把扫雷游戏的基本逻辑进行阐述,大家在之前玩的时候,首先会有一个大面板,面板上会有许多的小格子,小格子不确定有没有地雷,需要玩家去点击进行探测,如果探测踩到地雷了,那么就游戏结束,如果没有踩到地雷,便会在点击的那么格子中显示除它之外周围8个格子中地雷的个数,那么这时候便会有一个问题,就是当我们探测到边边角角的时候,并没有8个格子让我们来计算,所以在这里我们对面板进行扩张处理,比如需要10*10的区域来玩,我们就创建12*12的面板,将外围格子全部置0,此时便只使用内核来进行操作,在扫雷中我们不断扫,当剩余未被探测的个数与事先埋好的地雷数相同的时候,便取得了游戏的胜利,这便是游戏的基本逻辑,下面我们对面板进行大致模拟
1.设计游戏思路
准备工作
在我们开始进行设计之前,可以想像得到我们需要对其进行设置宏定义,显而易见需要设置长ROW,宽COL,雷的个数NUM,以及未被探测时的显示,接下来我们进行编写mine.h函数来引入宏定义以及其他基础代码
#pragma once//防止头文件被重复包含
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define ROW 8
#define COL 8
#define STYLE '?'
#define NUM 20
extern void Game();
与三子棋一样,菜单的设置基本一样,在扫雷游戏中,我们计划设置2种调用情况,1.开始扫雷游戏 0.退出,所以我们需要对之前的菜单进行稍微的修改
#include "clear_mine.h"
static void Menu()
{
printf("####################\n");
printf("# 1. Play 0.Exit \n");
printf("####################\n");
}
int main()
{
int quit = 0;
int select = 0;
while (!quit){
Menu();
printf("Please Enter# ");
scanf("%d", &select);
switch (select){
case 1:
Game();
break;
case 0:
quit = 1;
break;
default:
printf("Postion Error, Try Again!\n");
break;
}
}
printf("byebye!\n");
system("pause");
return 0;
}
在此不再赘述main函数中的过多细节,同三子棋基本一致
2.设计游戏逻辑
接下来进入我们真正的扫雷游戏的编写
可以料想得到,当我们进行扫雷游戏踩到雷被炸死结束时,我们需要将所有雷的具体位置显示出来,这时候如果在原先的面板中进行显示将是非常麻烦的,所以我们在定义面板时候,定义两个相同的,一模一样的面板,一个是给用户进行探测,可以改变的面板show_board,另一个是在我们游戏逻辑中,不可改变的,显示所有雷具体位置的面板mine_board,此时我们便需要对两个面板进行初始化,我们将1代表有雷,0代表没有雷,这里我们调用memset函数来对面板在内存层面进行初始化(与之前数组方法效果一致)
char show_board[ROW][COL];
char mine_board[ROW][COL];
memset(show_board, STYLE, sizeof(show_board));
memset(mine_board, '0', sizeof(mine_board));
当我们对面板进行初始化了之后,首先需要的就是调用SetMines来进行埋雷,埋好之后就需要ShowBoard函数显示我们的面板了,接下来准备工作完成,便需要用户在键盘中输入坐标来进行探测,此时需要限定用户输入的合法性,注意,我们在定义面板的时候,真正进行游戏的是中间的那部分,而不是整个长宽都可以探测,所以我们需要在左右及上下都需要对长宽进行减一操作,一共是减去2,来限定我们输入的合法性,当范围被确定时,我们还需要判断位置是否已经被探测,当这个位置未被探测且在范围之内时,便可以进行探测,在可以探测的条件下我们还需要判断这个位置是否有雷,如果有雷则game over并将雷的位置通过ShowBoard函数将我们的mine_board函数整体显示给用户,如果没有雷则需要调用一个Count_Mines函数将Mines图中的雷的个数显示到当前格子中,这时候便完成了我们一次的探测,最后需要在外围套上一层while循环来重复判断即可,当我们重复探测直到减去雷数后未探测的面板数减为0时,则用户胜利,我们将上述想法用代码进行实现
void Game()
{
srand((unsigned long)time(NULL));//种一颗随机数种子,在埋雷的时候会用到
char show_board[ROW][COL];//用户面板
char mine_board[ROW][COL];//雷面板
memset(show_board, STYLE, sizeof(show_board));//初始化用户面板
memset(mine_board, '0', sizeof(mine_board));//初始化雷面板
SetMines(mine_board, ROW, COL);//埋雷
int count = (ROW - 2)*(COL - 2) - NUM;//减去雷之后未探测的格子数
while (count){
system("cls");//刷新屏幕
ShowBoard(show_board, ROW, COL);//显示用户面板
printf("Please Enter Your Postion<x,y># ");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);//用户输入
if (x < 1 || x > ROW-2 || y < 1 || y > COL-2){//判断范围
printf("Postion Error!\n");
continue;
}
if (show_board[x][y] != STYLE){//判断是否已被探测
printf("Postion Is not *\n");
continue;
}
if (mine_board[x][y] == '1'){//从雷图中判断踩到雷
printf("game over!\n");
ShowBoard(mine_board, ROW, COL);//显示雷图
break;
}
//['0', '8']
show_board[x][y] = CountMines(mine_board, x, y);//显示周围雷的个数
count--;//减去雷之后未探测的格子数减一
}
}
这边是我们对于扫雷游戏的整体框架设置,接下来需要完成的是各个函数的具体实现
3.设计函数
在我们主框架中出现了很多我们需要设计的函数,自顶而下的设计方式,接下来我们对他们进行封装处理并实现
SetMines(mine_board, ROW, COL);
ShowBoard(show_board, ROW, COL);
CountMines(mine_board, x, y);
对于扫雷游戏的编写只需要建立这3个接口便可以完成
SetMines函数
我们的埋雷函数,需要利用随机数来对特定长宽范围进行赋值,注意的是我们用的是ROW与COL的内核,所以需要对他们分别减2,初始设置雷数,利用while循环对面板进行随机赋值,注意,如果该位置已经被埋下了雷,就不能继续埋雷,所以需要先对位置进行是否没有雷的判断,而后才可以进行埋雷,当埋一个雷时就将总个数减一
static void SetMines(char board[][COL], int row, int col)
{
int count = NUM;
while (count){
int x = rand() % (row - 2) + 1;
int y = rand() % (col - 2) + 1;
if (board[x][y] == '0'){
board[x][y] = '1';
count--;
}
}
}
CountMines函数
对于计数函数,因为我们之前对没有雷的格子赋值为字符‘0’,有雷的个数赋值为‘1’,所以在我们返回总个数的时候只需要将探测周边8个位置的字符加起来再都减去字符‘0’就可以了(ASIC码中会将其转换为加和的那个整型数字)而后我们再对其进行加上一个‘0’便可将那个得到的值重新转成字符,所以我们需要减去7个‘0’,而且我们不需要考虑在边角中的特殊情况,因为我们将边角的值都赋成了‘0’,加和不影响总值
static char CountMines(char board[][COL], int x, int y)
{
return board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + \
board[x][y + 1] + board[x + 1][y + 1] + board[x + 1][y] + \
board[x + 1][y - 1] + board[x][y - 1] - 7 * '0';
}
ShowBoard函数
接下来便是我们复杂度最高的ShowBoard函数,虽然其代码逻辑简单,但是需要不断地调试移动位置大小,所以复杂度会很高,我们尽量对其进行可维护性调整,再创建了一个ShowLine函数用于控制横线,以方便我们创建面板
static void ShowLine(int col)//显示横线的函数,方便随ROW与COL变化
{
for (int i = 0; i <= (col - 2); i++){
printf("----");
}
printf("\n");
}
static void ShowBoard(char board[][COL], int row, int col)
{
printf(" ");
for (int i = 1; i <= (col - 2); i++){
printf("%d ", i);
}
printf("\n");
ShowLine(col);
for (int i = 1; i <= (row - 2); i++){
printf("%-3d|", i);
for (int j = 1; j <= (col - 2); j++){
printf(" %c |", board[i][j]);
}
printf("\n");
ShowLine(col);
}
}
自此我们的扫雷游戏就得到了完成,下面演示整体代码
//mine.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#define ROW 8
#define COL 8
#define STYLE '?'
#define NUM 20
extern void Game();
//mine.c
#include "mine.h"
static void SetMines(char board[][COL], int row, int col)
{
int count = NUM;
while (count){
int x = rand() % (row - 2) + 1;
int y = rand() % (col - 2) + 1;
if (board[x][y] == '0'){
board[x][y] = '1';
count--;
}
}
}
static void ShowLine(int col)
{
for (int i = 0; i <= (col - 2); i++){
printf("----");
}
printf("\n");
}
static void ShowBoard(char board[][COL], int row, int col)
{
printf(" ");
for (int i = 1; i <= (col - 2); i++){
printf("%d ", i);
}
printf("\n");
ShowLine(col);
for (int i = 1; i <= (row - 2); i++){
printf("%-3d|", i);
for (int j = 1; j <= (col - 2); j++){
printf(" %c |", board[i][j]);
}
printf("\n");
ShowLine(col);
}
}
static char CountMines(char board[][COL], int x, int y)
{
return board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + \
board[x][y + 1] + board[x + 1][y + 1] + board[x + 1][y] + \
board[x + 1][y - 1] + board[x][y - 1] - 7 * '0';
}
void Game()
{
srand((unsigned long)time(NULL));
char show_board[ROW][COL];//用户面板
char mine_board[ROW][COL];//雷面板
memset(show_board, STYLE, sizeof(show_board));//初始化用户面板
memset(mine_board, '0', sizeof(mine_board));//初始化雷面板
SetMines(mine_board, ROW, COL);//埋雷
int count = (ROW - 2)*(COL - 2) - NUM;//减去雷之后未探测的格子数
while (count){
system("cls");//刷新屏幕
ShowBoard(show_board, ROW, COL);//显示用户面板
printf("Please Enter Your Postion<x,y># ");
int x = 0;
int y = 0;
scanf("%d %d", &x, &y);//用户输入
if (x < 1 || x > ROW-2 || y < 1 || y > COL-2){//判断范围
printf("Postion Error!\n");
continue;
}
if (show_board[x][y] != STYLE){//判断是否已被探测
printf("Postion Is not *\n");
continue;
}
if (mine_board[x][y] == '1'){//从雷图中判断踩到雷
printf("game over!\n");
ShowBoard(mine_board, ROW, COL);//显示雷图
break;
}
//['0', '8']
show_board[x][y] = CountMines(mine_board, x, y);//显示周围雷的个数
count--;//减去雷之后未探测的格子数减一
}
}
//main.c
#include "mine.h"
static void Menu()
{
printf("###################\n");
printf("# 1. Play 0.Exit \n");
printf("###################\n");
}
int main()
{
int quit = 0;
int select = 0;
while (!quit){
Menu();
printf("Please Enter# ");
scanf("%d", &select);
switch (select){
case 1:
Game();
break;
case 0:
quit = 1;
break;
default:
printf("Postion Error, Try Again!\n");
break;
}
}
printf("byebye!\n");
system("pause");
return 0;
}
这便是扫雷游戏的所有代码,接下来我们对其进行思维导图分析
以上便是我们的扫雷游戏小项目的实现与总结,我们共同学习,共同进步