【C语言】扫雷(递归展开 + 标记功能)2

简介: 【C语言】扫雷(递归展开 + 标记功能)

5.5 新手保护机制


扫雷对于新手玩家和运气比较差的玩家体验很不友好,可能第一次就踩到雷被炸死了(比如我)。

该机制用于将第一次踩雷的坐标上的雷进行转移,让玩家有良好的游戏体验~


思路


利用rand函数生成1-9的随机数,然后转移并给出提示。


对应代码:

static void change_place(char mine[ROWS][COLS], int row, int col, int x, int y)
{
  x = rand() % row + 1;//1 - 9的随机值
  y = rand() % col + 1;
  mine[x][y] == '1';//转移
  printf("第一次踩雷,可真有你的,重新选!\n");
}


5.6 标记与取消标记


在扫雷游戏中,右击鼠标可以对你认为是雷的点进行标记,并且可以取消。


我们是否可以用C语言实现一下呢?


示意图:


0e6c02523728766cd61b943c4a7af76f.png


5.6.1 标记菜单


由于显示界面使用键盘操作,无法使用鼠标。所以不妨我们采用如下思想,设计一个菜单,让玩家决定排雷,或标记,或取消标记。


对应代码:

static void flag_menu()
{
  printf("####################################\n");
  printf("######### 1.选择非雷区域  ##########\n");
  printf("######### 2.标记雷的位置  ##########\n");
  printf("######### 3.取消雷的标记  ##########\n");
  printf("####################################\n");
}


5.6.2 设置标记


思路:


将需要标记的坐标和所需参数传过去,首先需要清楚的一点是,标记 = 雷数。每标记一个位置,标记数都需要自增,这里我们选择传址调用的方式改变标记数的大小(注意:形参是实参的一份临时拷贝,所以将标记数直接传入并不能改变值)。


若坐标被排查,则需重新输入,坐标非法需要提示。


对应代码:


static void set_flag(char show[ROWS][COLS], int row, int col,int *pf)
{
  int x = 0;
  int y = 0;
  if (*pf == EASY_COUNT)
  {
    return ;//返回,停止标记
  }
  while (1)
  {
    printf("请输入标记坐标:>");
    scanf("%d %d", &x, &y);
    if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
    {
      if (show[x][y] == '*')
      {
        show[x][y] = '#';
        (*pf)++;//自增
        break;//标记完退出
      }
      else
      {
        printf("该位置已被排查,请重新输入!\n");
        continue;
      }
    }
    else
    {
      printf("坐标非法,请重新输入!\n");
      continue;
    }
  }
}


标记效果:

09b6766345da22c601e5fa9d218b41ff.png



5.6.3 取消标记


思路


与标记的参数相同,该函数需要判断是否被标记,若被标记,则取消标记改为*,并且标记数需要减少。若未标记,则退出循环,让用户重新选择是否标记,坐标非法需提示。


注:未标记后不要用cotinue,避免无法取消标记导致死循环。


对应代码:

static void cancel_flag(char show[ROWS][COLS], int row, int col, int *pf)
{
  int x = 0;
  int y = 0;
  while (1)
  {
    printf("请输入取消标记的坐标:>");
    scanf("%d %d", &x, &y);
    if (x >= 1 && y >= 1 && x <= row && y <= col)
    {
      if (show[x][y] == '#')
      {
        show[x][y] = '*';
        (*pf)--;
        break;//取消标记后退出
      }
      else
      {
        printf("该位置未标记,无法取消标记\n");
        break;//一定要break,不能用continue,否则会导致死循环
      }
    }
    else
    {
      printf("坐标非法,请重新输入!\n");
      continue;
    }
  }
}


5.7 获取周围雷的个数


排雷过程中点击某点,当该点无雷时,需要展示该点周围八个坐标的雷的个数。


思路


采用循环对排查坐标及周围八个点进行遍历即可。


对应代码:

static int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
  int i = 0;
  int count = 0;
  //x-1 ~ x+1 && y-1 ~ y+1 
  for (i = -1; i <= 1; i++)
  {
    int j = 0;
    for (j = -1; j <= 1; j++)
    {
      if (mine[x + i][y + j] == '1')
        count++;
    }
  }
  return count;
}



5.8 递归式展开一片和接收雷的个数


扫雷游戏中,当用户点击一个坐标,如果该坐标及其周围的坐标都没有雷,通过连续判断,那么雷盘就会一次性展开一片。


相比较于自己排雷71次,这样的设计也优化了玩家的体验。


示意图:

73e4fe69bb4a996f48dbf9ec1924b787.png

那么这个思想的原理是什么呢?


展开一片,无非就是对于排查坐标和周围八个坐标进行排查,若周围坐标周围均无雷,便可以循环往复,直到周围坐标已被排查,这也正是递归的思想。



另外对于周围雷数的返回值也可以在这个函数中接收,并将返回值转化为字符储存到展示的棋盘中。


注:p为需排查数:win的地址,递归展开时仍需要自增,让游戏正常结束。


对应代码:

static void boom_board(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* p)
{
  if (x >= 1 && x <= row && y >= 1 && y <= col)
  {
    int ret = get_mine_count(mine, x, y);//接收排查点周围坐标雷的个数
    if (ret == 0)
    {
      (*p)++;//自增
      show[x][y] = ' ';//置为空
      int i = 0;
      for (i = -1; i <= 1; i++)//遍历
      {
        int j = 0;
        for (j = -1; j <= 1; j++)
        {
          if (show[x + i][y + j] == '*')//未排查则递归,避免重复形成死递归
            boom_board(mine, show, row, col, x + i, y + j, p);
        }
      }
    }
    else
    {
      (*p)++;//自增
      show[x][y] = ret + '0';//转化为字符
    }
  }
}



5.9 排查雷


美名其曰是排雷,其实该过程就是一个调用展开一片,标记,取消标记等函数,以及根据结果判断是否获胜的过程。


话不多说,我们在代码中看:

void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
  int x = 0;
  int y = 0;
  int win = 0;//排查数,目前为71
  int* p = &win;
  int op = 0;//菜单选项
  int fch = 1;//判断是否为第一次排查的变量
  unsigned int flag_count = 0;//该变量为标记数,恒>0,标记数<=雷数
  int* pf = &flag_count;
  while (win < row * col - EASY_COUNT)
  {
  again:
    flag_menu();//调用菜单
    scanf("%d", &op);
    if (op == 1)
    {
      printf("请输入要排查的坐标:>");
      scanf("%d %d", &x, &y);
      if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
      {
        if (fch == 1 && mine[x][y] == '1')//第一次排查且为雷
        {
          change_place(mine, row, col, x, y);
          fch++;//自增防止多次调用换位函数
        }
        else
        {
                    if(show[x][y] == '*')
                    {
            if (mine[x][y] == '1')
            {
              system("cls");//清屏
              printf("很遗憾,你被雷炸死了!\n");
              printf("游戏结束!\n");
              show_board(mine, row, col);//复盘
              break;
            }
            else
            {
              boom_board(mine, show, row, col, x, y, p);//展开一片
              system("cls");//清屏
              show_board(show, row, col);
            }
            fch++;//自增防止多次调用换位函数
          }
                  else
                  {
                      printf("该坐标已被排查,请重新输入\n");
                  }
                }
      }
      else
      {
        printf("非法坐标,请重新输入!\n");
        continue;
      }
    }
    else if (op == 2)//标记
    {
            set_flag(show, row, col, pf);//传址
      flag_count = *pf;
      system("cls");//清屏
            if (*pf == 10)
      {
        printf("标记数和雷数相等,无法标记!\n");
      }
      show_board(show, row, col);
    }
    else if (op == 3)
    {
            cancel_flag(show, row, col, pf);//传址
      flag_count = *pf;
      system("cls");//清屏
      show_board(show, row, col);
    }
    else
    {
      printf("选择错误,请重新选择:>\n");
      goto again;//跳转到选择处
    }
  }
  if (win == row * col -EASY_COUNT)
  {
    system("cls");
    show_board(show, ROW, COL);//最后一步展示
    printf("恭喜你,扫雷成功!\n");
    printf("获得称号,排雷战士!\n");
    printf("答案揭晓:\n");
    show_board(mine, ROW, COL);//初始答案展示
  }
}



6. 完整代码


game.h

#pragma once
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define EASY_COUNT 10//雷数
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>
//初始化棋盘
void init_board(char arr[ROWS][COLS], int rows, int cols, char set);
//布置雷
void set_mine(char mine[ROWS][COLS], int row, int col);
//打印棋盘
void show_board(char arr[ROWS][COLS], int row, int col);
//排查雷
void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

game.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include"game.h"
void init_board(char arr[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++)
    {
      arr[i][j] = set;
    }
  }
}
void set_mine(char mine[ROWS][COLS], int row, int col)
{
  int count = EASY_COUNT;
  int x = 0;
  int y = 0;
  while (count)
  {
    x = rand() % row + 1;
    y = rand() % col + 1;
    if (mine[x][y] == '0')
    {
      mine[x][y] = '1';
      count--;
    }
  }
}
void show_board(char arr[ROWS][COLS], int row, int col)
{
  int i = 0;
  printf("------------扫雷------------\n");//分割线
  for (i = 0; i <= col; i++)
  {
    printf("%d ", i);//输出对应列信息
  }
  printf("\n");
  for (i = 1; i <= row; i++)
  {
    int j = 0;
    printf("%d ", i);//输出对应行信息
    for (j = 1; j <= col; j++)
    {
      printf("%c ", arr[i][j]);
    }
    printf("\n");
  }
  printf("------------扫雷------------\n");
}
static void flag_menu()
{
  printf("####################################\n");
  printf("######### 1.选择非雷区域  ##########\n");
  printf("######### 2.标记雷的位置  ##########\n");
  printf("######### 3.取消雷的标记  ##########\n");
  printf("####################################\n");
}
//标记雷的位置
static void set_flag(char show[ROWS][COLS], int row, int col,int *pf)
{
  int x = 0;
  int y = 0;
  if (*pf == EASY_COUNT)
  {
    return ;//返回,停止标记
  }
  while (1)
  {
    printf("请输入标记坐标:>");
    scanf("%d %d", &x, &y);
    if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
    {
      if (show[x][y] == '*')
      {
        show[x][y] = '#';
        (*pf)++;//自增
        break;//标记完退出
      }
      else
      {
        printf("该位置已被排查,请重新输入!\n");
        continue;
      }
    }
    else
    {
      printf("坐标非法,请重新输入!\n");
      continue;
    }
  }
}
//取消标记
static void cancel_flag(char show[ROWS][COLS], int row, int col, int *pf)
{
  int x = 0;
  int y = 0;
  while (1)
  {
    printf("请输入取消标记的坐标:>");
    scanf("%d %d", &x, &y);
    if (x >= 1 && y >= 1 && x <= row && y <= col)
    {
      if (show[x][y] == '#')
      {
        show[x][y] = '*';
        (*pf)--;
        break;//取消标记后退出
      }
      else
      {
        printf("该位置未标记,无法取消标记\n");
        break;//一定要break,不能用continue,否则会导致死循环
      }
      }
    }
    else
    {
      printf("坐标非法,请重新输入!\n");
      continue;
    }
  }
}
static void change_place(char mine[ROWS][COLS], int row, int col, int x, int y)
{
  x = rand() % row + 1;
  y = rand() % col + 1;
  mine[x][y] == '1';
  printf("第一次踩雷,可真有你的,重新选!\n");
}
static int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
  int i = 0;
  int count = 0;
  for (i = -1; i <= 1; i++)
  {
    int j = 0;
    for (j = -1; j <= 1; j++)
    {
      if (mine[x + i][y + j] == '1')
        count++;
    }
  }
  return count;
}
//爆炸展开
static void boom_board(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* p)
{
  if (x >= 1 && x <= row && y >= 1 && y <= col)
  {
    int ret = get_mine_count(mine, x, y);//接收排查点周围坐标雷的个数
    if (ret == 0)
    {
      (*p)++;//自增
      show[x][y] = ' ';//置为空
      int i = 0;
      for (i = -1; i <= 1; i++)//遍历
      {
        int j = 0;
        for (j = -1; j <= 1; j++)
        {
          if (show[x + i][y + j] == '*')//未排查则递归,避免重复形成死递归
            boom_board(mine, show, row, col, x + i, y + j, p);
        }
      }
    }
    else
    {
      (*p)++;//自增
      show[x][y] = ret + '0';//转化为字符
    }
  }
}
void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
  int x = 0;
  int y = 0;
  int win = 0;//排查数,目前为71
  int* p = &win;
  int op = 0;//菜单选项
  int fch = 1;//判断是否为第一次排查的变量
  unsigned int flag_count = 0;//该变量为标记数,恒>0,标记数<=雷数
  int* pf = &flag_count;
  while (win < row * col - EASY_COUNT)
  {
  again:
    flag_menu();//调用菜单
    scanf("%d", &op);
    if (op == 1)
    {
      printf("请输入要排查的坐标:>");
      scanf("%d %d", &x, &y);
      if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
      {
        if (fch == 1 && mine[x][y] == '1')//第一次排查且为雷
        {
          change_place(mine, row, col, x, y);
          fch++;//自增防止多次调用换位函数
        }
        else
        {
                    if(show[x][y] == '*')
                    {
            if (mine[x][y] == '1')
            {
              system("cls");//清屏
              printf("很遗憾,你被雷炸死了!\n");
              printf("游戏结束!\n");
              show_board(mine, row, col);//复盘
              break;
            }
            else
            {
              boom_board(mine, show, row, col, x, y, p);//展开一片
              system("cls");//清屏
              show_board(show, row, col);
            }
            fch++;//自增防止多次调用换位函数
          }
                  else
                  {
                      printf("该坐标已被排查,请重新输入\n");
                  }
                }
      }
      else
      {
        printf("非法坐标,请重新输入!\n");
        continue;
      }
    }
    else if (op == 2)//标记
    {
            set_flag(show, row, col, pf);//传址
      flag_count = *pf;
      system("cls");//清屏
            if (*pf == 10)
      {
        printf("标记数和雷数相等,无法标记!\n");
      }
      show_board(show, row, col);
    }
    else if (op == 3)
    {
            cancel_flag(show, row, col, pf);//传址
      flag_count = *pf;
      system("cls");//清屏
      show_board(show, row, col);
    }
    else
    {
      printf("选择错误,请重新选择:>\n");
      goto again;//跳转到选择处
    }
  }
  if (win == row * col -EASY_COUNT)
  {
    system("cls");
    show_board(show, ROW, COL);//最后一步展示
    printf("恭喜你,扫雷成功!\n");
    printf("获得称号,排雷战士!\n");
    printf("答案揭晓:\n");
    show_board(mine, ROW, COL);//初始答案展示
  }
}



test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
  printf("###################################\n");
  printf("############  1.play   ############\n");
  printf("############  0.exit   ############\n");
  printf("###################################\n");
}
void game()
{
  char mine[ROWS][COLS] = { 0 };
  char show[ROWS][COLS] = { 0 };
  //初始化棋盘
  init_board(mine, ROWS, COLS, '0');
  init_board(show, ROWS, COLS, '*');
  //布置雷
  set_mine(mine, ROW, COL);
  system("cls");
  //打印棋盘
  show_board(show, ROW, COL);
  //排查雷
  fine_mine(mine, show, ROW, COL);
}
int main()
{
  int input = 0;
  srand((unsigned int)time(NULL));
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case 1:
      game();
      break;
    case 0:
      printf("退出游戏\n");
      break;
    default:
      printf("选择错误,请重新选择:>\n");
      break;
    }
  } while (input);
  return 0;
}



7. 动画展示



ea665b86880f6504e4c35c9bbfc887ef.gif




8. 结语

到这里,一个还原度较高的扫雷游戏就实现成功了!

以上就是C语言实现扫雷的全部内容,如果觉得anduin写的还不错的话,还请一键三连!

我是anduin,一名C语言初学者,我们下期见!





相关文章
|
3月前
|
C语言
扫雷游戏(用C语言实现)
扫雷游戏(用C语言实现)
133 0
|
3月前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
80 7
|
3月前
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
51 2
|
3月前
|
C语言
初学者指南:使用C语言实现简易版扫雷游戏
初学者指南:使用C语言实现简易版扫雷游戏
55 0
|
3月前
|
C语言
c语言回顾-函数递归(下)
c语言回顾-函数递归(下)
50 0
|
3月前
|
C语言
C语言扫雷游戏(详解)
C语言扫雷游戏(详解)
46 0
|
1月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
62 10
|
1月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
50 9
|
1月前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
40 8
|
1月前
|
C语言 开发者
【C语言】数学函数详解
在C语言中,数学函数是由标准库 `math.h` 提供的。使用这些函数时,需要包含 `#include <math.h>` 头文件。以下是一些常用的数学函数的详细讲解,包括函数原型、参数说明、返回值说明以及示例代码和表格汇总。
49 6