C语言实现扫雷游戏
使用easyx实现可视化编程
1.目标功能
- 非控制台的游戏界面显示
- 基本游戏规则
扫雷规则
- 区分探测未探测格子的颜色
- 插旗
- 实时显示游戏中未探测到雷的个数
- 用表情包显示出目前扫雷状态(有扫雷中,成功,失败三种表情)
- 保证玩家第一次点击时不会被雷炸到
- 一点一大片(当一个格子周围没有雷时,该格子周围的格子会被自动探测,该格子周围的空白格会引发连锁反应)
- 计时器
- 每轮游戏结束后可以自己选择退出或重开
2.游戏实现
核心思路:玩家操作会改变数组数值,利用数组的数值变化,对不同数值的格子进行贴图,从而控制格子呈现不同状态
格子数值 | 对应状态及图片 | 左击对应操作和在数组中的体现 | 右击对应操作和在数组中的体现 |
---|---|---|---|
-1 | 10(雷) | ||
0-8 | 0-8(数字) | ||
19-28 | 9(空白格) | -20 翻开格子 | +20 插旗 |
>30 | 11(旗子) | -20 复原插旗处 |
(这张表格里的内容在接下来会进一步解释)
具体过程:
-
游戏初始化+随机布雷
-
定义地图行列,目前难度的雷数
-
创建数组,全部初始化为0
-
随机挑选10个位置改变数值为-1,即通过不同数值实现雷与普通格子的区分(若挑选中的位置数值已经为-1,则重新选择另一位置改变数值)
# define ROW 10 # define COL 10 # define EASYCOUNT 10 # define SIZE 40 int Mine = EASYCOUNT; int i, j, flag; int k = 0; int click = 0; int playtime = 0; clock_t start=0; clock_t end = 0; int map[ROW][COL]; IMAGE img[16]; void initMap() { Mine = EASYCOUNT; playtime = 0; k = 0;//每次重开时计时变量k都初始化为0 click = 0;//每次重开时计次变量click都初始化为0 //数组初始化 for (i = 0; i < ROW; i++) { for (j = 0; j < COL; j++) { map[i][j] = 0; } } //随机布雷 srand((unsigned)time(NULL)); int count = EASYCOUNT; while (count) { int m = rand() % ROW; int n = rand() % COL; if (map[m][n] == 0) { map[m][n] = -1; count--; } }
-
-
改变数组数值来实现不同状态无雷格子的区分(与上个代码块封装在同一函数中)
- 寻找出所有有雷格,将其所在九宫格除自身外所有格子数值+1,使得无雷的格子的数值更改为周围雷数
- +20使得所有格子在初始时进入空白格状态(代表未翻开)
//显示周围格子中雷的个数
for (int i= 0; i< ROW; i++)
{
for (int j = 0; j < COL; j++)
if (map[i][j] == -1)
{
for (int x = i - 1; x <=i + 1; x++)
{
for (int y = j - 1; y <= j + 1; y++)
{
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != -1)
{
map[x][y]++;
}
}
}
}
}
//赋值,为进入格子未翻开状态做准备
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
map[i][j] += 20;
}
}
}
-
导入图片
素材展示
for (int i = 0; i <16; i++)
{
char file[50] = "";
sprintf(file, "./photo/%d.gif",i);
loadimage(&img[i], file,SIZE,SIZE);
}
- 为不同状态的格子贴图,实现可视化
void GameDraw()
{
cleardevice();
//为不同状态的格子贴图
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
if (map[i][j] == -1)
{
putimage(j * SIZE, i * SIZE, &img[10]);
}
else if (map[i][j] >= 0 && map[i][j] <= 8)
{
putimage(j * SIZE, i * SIZE, &img[map[i][j]]);
}
else if (map[i][j] >= 19 && map[i][j] <= 28)
{
putimage(j * SIZE, i * SIZE, &img[9]);
}
else if (map[i][j] > 30)
{
putimage(j * SIZE, i * SIZE, &img[11]);
}
}
}
}
- 通过鼠标点击实现玩家操作
-
翻开格子
-
插旗
-
当玩家第一次点击即踩雷时,将原来位置上的雷消去(赋值为0),再随机生成一个雷(此处别忘记更新旧雷和新雷周围格子数值)
-
设置了计次变量click来检验玩家的第一次鼠标左键点击,click初始值设为0,而每次点击使得click的值+1,由此可得当click变量改变为1时,玩家进行了第一次鼠标左键点击
效果展示
-
int MouseControl()
{
//通过鼠标点击改变数值,从而实现翻开格子,插旗等功能
if (MouseHit())
{
MOUSEMSG msg = GetMouseMsg();
int row = msg.y/ SIZE;
int col = msg.x/ SIZE;
char str[50] = "";
switch (msg.uMsg)
{
case WM_LBUTTONDOWN:
//翻开格子
if (map[row][col] > 8)
{
map[row][col] -= 20;
flag++;
k++;
click++;
//当第一次点击即踩雷时改变雷的位置
if (click == 1 && map[row][col] == -1)
{
//将原来位置的雷消去
map[row][col] = 0;
//更新原来雷周围数据
for (int x = row - 1; x <= row + 1; x++)
{
for (int y = col - 1; y <= col + 1; y++)
{
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] == 19)
{
map[row][col]++;
}
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != 19&& (x!= row || y!= col))
{
map[x][y]--;
}
}
}
//再随机布雷一次
int a = rand() % ROW ;
int b = rand() % COL ;
if ((a != row || b != col) && map[a][b] != 19)
{
map[a][b] = 19;
//更新新雷周围数据
for (int m = a - 1; m <= a + 1; m++)
{
for (int n = b - 1; n <= b + 1; n++)
{
if ((m >= 0 && m <= 9) && (n >= 0 && n <= 9) && map[m][n] != 19)
{
map[m][n]++;
}
}
}
}
}
if (k == 1)
{
start = clock();
}
openNull(row, col);
}
case WM_RBUTTONDOWN:
//未插旗的格子插旗
if (map[row][col] > 8 && map[row][col] <= 28)
{
map[row][col] += 20;
Mine--;
k++;
if (k == 1)
{
start = clock();
}
}
//已插旗的格子复原
else if (map[row][col] > 28)
{
map[row][col] -= 20;
Mine++;
}
break;
}
return map[row][col];
}
}
- 遍历数值为0的格子(数值为0表示已打开,周围雷数为0)所在九宫格将其全部打开(在数组中体现为-20操作),此时这些格子中可能会再次出现数值为0的格子,则再次调用此函数(openNull)打开其周围格子,直到上一批打开的格子中不再出现数值为0的格子,递归停止
- 注意此处特意将误插的旗子一并复原(当玩家之前错误地把旗子插在了非雷的空白格上,而该空白格恰好在此次操作中被打开)
void openNull(int row, int col);
void openNull(int row, int col)
{
//通过递归实现点开一大片功能
if (map[row][col] == 0)
{
for (int x = row - 1; x <= row + 1; x++)
{
for (int y = col - 1; y <= col + 1; y++)
{
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && (map[x][y] != 19) && (map[x][y] > 8))
{
if (map[x][y] >= 30)
{
Mine++;
}
map[x][y] -= 20;
flag++;
openNull(x, y);
}
}
}
}
}
-
调试用的数组显示
- 为了快速测试我们的代码中的各个功能是否能够正常运行,我们将数组内容打印到屏幕上来,这样可以轻松地找到所有的雷,以便测试踩雷,游戏失败或胜利等一系列操作而不用自己一遍遍玩游戏
(
传说中的开外挂行为)- 记得调试完后把这个函数屏蔽掉,这部分内容不会呈现给玩家
void show()
{
//显示数组
for (int i = 0; i< ROW;i++)
{
for (int j = 0; j < COL; j++)
{
printf("%2d ", map[i][j]);
}
putchar( '\n');
}
system("cls");
}
-
判断是否踩雷或扫雷完毕
-
如果格子点开后数值为-1(原先数值为19,点开操作-20)即为踩雷,游戏结束
-
如果插旗数+点开格子数=总的坐标数即为扫雷成功,游戏结束
-
游戏结束后提供后续选项
效果展示
void Judge() { //踩雷及后续选项提供 if (MouseControl() == -1) { for (i = 0; i < ROW; i++) { for (j = 0; j < COL; j++) { if (map[i][j] == -1) { putimage(360, 400, &img[15]); putimage(j * SIZE, i * SIZE, &img[10]); } } } FlushBatchDraw(); int isok=MessageBox(GetHWnd(), "扫雷失败,是否重新开始?","提示", MB_OKCANCEL); if (IDOK == isok) { initMap(); flag = 0; } else { exit(1); } } //获胜及后续选项提供 if (flag == ROW * COL - EASYCOUNT) { putimage(360, 400, &img[14]); FlushBatchDraw(); int isok = MessageBox(GetHWnd(), "扫雷成功,是否重新开始","提示", MB_OKCANCEL); if (IDOK == isok) { initMap(); flag = 0; } else { exit(1); } } }
-
-
游戏中剩余雷数判断
-
我们在实现插旗相关代码时已经设置了对于“Mine”这个变量的改变,每成功插旗一次雷数减1,每取消插旗一次雷数加1
if (map[row][col] > 8 && map[row][col] <= 28) { map[row][col] += 20; Mine--; k++; if (k == 1) { start = clock(); } } //已插旗的格子复原 else if (map[row][col] > 28) { map[row][col] -= 20; Mine++; }
-
注意此处要限制雷的个数不能为负数,即插旗的个数不能超过10
void mine() { settextstyle(25, 10, 0); settextcolor(BLUE); char str[50] = ""; if (Mine >= 0) { sprintf(str, "剩余雷数为% d", Mine); outtextxy(10, 410, str); } else { sprintf(str, "插旗超过上限,请减少您的旗子个数"); outtextxy(10, 410, str); } }
-
-
计时器的实现
-
设置了计时变量k用于检测玩家的第一次鼠标点击(此刻开始计时比较符合现实中的游戏场景,玩家进入程序后可能不会第一时间开始游戏,如果一进入程序就开始计时会使得玩家得到的游戏用时有所偏差,与实际不符)
-
k在玩家第一次点击鼠标左键时+1而初始值设为0
-
结合下面代码中的判断条件则可检测到玩家的第一次鼠标左键点击,此时开始计时,使用clock函数检测到开始时间
k++; if (k == 1) { start = clock(); }
-
使用clock函数实时检测现在时间,计算出游戏时间=现在时间-开始时间并输出在屏幕上
-
void timeplay(clock_t start)
{
TCHAR time_text[50];
_stprintf_s(time_text, _T("用时:%d"), 0);
outtextxy(160, 410, time_text);
if (k > 0)
{
end = clock();
playtime = (end - start) / 1000;
_stprintf_s(time_text, _T("用时:%d"), playtime);
outtextxy(160, 410, time_text);
}
}
3.加上主函数的完整代码展示
#define _CRT_SECURE_NO_WARNINGS 1
#include <graphics.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include<conio.h>
#include <windows.h>
#pragma warning(disable : 4996)
# define ROW 10
# define COL 10
# define EASYCOUNT 10
# define SIZE 40
int Mine = EASYCOUNT;
int i, j, flag;
int k = 0;
int click = 0;
int playtime = 0;
clock_t start=0;
clock_t end = 0;
int map[ROW][COL];
IMAGE img[16];
void initMap()
{
Mine = EASYCOUNT;
playtime = 0;
k = 0;
click = 0;
//数组初始化
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
map[i][j] = 0;
}
}
//随机布雷
srand((unsigned)time(NULL));
int count = EASYCOUNT;
while (count)
{
int m = rand() % ROW;
int n = rand() % COL;
if (map[m][n] == 0)
{
map[m][n] = -1;
count--;
}
}
//显示周围格子中雷的个数
for (int i= 0; i< ROW; i++)
{
for (int j = 0; j < COL; j++)
if (map[i][j] == -1)
{
for (int x = i - 1; x <=i + 1; x++)
{
for (int y = j - 1; y <= j + 1; y++)
{
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != -1)
{
map[x][y]++;
}
}
}
}
}
//图片的导入
for (int i = 0; i <16; i++)
{
char file[50] = "";
sprintf(file, "./photo/%d.gif",i);
loadimage(&img[i], file,SIZE,SIZE);
}
//赋值,为进入格子未翻开状态做准备
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
map[i][j] += 20;
}
}
}
void GameDraw()
{
cleardevice();
//为不同状态的格子贴图
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
if (map[i][j] == -1)
{
putimage(j * SIZE, i * SIZE, &img[10]);
}
else if (map[i][j] >= 0 && map[i][j] <= 8)
{
putimage(j * SIZE, i * SIZE, &img[map[i][j]]);
}
else if (map[i][j] >= 19 && map[i][j] <= 28)
{
putimage(j * SIZE, i * SIZE, &img[9]);
}
else if (map[i][j] > 30)
{
putimage(j * SIZE, i * SIZE, &img[11]);
}
}
}
}
void openNull(int row, int col);
int MouseControl()
{
//通过鼠标点击改变数值,从而实现翻开格子,插旗等功能
if (MouseHit())
{
MOUSEMSG msg = GetMouseMsg();
int row = msg.y/ SIZE;
int col = msg.x/ SIZE;
char str[50] = "";
switch (msg.uMsg)
{
case WM_LBUTTONDOWN:
//翻开格子
if (map[row][col] > 8)
{
map[row][col] -= 20;
flag++;
k++;
click++;
//当第一次点击即踩雷时改变雷的位置
if (click == 1 && map[row][col] == -1)
{
//将原来位置的雷消去
map[row][col] = 0;
//更新原来雷周围数据
for (int x = row - 1; x <= row + 1; x++)
{
for (int y = col - 1; y <= col + 1; y++)
{
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] == 19)
{
map[row][col]++;
}
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && map[x][y] != 19&& (x!= row || y!= col))
{
map[x][y]--;
}
}
}
//再随机布雷一次
int a = rand() % ROW ;
int b = rand() % COL ;
if ((a != row || b != col) && map[a][b] != 19)
{
map[a][b] = 19;
//更新新雷周围数据
for (int m = a - 1; m <= a + 1; m++)
{
for (int n = b - 1; n <= b + 1; n++)
{
if ((m >= 0 && m <= 9) && (n >= 0 && n <= 9) && map[m][n] != 19)
{
map[m][n]++;
}
}
}
}
}
if (k == 1)
{
start = clock();
}
openNull(row, col);
}
case WM_RBUTTONDOWN:
//未插旗的格子插旗
if (map[row][col] > 8 && map[row][col] <= 28)
{
map[row][col] += 20;
Mine--;
k++;
if (k == 1)
{
start = clock();
}
}
//已插旗的格子复原
else if (map[row][col] > 28)
{
map[row][col] -= 20;
Mine++;
}
break;
}
return map[row][col];
}
}
void openNull(int row, int col)
{
//通过递归实现点开一大片功能
if (map[row][col] == 0)
{
for (int x = row - 1; x <= row + 1; x++)
{
for (int y = col - 1; y <= col + 1; y++)
{
if ((x >= 0 && x <= 9) && (y >= 0 && y <= 9) && (map[x][y] != 19) && (map[x][y] > 8))
{
if (map[x][y] >= 30)
{
Mine++;
}
map[x][y] -= 20;
flag++;
openNull(x, y);
}
}
}
}
}
void show()
{
//显示数组
for (int i = 0; i< ROW;i++)
{
for (int j = 0; j < COL; j++)
{
printf("%2d ", map[i][j]);
}
putchar( '\n');
}
system("cls");
}
void Judge()
{
//踩雷及后续选项提供
if (MouseControl() == -1)
{
for (i = 0; i < ROW; i++)
{
for (j = 0; j < COL; j++)
{
if (map[i][j] == -1)
{
putimage(360, 400, &img[15]);
putimage(j * SIZE, i * SIZE, &img[10]);
}
}
}
FlushBatchDraw();
int isok=MessageBox(GetHWnd(), "扫雷失败,是否重新开始?","提示", MB_OKCANCEL);
if (IDOK == isok)
{
initMap();
flag = 0;
}
else
{
exit(1);
}
}
//获胜及后续选项提供
if (flag == ROW * COL - EASYCOUNT)
{
putimage(360, 400, &img[14]);
FlushBatchDraw();
int isok = MessageBox(GetHWnd(), "扫雷成功,是否重新开始","提示", MB_OKCANCEL);
if (IDOK == isok)
{
initMap();
flag = 0;
}
else
{
exit(1);
}
}
}
void mine()
{
settextstyle(25, 10, 0);
settextcolor(BLUE);
char str[50] = "";
if (Mine >= 0)
{
sprintf(str, "剩余雷数为% d", Mine);
outtextxy(10, 410, str);
}
else
{
sprintf(str, "插旗超过上限,请减少您的旗子个数");
outtextxy(10, 410, str);
}
}
void timeplay(clock_t start)
{
TCHAR time_text[50];
_stprintf_s(time_text, _T("用时:%d"), 0);
outtextxy(160, 410, time_text);
if (k > 0)
{
end = clock();
playtime = (end - start) / 1000;
_stprintf_s(time_text, _T("用时:%d"), playtime);
outtextxy(160, 410, time_text);
}
}
int main()
{
initgraph(ROW*SIZE, (COL+1)*SIZE, SHOWCONSOLE);
setbkcolor(WHITE);
initMap();
BeginBatchDraw();
while (1)
{
//show();
Judge();
GameDraw();
putimage(360, 400, &img[13]);
mine();
timeplay(start);
FlushBatchDraw();
}
EndBatchDraw();
getchar();
return 0;
}