目录
前言:
在上一篇文章中,我和各位小伙伴们一起,成功编写出了一个基础版扫雷游戏。但作为基础版的它依旧存在着一些缺陷,而在本文中,我们将要对基础版扫雷游戏进行功能、界面两个方面的优化。
一、功能优化:
1.清理展开优化:
我们上次说到,在我们进行扫雷时,当我们对某坐标进行排雷时,只能排除该坐标,而不能像真正的扫雷游戏一样,在排除一个坐标时,选中一次就可以展开相邻的一大片安全位置。我们现在就来增加并优化这个功能。
我们该怎么优化这个功能呢?首先,我们执行该功能的前提是该位置是安全的。则我们将该功能写成一个函数,并将其添加在我们之前就写好的,判断该位置没有“地雷”、已经判断安全的位置上:
else
{
int n = get_mine_count(mine, x, y);
show[x][y] = n + '0';
//清除展开函数:
Cls(mine, show, row, col, x, y);
DisplayBoard(show, ROW, COL);
win++;
break;
}
接下来就是这个函数功能的实现了,首先当前选中位置已经确定为安全了,则将数组与该位置坐标全部传入函数中,若该位置存放的位置为' 0 ',则说明该以位置为中心的九宫格内均不是“地雷”,则将该位置替换为空格符' '并放置在展示数组中,在每一次清除展开处理结束后打印展示给玩家:
void Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (show[x][y] == '0')
//若此元素已经为'0',则进入此函数
{
show[x][y] = ' ';
}
}
当然,仅仅只处理选中坐标是远远不够的,我们的目标是一次性处理大片的坐标。我们的处理方法是,通过遍历来对以该坐标为中心的九宫格内其他坐标进行相同的操作:
void Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (show[x][y] == '0')
//若此坐标内元素为'0',则进入此函数
{
show[x][y] = ' ';
int i = x - 1;
for (; i <= x + 1; i++)
{
int j = y - 1;
for (; j <= y + 1; j++)
{
if (i >= 1 && i <= row && j >= 1 && j <= col)
//检查坐标越界
{
int ret = get_mine_count(mine, i, j);
show[i][j] = '0' + ret;
}
}
}
}
}
同时,在对周围坐标进行处理前,若周围坐标已经在之前的处理中被替换为了空格符,则不再进行接下来的替换操作,进行下一个坐标的判断与操作:
void Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (show[x][y] == '0')
//若此坐标内元素为'0',则进入此函数
{
show[x][y] = ' ';
int i = x - 1;
for (; i <= x + 1; i++)
{
int j = y - 1;
for (; j <= y + 1; j++)
{
if (show[i][j] == ' ')
{
continue;
}
if (i >= 1 && i <= row && j >= 1 && j <= col)
//检查坐标越界
{
int ret = get_mine_count(mine, i, j);
show[i][j] = '0' + ret;
Cls(mine, show, row, col, i, j);
//不断迭代,直到不再出现0或数字为止
}
}
}
}
}
同时我们还要注意之前进行过周围计数处理,并且需要在屏幕上打印出周围“地雷”数量的坐标,我们也要将其跳过,不能进行重复处理:
void Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (show[x][y] == '0')
//若此坐标内元素为'0',则进入此函数
{
show[x][y] = ' ';
int i = x - 1;
for (; i <= x + 1; i++)
{
int j = y - 1;
for (; j <= y + 1; j++)
{
if (show[i][j] == ' ')
{
continue;
}
if (i >= 1 && i <= row && j >= 1 && j <= col)
//检查坐标越界
{
if (show[i][j] != '*')
//如果该元素不是'*',即已经被赋值,则无需进行下面的操作
{
continue;
}
int ret = get_mine_count(mine, i, j);
show[i][j] = '0' + ret;
}
}
}
}
}
最后,我们通过使用迭代处理,对最初的坐标周围九宫格内的坐标进行相同的上述操作,清除展开功能实现:
void Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (show[x][y] == '0')
//若此坐标内元素为'0',则进入此函数
{
show[x][y] = ' ';
int i = x - 1;
for (; i <= x + 1; i++)
{
int j = y - 1;
for (; j <= y + 1; j++)
{
if (show[i][j] == ' ')
{
continue;
}
if (i >= 1 && i <= row && j >= 1 && j <= col)
//检查坐标越界
{
if (show[i][j] != '*')
//如果该元素不是'*',即已经被赋值,则无需进行下面的操作
{
continue;
}
int ret = get_mine_count(mine, i, j);
show[i][j] = '0' + ret;
Cls(mine, show, row, col, i, j);
//不断迭代,直到不再出现0或数字为止
}
}
}
}
}
2.估测地雷标记:
为了方便玩家更好的进行排雷,我们还可以在每次排雷前向玩家提供一个标记地雷的功能,来帮助玩家对排雷推理的展示更加清晰,使玩家的游戏过程更加轻松:
void PlayerJudge(char show[ROWS][COLS], int row, int col)
{
int m = 0;
do
{
printf("请问您是否想要标记某位置?\n");
printf("是:1 否:0:");
scanf("%d", &m);
switch (m)
{
case 1:
Judge(show, row, col);
break;
case 0:
break;
default:
printf("输入有误,请重新输入:\n");
break;
}
} while (m);
}
实现原理类似于菜单函数的处理,通过do-while语句与switch语句的结合使用来达到我们的目的。
而该功能的重点依旧是标记功能函数Judge的实现定义:
void Judge(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
int a = 0;
int b = 0;
printf("请输入你想标记的坐标行号:");
scanf("%d", &a);
printf("请输入你想标记的坐标列号:");
scanf("%d", &b);
if (a >= 1 && a <= row && b >= 1 && b <= col)
{
x = a;
y = b;
break;
}
else
{
printf("您选择的坐标有误,请重新选择!\n");
}
}
while (1)
{
printf("1 - 不确定是否为雷\n");
printf("2 - 确定是雷\n");
printf("3 - 取消标记\n");
printf("4 - 取消操作\n");
printf("请输入你想对该坐标进行的操作:");
int c = 0;
scanf("%d", &c);
if (show[x][y] == '*')
{
if (c == 1)
{
c = 63;
//符号?对应的ASCII码值为63
}
if (c == 2)
{
c = 35;
//符号#对应的ASCII码值为35
}
if (c == 4)
{
printf("操作已取消,请重新选择!\n");
DisplayBoard(show, row, col);
break;
}
char ch = c;
show[x][y] = ch;
printf("标记成功!\n");
DisplayBoard(show, row, col);
break;
}
else if (show[x][y] == '?' || show[x][y] == '#' && c == 3)
//如果输入3还要判断该元素是否已经被标记
{
show[x][y] = '*';
printf("取消成功!\n");
DisplayBoard(show, row, col);
break;
}
if (c == 4)
{
printf("操作已取消,请重新选择!\n");
break;
}
else
{
printf("输入位置不合法,请重新输入\n");
}
}
}
首先,我们让玩家按照一定格式输入想要进行标记的坐标,并对该坐标进行合理性确认。接着我们定义出一个整型变量C,用于接收玩家的标记操作输入选择,这个时候我们需要对玩家输入的操作选项C进行分类操作,确保进行正确的对应操作,同时对玩家的标记操作选择合理性也应当进行检查。
这里我们需要注意,当我们进行取消操作时,会出现两种情况,即该位置上的符号可能为' # ',也可能不为' # ',所以我们在对选项4进行处理时,应当对各种情况均考虑到位。
3.剩余雷量显示:
在上面的游戏中,我们便可以进行排雷和标记,则我们可以再度添加一个剩余雷量显示来辅助玩家进行游戏:
else
{
int n = get_mine_count(mine, x, y);
show[x][y] = n + '0';
Cls(mine, show, row, col, x, y);
int res = LeaveMine(show, row, col);
printf("剩余雷量:%d\n", EASY_COUNT - res);
DisplayBoard(show, ROW, COL);
win++;
break;
}
剩余雷量的计算则十分简单了,它的计算不是去计算真实的雷量,我们只需要采用遍历思想,计算展示数组show中被我们标记为“地雷”的表示符号' # '即可:
int LeaveMine(char show[ROWS][COLS], int row, int col)
{
int i = 1;
int num = 0;
for (i = 1; i <= row; i++)
{
int j = 1;
for (j = 1; j <= col; j++)
{
if (show[i][j] == '#')
{
num++;
}
}
}
return num;
}
4.胜利条件更新:
细心的小伙伴们应该已经发现了,在我们进行清除展开时,在函数中同时处理了大量的坐标,而在这些坐标被操作时并没有被计算,最终导致原有的胜利条件无法进行计算,于是我们就需要一个新的胜利判断函数来帮助我们进行胜利条件判断。
那么我们应该怎么进行判断呢?这里使用的方法是,在执行完每次循环中每个坐标操作后,进行胜利条件判断,死亡或胜利则破坏循环条件,终止循环,在终止循环时再对死亡或胜利进行区别处理即可:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
printf("请输入您想要排查的坐标:\n");
int x = 0;
int y = 0;
int win = 1;
while (win)
{
PlayerJudge(show, row, col);
printf("您想排查哪一行:");
scanf("%d", &x);
if (x >= 1 && x <= row)
{
while (1)
{
printf("您想排查哪一列:");
scanf("%d", &y);
if (y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标已被排查过!\n");
DisplayBoard(show, ROW, COL);
break;
}
if (mine[x][y] == '1')
{
DisplayBoard(mine, ROW, COL);
printf("很遗憾,您死了!\n");
win = 0;
break;
}
else
{
int n = get_mine_count(mine, x, y);
show[x][y] = n + '0';
Cls(mine, show, row, col, x, y, &win);
int res = LeaveMine(show, row, col);
printf("剩余雷量:%d\n", EASY_COUNT - res);
Really_win(show, row, col, &win);
DisplayBoard(show, ROW, COL);
break;
}
}
else
{
printf("您输入的列数有误,请重新输入!\n");
}
}
}
else
{
printf("您输入的行数有误,请重新输入!\n");
}
}
}
我们对这个胜利判断函数进行定义,当我们排出的“地雷”的数量等于设置的“地雷”数量时,且没有空余格子时,破坏判断条件:
void Really_win(char show[ROWS][COLS], int row, int col, int* win)
{
int count1 = 0;
int count2 = 0;
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (show[i][j] == '#')
{
count1++;
}
if (show[i][j] == '*')
{
count2++;
}
}
}
if (count1 == EASY_COUNT && count2 == 0)
{
*win = 0;
}
}
接着我们最后再添加一个变量really_win,并在判断为胜利后改变其取值,再根据其取值来区别于游戏失败,反馈玩家胜利。
定义并初始化变量really_win:
int really_win = 0;
传入函数进行操作:
Really_win(show, row, col, &win, &really_win);
函数进行操作:
void Really_win(char show[ROWS][COLS], int row, int col, int* win, int* really_win)
{
int count1 = 0;
int count2 = 0;
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (show[i][j] == '#')
{
count1++;
}
if (show[i][j] == '*')
{
count2++;
}
}
}
if (count1 == EASY_COUNT && count2 == 0)
{
*win = 0;
*really_win = 1;
}
}
最后在排雷步骤最后根据really_win的取值反馈给玩家:
if (really_win == 1)
{
printf("恭喜您,扫雷成功!\n");
}
将其组合起来即为完整的功能实现:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
printf("请输入您想要排查的坐标:\n");
int x = 0;
int y = 0;
int win = 1;
int really_win = 0;
while (win)
{
PlayerJudge(show, row, col);
printf("您想排查哪一行:");
scanf("%d", &x);
if (x >= 1 && x <= row)
{
while (1)
{
printf("您想排查哪一列:");
scanf("%d", &y);
if (y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标已被排查过!\n");
DisplayBoard(show, ROW, COL);
break;
}
if (mine[x][y] == '1')
{
DisplayBoard(mine, ROW, COL);
printf("很遗憾,您死了!\n");
win = 0;
break;
}
else
{
int n = get_mine_count(mine, x, y);
show[x][y] = n + '0';
Cls(mine, show, row, col, x, y, &win);
int res = LeaveMine(show, row, col);
printf("剩余雷量:%d\n", EASY_COUNT - res);
Really_win(show, row, col, &win, &really_win);
DisplayBoard(show, ROW, COL);
break;
}
}
else
{
printf("您输入的列数有误,请重新输入!\n");
}
}
}
else
{
printf("您输入的行数有误,请重新输入!\n");
}
}
if (really_win == 1)
{
printf("恭喜您,扫雷成功!\n");
}
}
二、界面优化:
同井字棋的界面优化一样,我们也使用同样的方式对扫雷游戏进行优化,使用的函数、头文件均与井字棋游戏的界面优化完全一致,这里便不再作过多阐述。
三、终极优化后最终全部代码:
1.game.h:
#pragma once
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS ROW+2
#define EASY_COUNT 10
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<Windows.h>
void menu();
void game();
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][ROWS], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
int get_mine_count(char mine[ROWS][COLS], int x, int y);
void Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* win);
void Judge(char show[ROWS][COLS], int row, int col);
void PlayerJudge(char show[ROWS][COLS], int row, int col);
int LeaveMine(char show[ROWS][COLS], int row, int col);
void Really_win(char show[ROWS][COLS], int row, int col, int* win, int* really_win);
2.game.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//打印菜单:
void menu()
{
printf("******************************\n");
printf("******************************\n");
printf("***** 欢迎来到扫雷游戏 *****\n");
printf("***** 1.play *****\n");
printf("***** 0.exit *****\n");
printf("******************************\n");
printf("******************************\n");
}
//初始化棋盘:
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
//打印棋盘:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf(" --------扫雷--------\n");
int i = 0;
printf("| \\ 1 2 3 4 5 6 7 8 9 |\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("| %d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("|\n");
}
printf(" --------扫雷--------\n");
}
//布置雷:
void SetMine(char mine[ROWS][ROWS], 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--;
}
}
}
//统计雷:
int get_mine_count(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 Cls(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* win)
{
if (show[x][y] == '0')
//若此坐标内元素为'0',则进入此函数
{
show[x][y] = ' ';
int i = x - 1;
for (; i <= x + 1; i++)
{
int j = y - 1;
for (; j <= y + 1; j++)
{
if (show[i][j] == ' ')
{
continue;
}
if (i >= 1 && i <= row && j >= 1 && j <= col)
//检查坐标越界
{
if (show[i][j] != '*')
//如果该元素不是'*',即已经被赋值,则无需进行下面的操作
{
continue;
}
int ret = get_mine_count(mine, i, j);
show[i][j] = '0' + ret;
*win++;
Cls(mine, show, row, col, i, j, &win);
//不断迭代,直到不再出现0或数字为止
}
}
}
}
}
//标记坐标:
void Judge(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
int a = 0;
int b = 0;
printf("请输入你想标记的坐标行号:");
scanf("%d", &a);
printf("请输入你想标记的坐标列号:");
scanf("%d", &b);
if (a >= 1 && a <= row && b >= 1 && b <= col)
{
x = a;
y = b;
Sleep(500);
break;
}
else
{
system("cls");
printf("您选择的坐标有误,请重新选择!\n");
Sleep(1000);
system("cls");
DisplayBoard(show, row, col);
}
}
while (1)
{
system("cls");
DisplayBoard(show, row, col);
printf("1 - 不确定是否为雷\n");
printf("2 - 确定是雷\n");
printf("3 - 取消标记\n");
printf("4 - 取消操作\n");
printf("请输入你想对该坐标进行的操作:");
int c = 0;
scanf("%d", &c);
Sleep(500);
system("cls");
if (show[x][y] == '*')
{
if (c == 1)
{
c = 63;
//符号?对应的ASCII码值为63
}
if (c == 2)
{
c = 35;
//符号#对应的ASCII码值为35
}
if (c == 4)
{
printf("操作已取消,请重新选择!\n");
DisplayBoard(show, row, col);
break;
}
char ch = c;
show[x][y] = ch;
printf("标记成功!\n");
DisplayBoard(show, row, col);
break;
}
else if (show[x][y] == '?' || show[x][y] == '#' && c == 3)
//如果输入3还要判断该元素是否已经被标记
{
show[x][y] = '*';
printf("取消成功!\n");
DisplayBoard(show, row, col);
break;
}
else if (c == 4)
{
printf("操作已取消,请重新选择!\n");
Sleep(1000);
system("cls");
DisplayBoard(show, row, col);
break;
}
else
{
printf("输入位置不合法,请重新输入\n");
}
}
}
//标记确认:
void PlayerJudge(char show[ROWS][COLS], int row, int col)
{
int m = 0;
do
{
printf("请问您是否想要标记某位置?\n");
printf("是:1 否:0:");
scanf("%d", &m);
switch (m)
{
case 1:
Sleep(500);
system("cls");
DisplayBoard(show, ROW, COL);
Judge(show, row, col);
break;
case 0:
Sleep(1000);
system("cls");
DisplayBoard(show, ROW, COL);
break;
default:
system("cls");
printf("输入有误,请重新输入:\n");
Sleep(1000);
system("cls");
DisplayBoard(show, ROW, COL);
break;
}
} while (m);
}
//剩余雷量计算:
int LeaveMine(char show[ROWS][COLS], int row, int col)
{
int i = 1;
int num = 0;
for (i = 1; i <= row; i++)
{
int j = 1;
for (j = 1; j <= col; j++)
{
if (show[i][j] == '#')
{
num++;
}
}
}
return num;
}
//胜利判定:
void Really_win(char show[ROWS][COLS], int row, int col, int* win, int* really_win)
{
int count1 = 0;
int count2 = 0;
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (show[i][j] == '#')
{
count1++;
}
if (show[i][j] == '*')
{
count2++;
}
}
}
if (count1 == EASY_COUNT && count2 == 0)
{
*win = 0;
*really_win = 1;
}
}
//排雷:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
printf("请输入您想要排查的坐标:\n");
int x = 0;
int y = 0;
int win = 1;
int really_win = 0;
while (win)
{
PlayerJudge(show, row, col);
printf("您想排查哪一行:");
scanf("%d", &x);
if (x >= 1 && x <= row)
{
while (1)
{
printf("您想排查哪一列:");
scanf("%d", &y);
if (y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
system("cls");
printf("该坐标已被排查过!\n");
Sleep(1000);
system("cls");
DisplayBoard(show, ROW, COL);
break;
}
if (mine[x][y] == '1')
{
system("cls");
DisplayBoard(mine, ROW, COL);
printf("很遗憾,您死了!\n");
win = 0;
Sleep(1000);
system("cls");
break;
}
else
{
int n = get_mine_count(mine, x, y);
show[x][y] = n + '0';
Cls(mine, show, row, col, x, y, &win);
int res = LeaveMine(show, row, col);
printf("剩余雷量:%d\n", EASY_COUNT - res);
Sleep(1000);
system("cls");
Really_win(show, row, col, &win, &really_win);
DisplayBoard(show, ROW, COL);
break;
}
}
else
{
printf("您输入的列数有误,请重新输入!\n");
}
}
}
else
{
printf("您输入的行数有误,请重新输入!\n");
}
}
if (really_win == 1)
{
system("cls");
printf("恭喜您,扫雷成功!\n");
Sleep(1000);
system("cls");
}
}
3.test.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//游戏逻辑:
void game()
{
char mine[ROWS][COLS] = { 0 };
//存放布置的雷的信息
char show[ROWS][COLS] = { 0 };
//存放查出的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
//扫雷
FindMine(mine, show, ROW, COL);
}
void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
Sleep(500);
system("cls");
game();
break;
case 0:
printf("退出游戏...\n");
Sleep(1000);
break;
default:
system("cls");
printf("输入错误,请重新选择!\n");
Sleep(1000);
system("cls");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
四、总结:
当我们完成以上优化后,一个扫雷游戏程序的雏形就基本完成了,我们对其功能和界面都进行了相当程度的优化。但是对于一个程序的全部优化过程来说,可远远不止如此,在以后的学习过程中,我们会学到更多知识,到时候我们还可以使用更高深的知识来对我们的游戏程序进行更深层次的优化。
到这里我们对扫雷游戏的优化就宣告结束啦,能熟练灵活的掌握、运用我们学过的知识,才是我们的最终目的。懒惰不会让你一下子跌倒,但会在不知不觉中减少你的收获;勤奋也不会让你一夜成功,但会在不知不觉中积累你的成果!
新人初来乍到,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 最后,本文仍有许多不足之处,欢迎各位看官老爷随时私信批评指正!