【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语言初学者,我们下期见!





相关文章
|
C语言
扫雷游戏(用C语言实现)
扫雷游戏(用C语言实现)
238 0
|
10月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
498 16
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
345 7
|
C语言
c语言回顾-函数递归(上)
c语言回顾-函数递归(上)
117 2
|
C语言
初学者指南:使用C语言实现简易版扫雷游戏
初学者指南:使用C语言实现简易版扫雷游戏
186 0
|
C语言
c语言回顾-函数递归(下)
c语言回顾-函数递归(下)
141 0
|
2月前
|
存储 C语言
`scanf`是C语言中用于按格式读取标准输入的函数
`scanf`是C语言中用于按格式读取标准输入的函数,通过格式字符串解析输入并存入指定变量。需注意输入格式严格匹配,并建议检查返回值以确保读取成功,提升程序健壮性。
905 0
|
4月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
297 15
|
10月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
518 23
|
9月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
308 1
一文彻底搞清楚C语言的函数