当前位置:   article > 正文

【用C语言轻松实现】- 扫雷【超详细教程】_扫雷怎么生成才能确保通过

扫雷怎么生成才能确保通过

一、游戏规则

扫雷就是要将所有非地雷的格子揭开即胜利,踩到地雷格子就算失败。游戏是由很多个方格组成,用鼠标随机点击一个方格,若格子被打开后没有地雷,即方格会被打开并显示出其周围 8 个方格隐藏地雷的个数。所以若想顺利过关就需要利用好这些显示出来的数字提示。


 二、文件的创建

  1. test.c(实现游戏的整体思路,用于测试扫雷游戏的逻辑
  2. game.c(游戏的实现
  3. game.h声明 .c 文件的函数,包含所有需要用到的头文件以及 define 定义的常量

 三、初始页面的实现 

1、实现主菜单页面

// test.c

(1)menu()
  1. void menu()
  2. {
  3. printf("***********************************\n");
  4. printf("********** 1.paly **********\n");
  5. printf("********** 0.exit **********\n");
  6. printf("***********************************\n");
  7. }

 效果图如下:

(2)test(): 
  1. void test()
  2. {
  3. int input = 0;
  4. srand((unsigned int)time(NULL));
  5. do
  6. {
  7. menu();// 主菜单
  8. printf("请选择:>");
  9. scanf("%d", &input);// 玩家选择
  10. switch (input)// 判断玩家是否进行游戏以及是否输入合法选项
  11. {
  12. case 1:
  13. game();// 游戏
  14. break;
  15. case 0:
  16. printf("退出游戏\n");
  17. break;
  18. default:
  19. printf("选择错误,请重新选择!\n");
  20. break;
  21. }
  22. } while (input);
  23. }

:这里使用 do while() 循环可以保证至少会进行一次选择。如果选择 0,会被判定为假,则跳出循环,退出游戏;如果输入选择则不会退出游戏,可以1保证游戏能够重复游玩。

srand((unsigned int)time(NULL))  利用时间戳形成随机数,主要目的是实现游戏中地雷的随机埋放。

(3)main(): 
  1. int main()
  2. {
  3. test();
  4. return 0;
  5. }

 2、在 game.h 中引用所需要的文件和必要的定义

// game.h

  1. #pragma once
  2. // 9*9
  3. #define ROW 9// 行
  4. #define COL 9// 列
  5. // 11*11
  6. #define ROWS ROW+2
  7. #define COLS COL+2
  8. #define EASY_COUNT 10// 地雷的数量
  9. #include <stdio.h>
  10. #include <stdlib.h>
  11. #include <time.h>

 四、棋盘函数的声明、创建和实现(打印棋盘函数同理)

1、声明棋盘的函数(附加打印棋盘函数)

// game.h

  1. //初始化排雷界面
  2. void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
  3. //打印棋盘
  4. void DisplayBoard(char board[ROWS][COLS], int row, int col);

 2、棋盘函数的创建 (附加打印功能)

// test.c

  1. void game()
  2. {
  3. // 扫雷游戏的实现
  4. char mine[ROWS][COLS] = { 0 };// 布置地雷的信息 // '0'
  5. char show[ROWS][COLS] = { 0 };// 排查地雷的信息 // '*'
  6. // 初始化棋盘
  7. InitBoard(mine, ROWS, COLS, '0');
  8. InitBoard(show, ROWS, COLS, '*');
  9. // 打印棋盘
  10. DisplayBoard(mine, ROW, COL);
  11. DisplayBoard(show, ROW, COL);
  12. }

char mine[ROWS][COLS]        '1' -- 雷        '0' -- 非雷
char show[ROWS][COLS]        '*‘ -- 未排查        数组字符 -- 已排查


:初始化两个棋盘,一个为 mine,一个为 show,其中在 mine 数组中我们定义有雷为 '1',没有雷为 '0'。由扫雷规则可知:当我们输入一个坐标时,会在该坐标上出现一个数字,表示在该坐标周围 8 个格子有多少个地雷,但是当四周只有一个雷的时候所出现的 ‘1’ 不是会与有雷的 '1' 混淆吗?所以我们创建了另外一个数组 show,show 数组中不表示雷,只打印数值,这样就解决了 '1' 的混淆问题。为什么要用 '1' 来代替地雷呢?后面会有介绍到。


首先对埋雷的数组进行初始化为字符 '0'(非雷用字符 '0' 表示,地雷用字符 '1' 表示),对显示棋盘的数组全部用 '*',将埋雷的数组覆盖起来,因此在 game() 函数里对 InitBorad() 函数调用两次


1、为什么要创建两个相同的数组呢?

答:因为我们在玩游戏时首先在屏幕上看到的是一个初始化的棋盘,并不清楚哪里埋放着地雷。如果我们触碰到地雷结束游戏,需要给玩家呈现这局游戏所有地雷位置的情况,这样以便于玩家清楚所有雷都在哪个位置,因此我们在屏幕上显示的是 9*9 的数组,但是创建的时候是 11*11 的数组,所以需要两个棋盘才能完成这项任务。其中一个隐藏在暗处用来存放这些地雷、数字的信息,玩家是看不见的;另一个则是专门向玩家展示的棋盘。


2、显示 9*9 大小的棋盘,为什么要创建 11*11 大小的棋盘

答:这是因为在排雷的过程中,会显示其周围 8 个格子地雷的个数。如果创建一个 9*9 的棋盘,那当我们选择边角方格的时候,我们周围的格子不足 8 个,但是我们还要访问周围 8 个格子,这时候必然会造成数组越界的问题。如果创建 11*11 的棋盘,对 11*11 的棋盘都初始化为字符 '0', 我们只显示内部的 9*9 的格子,这样就解决了数组越界的问题,游戏正常运行。 

3、棋盘函数的实现(附加打印棋盘函数的实现)

// game.c

  1. void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
  2. {
  3. int i = 0;
  4. int j = 0;
  5. for (i = 0; i < rows; i++)
  6. {
  7. for (j = 0; j < cols; j++)
  8. {
  9. board[i][j] = set;
  10. }
  11. }
  12. }
  13. void DisplayBoard(char board[ROWS][COLS], int row, int col)
  14. {
  15. int i = 0;
  16. int j = 0;
  17. for (i = 0; i <= col; i++)
  18. {
  19. printf("%d ", i);
  20. }
  21. printf("\n");
  22. for (i = 1; i <= row; i++)
  23. {
  24. printf("%d ", i);
  25. for (j = 1; j <= col; j++)
  26. {
  27. printf("%c ", board[i][j]);// 要用%c打印 否则打印结果会出现问题
  28. }
  29. printf("\n");
  30. }
  31. }

:因为游戏要重复进行,所以每次都要先对数组进行初始化,防止因上次游戏的干扰。


我们对第一块 mine 棋盘全部初始化为字符 '0' ,对第二块 show 棋盘全部初始化为字符 '*' ,最终玩家会先看到一个全是字符 '*' 的棋盘,这样也符合游戏规则。

打印效果如下:(只需要将show棋盘展示给游戏用户)

(1)布置棋盘(mine):


(2)展示棋盘(show): 


 五、布置地雷

// game.h

  1. // 布置地雷
  2. void SetMine(char board[ROWS][COLS], int row, int col);

// game.c

  1. void SetMine(char board[ROWS][COLS], int row, int col)
  2. {
  3. int count = EASY_COUNT;// 在 9*9 棋盘里随机布置10个雷
  4. while (count)
  5. {
  6. int x = rand() % row + 1;// 1~9
  7. int y = rand() % col + 1;// 1~9
  8. if (board[x][y] == '0')// 如果没有雷就布置雷
  9. {
  10. board[x][y] = '1';
  11. count--;// 布置完一个雷,雷数-1
  12. }
  13. }
  14. }
如何实现随机布雷?

答:创建 rand 函数,利用时间戳生成随机数。因为我们创建的棋盘大小是 9*9 大小的,所以只需要给生成的随机数模上 row/col ,即可得到 0~row-1,因此我们+1即可得到一个在 0~row(col) 的随机数,从而实现随机布雷的效果。一共设定 count 个雷,如果我们埋下一颗雷,就 count -1,同时将初始化的字符 '0' 变成字符 '1',只有我们识别到目标方格是字符 '0' 时才会埋雷,这也解决了在同一位置重复埋雷的问题。在使用 while() 循环,直到减到 count 为 0 时,while 不再循环,count 颗雷正好布置完成。


 六、排查地雷

// game.h

  1. // 排查地雷
  2. void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

:为什么 FindMine 传的参数是 ROW COL 而不是 ROWS COLS ?原因:首先要明白,要打印 9*9 的棋盘,如果定义棋盘大小只有 9*9,那么当我们检查边界坐标的周围时就会出现越界访问的问题。所以排雷时就只需要将 ROW COL 传过去就好了。

//game.c

  1. int get_mine_count(char mine[ROWS][COLS], int x, int y)
  2. {
  3. return
  4. mine[x - 1][y] +
  5. mine[x - 1][y - 1] +
  6. mine[x][y - 1] +
  7. mine[x + 1][y - 1] +
  8. mine[x + 1][y] +
  9. mine[x + 1][y + 1] +
  10. mine[x][y + 1] +
  11. mine[x - 1][y + 1] - 8 * '0';
  12. }
  13. void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
  14. {
  15. int x = 0;
  16. int y = 0;
  17. int win = 0; //9*9-10=71 //判断雷是否都被排查干净
  18. while (win < row * col - EASY_COUNT)
  19. {
  20. printf("请输入排查雷的坐标:>");
  21. scanf("%d%d", &x, &y);
  22. if (x >= 1 && x <= row && y >= 1 && y <= col)
  23. {
  24. //坐标合法
  25. if (mine[x][y] == '1')
  26. {
  27. printf("很遗憾,你被炸死了\n");//踩雷
  28. DisplayBoard(mine, row, col);
  29. break;
  30. }
  31. else//不是雷
  32. {
  33. //计算x,y坐标周围有几个雷
  34. int count = get_mine_count(mine, x, y);
  35. show[x][y] = count + '0';
  36. //必须加上'0'(实际上是 count 返回的整型值加上0的ASCII值后,再转化为字符)
  37. DisplayBoard(show, row, col);
  38. win++;
  39. }
  40. }
  41. else
  42. {
  43. printf("输入坐标非法,请重新输入!\n");
  44. }
  45. }
  46. if (win == row * col - EASY_COUNT)
  47. {
  48. printf("恭喜你,排雷成功\n");
  49. DisplayBoard(mine, row, col);
  50. }
  51. }

:将周围 8 个格子的字符全部加起来,如果是字符 '0' 和字符 '1' 所对应的 ASCII 码值相差1,我们可以通过 ASCII 码值来进行判断。将周围 8 个字符相加,然后再减去8个字符 '0' 的和,就可以得到周围 8 个格子中有多少个字符 '1' 的格子,也就是雷的个数。


当我们输入正确的坐标时,我们需要对这个坐标下所对应的字符进行判断,如果是字符 '1' ,说明踩中地雷,游戏结束,这时再将完整的棋盘打印出来,玩家也可以了解本局游戏的情况。如果是字符 '0',说明玩家没有踩中地雷,根据游戏规则,我们需要显示这个格子周围 8个格子中存在地雷的个数,所以调用了get_mine_count函数。


 七、代码整合

1、// test.c

  1. #include "game.h"
  2. void menu()
  3. {
  4. printf("***********************************\n");
  5. printf("********** 1.paly **********\n");
  6. printf("********** 0.exit **********\n");
  7. printf("***********************************\n");
  8. }
  9. void game()
  10. {
  11. //布置雷的信息
  12. char mine[ROWS][COLS] = { 0 };// '0'
  13. //排查出雷的信息
  14. char show[ROWS][COLS] = { 0 };// '*'
  15. //两个棋盘的初始化
  16. InitBoard(mine, ROWS, COLS, '0');
  17. InitBoard(show, ROWS, COLS, '*');
  18. //棋盘的打印
  19. //DisplayBoard(mine, ROW, COL);
  20. DisplayBoard(show, ROW, COL);
  21. //布置雷
  22. SetMine(mine, ROW, COL);
  23. //排查地雷
  24. FindMine(mine, show, ROW, COL);
  25. }
  26. void test()
  27. {
  28. int input = 0;
  29. srand((unsigned int)time(NULL));
  30. do
  31. {
  32. menu();// 主菜单
  33. printf("请选择:> ");
  34. scanf("%d", &input);// 玩家选择
  35. switch (input)// 判断玩家是否进行游戏以及是否输入合法选项
  36. {
  37. case 1:
  38. game();// 游戏
  39. break;
  40. case 0:
  41. printf("退出游戏\n");
  42. break;
  43. default:
  44. printf("选择错误,请重新选择!\n");
  45. break;
  46. }
  47. } while (input);
  48. }
  49. int main()
  50. {
  51. test();
  52. return 0;
  53. }

2、// game.c

  1. #include "game.h"
  2. void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
  3. {
  4. int i = 0;
  5. int j = 0;
  6. for (i = 0; i < rows; i++)
  7. {
  8. for (j = 0; j < cols; j++)
  9. {
  10. board[i][j] = set;
  11. }
  12. }
  13. }
  14. void DisplayBoard(char board[ROWS][COLS], int row, int col)
  15. {
  16. int i = 0;
  17. int j = 0;
  18. printf("--------扫雷--------\n");
  19. for (i = 0; i <= col; i++)
  20. {
  21. printf("%d ", i);
  22. }
  23. printf("\n");
  24. for (i = 1; i <= row; i++)
  25. {
  26. printf("%d ", i);
  27. for (j = 1; j <= col; j++)
  28. {
  29. printf("%c ", board[i][j]);// 要用%c打印 否则打印结果会出现问题
  30. }
  31. printf("\n");
  32. }
  33. printf("--------扫雷--------\n");
  34. }
  35. void DisplayBoard(char board[ROWS][COLS], int row, int col)
  36. {
  37. printf(" ");// 考虑到y轴占3格
  38. for (int j = 1; j <= col; j++)// 打印x轴坐标
  39. {
  40. printf(" %d ", j);
  41. }
  42. printf("\n");
  43. printf(" |");
  44. for (int j = 0; j < col; j++)// 打印棋盘封顶
  45. {
  46. printf("---|");
  47. }
  48. printf("\n");
  49. for (int i = 1; i <= row; i++)
  50. {
  51. for (int j = 0; j <= col; j++)
  52. {
  53. if (j == 0)
  54. {
  55. printf("%2d|", i);// 顺带打印y轴坐标
  56. }
  57. else
  58. printf(" %c |", board[i][j]);// 打印数据
  59. }
  60. printf("\n");
  61. for (int j = 1; j <= col+1; j++)// 注意这里col应该+1
  62. {
  63. if (j == 1)
  64. printf(" |");
  65. else
  66. printf("---|");
  67. }
  68. printf("\n");
  69. }
  70. }
  71. void SetMine(char board[ROWS][COLS], int row, int col)
  72. {
  73. int count = EASY_COUNT;// 在 9*9 棋盘里随机布置10个雷
  74. while (count)
  75. {
  76. int x = rand() % row + 1;// 1~9
  77. int y = rand() % col + 1;// 1~9
  78. if (board[x][y] == '0')// 如果没有雷就布置雷
  79. {
  80. board[x][y] = '1';
  81. count--;// 布置完一个雷,雷数-1
  82. }
  83. }
  84. }
  85. int get_mine_count(char mine[ROWS][COLS], int x, int y)
  86. {
  87. return
  88. mine[x - 1][y] +
  89. mine[x - 1][y - 1] +
  90. mine[x][y - 1] +
  91. mine[x + 1][y - 1] +
  92. mine[x + 1][y] +
  93. mine[x + 1][y + 1] +
  94. mine[x][y + 1] +
  95. mine[x - 1][y + 1] - 8 * '0';
  96. }
  97. 炸金花式展开函数
  98. //void explode_spread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
  99. //{
  100. // // 限制非法坐标的展开
  101. // if (x >= 1 && x <= row && y >= 1 && y <= col)
  102. // {
  103. // // 计算该位置附近四周地雷的个数
  104. // int count = get_mine_count(mine, x, y);
  105. // // 若四周没有一个地雷,则需要向该位置的四周展开,直到展开到某个位置附近存在地雷为止
  106. // if (count == 0)
  107. // {
  108. // // 把附近没有地雷的位置变成字符' '
  109. // show[x][y] = '0';
  110. // int i = 0;
  111. // // 向四周共8个位置递归调用
  112. // for (i = x - 1; i <= x + 1; i++)
  113. // {
  114. // int j = 0;
  115. // for (j = y - 1; j <= y + 1; j++)
  116. // {
  117. // // 限制对点位置的重复展开调用,使得每一个位置只能向四周展开一次
  118. // if (show[i][j] == '*')
  119. // {
  120. // explode_spread(mine, show, row, col, i, j);
  121. // }
  122. // }
  123. // }
  124. // }
  125. // // 若四周存在地雷则应该在这个位置上标注上地雷的个数
  126. // else
  127. // {
  128. // show[x][y] = count + '0';
  129. // }
  130. // }
  131. //}
  132. void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
  133. {
  134. int x = 0;
  135. int y = 0;
  136. int win = 0; //9*9-10=71 //判断雷是否都被排查干净
  137. while (win < row * col - EASY_COUNT)
  138. {
  139. printf("请输入排查雷的坐标:>");
  140. scanf("%d%d", &x, &y);
  141. if (x >= 1 && x <= row && y >= 1 && y <= col)
  142. {
  143. //坐标合法
  144. if (mine[x][y] == '1')
  145. {
  146. printf("很遗憾,你被炸死了\n");//踩雷
  147. DisplayBoard(mine, row, col);
  148. break;
  149. }
  150. else//不是雷
  151. {
  152. //explode_spread(mine, show, row, col, x, y);
  153. //计算x,y坐标周围有几个雷
  154. int count = get_mine_count(mine, x, y);
  155. show[x][y] = count + '0';
  156. //必须加上'0'(实际上是 count 返回的整型值加上0的ASCII值后,再转化为字符)
  157. DisplayBoard(show, row, col);
  158. win++;
  159. }
  160. }
  161. else
  162. {
  163. printf("输入坐标非法,请重新输入!\n");
  164. }
  165. }
  166. if (win == row * col - EASY_COUNT)
  167. {
  168. printf("恭喜你,排雷成功\n");
  169. DisplayBoard(mine, row, col);
  170. }
  171. }

3、//game.h

  1. #pragma once
  2. // 9*9
  3. #define ROW 9// 行
  4. #define COL 9// 列
  5. // 11*11
  6. #define ROWS ROW+2
  7. #define COLS COL+2
  8. #define EASY_COUNT 10// 地雷的数量
  9. #include <stdio.h>
  10. #include <stdlib.h>
  11. #include <time.h>
  12. // 初始化排雷界面
  13. void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
  14. // 打印棋盘
  15. void DisplayBoard(char board[ROWS][COLS], int row, int col);
  16. // 布置地雷
  17. void SetMine(char board[ROWS][COLS], int row, int col);
  18. // 排查地雷
  19. void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

 八、程序展示

运行效果图:


 九、改进建议

1、棋盘优化

  1. void DisplayBoard(char board[ROWS][COLS], int row, int col)
  2. {
  3. printf(" ");// 考虑到y轴占3格
  4. for (int j = 1; j <= col; j++)// 打印x轴坐标
  5. {
  6. printf(" %d ", j);
  7. }
  8. printf("\n");
  9. printf(" |");
  10. for (int j = 0; j < col; j++)// 打印棋盘封顶
  11. {
  12. printf("---|");
  13. }
  14. printf("\n");
  15. for (int i = 1; i <= row; i++)
  16. {
  17. for (int j = 0; j <= col; j++)
  18. {
  19. if (j == 0)
  20. {
  21. printf("%2d|", i);// 顺带打印y轴坐标
  22. }
  23. else
  24. printf(" %c |", board[i][j]);// 打印数据
  25. }
  26. printf("\n");
  27. for (int j = 1; j <= col+1; j++)// 注意这里col应该+1
  28. {
  29. if (j == 1)
  30. printf(" |");
  31. else
  32. printf("---|");
  33. }
  34. printf("\n");
  35. }
  36. }

运行效果图如下:(增加了棋盘边界线)

2、炸金花式展开函数 

这是一个递归函数,该函数实现的功能为:如果排查的位置周围没有雷,则向四周爆炸式展开,直至遇到周围有地雷的坐标时才停下来。那该怎么实现该递归函数呢?当排查的位置没有雷且该位置周围没有雷时,就展开其周围一圈的 8 个坐标,然后看这 8 个坐标是否可以再逐个向其自身周围接着展开,这样一次就递归调用函数自身 8 次的展开速度就像爆炸了一样,所以称其为:炸金花式展开深度优先遍历)。

 

 :会发现不管先展开哪一个位置都将陷入死递归当中。当要展开 (x,y) 坐标周围的一圈坐标时,假如其首先会从 (x-1,y+1) 位置开始,然后又由该坐标向其自身周围的一圈展开时,你会发现坐标 (x,y) 也在需要展开的范围内。这样不就会重复要求再次展开坐标(x,y),然后再由 (x,y) 要求展开(x-1,y+1),两个坐标相互疯狂调用。若每次判断一个坐标上没有雷且该坐标周围一圈同样没有雷时,则将向我们展示的棋盘上表示未知的字符 '*' 改成 ' '。然后在之后每次递归调用前给一个判断:如果这个将要被递归调用的坐标在展示的棋盘上存放的是字符 '*' 时才能进行下一步,否则将直接跳过此次递归。


如若发生向外扩张的递归,那拓展出来最外围的一圈坐标完全无法起到限制的作用,无法阻止炸金花式的向外展开。因此只能在每次进入递归函数后加一条限制

  1. //炸金花式展开函数
  2. void explode_spread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
  3. {
  4. // 限制非法坐标的展开
  5. if (x >= 1 && x <= row && y >= 1 && y <= col)
  6. {
  7. // 计算该位置附近四周地雷的个数
  8. int count = get_mine_count(mine, x, y);
  9. // 若四周没有一个地雷,则需要向该位置的四周展开,直到展开到某个位置附近存在地雷为止
  10. if (count == 0)
  11. {
  12. // 把附近没有地雷的位置变成字符' '
  13. show[x][y] = ' ';
  14. int i = 0;
  15. // 向四周共8个位置递归调用
  16. for (i = x - 1; i <= x + 1; i++)
  17. {
  18. int j = 0;
  19. for (j = y - 1; j <= y + 1; j++)
  20. {
  21. // 限制对点位置的重复展开调用,使得每一个位置只能向四周展开一次
  22. if (show[i][j] == '*')
  23. {
  24. explode_spread(mine, show, row, col, i, j);
  25. }
  26. }
  27. }
  28. }
  29. // 若四周存在地雷则应该在这个位置上标注上地雷的个数
  30. else
  31. {
  32. show[x][y] = count + '0';
  33. }
  34. }
  35. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/921230
推荐阅读
相关标签
  

闽ICP备14008679号