【C语言篇】数组和函数的实践:扫雷游戏(附源码)

简介: 【C语言篇】数组和函数的实践:扫雷游戏(附源码)

前言

源码在最后

扫雷游戏的分析和设计

经典扫雷游戏

扫雷游戏的功能说明

使⽤控制台实现经典的扫雷游戏

  • 游戏可以通过菜单实现继续玩或者退出游戏
  • 扫雷的棋盘是9*9的格⼦
  • 默认随机布置10个雷
  • 可以排查雷
  • 如果位置不是雷,就显⽰周围有⼏个雷
  • 如果位置是雷,就炸死游戏结束
  • 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束

游戏的界⾯

被炸死,游戏结束


游戏的分析和设计

扫雷的过程中,布置的雷排查出的雷的信息都需要存储,所以我们需要⼀定的数据结构来存储这些信息。

因为我们需要在9*9的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个9*9的数组来存放信息。


那如果这个位置布置雷,我们就存放1,没有布置雷就存放0.

假设我们排查(2,5)这个坐标时,我们访问周围的⼀圈8个⻩⾊位置,统计周围雷的个数是1

假设我们排查(8,6)这个坐标时,我们访问周围的⼀圈8个⻩⾊位置,统计周围雷的个数时,最下⾯的三个坐标就会越界,为了防⽌越界,我们在设计的时候,给数组扩⼤⼀圈,雷还是布置在中间的9*9的坐标上,周围⼀圈不去布置雷就⾏,这样就解决了越界的问题。所以我们将存放数据的数组创建成11*11⽐较合适。

再继续分析,我们在棋盘上布置了雷,棋盘上雷的信息(1)和⾮雷的信息(0),假设我们排查了某⼀个位置后,这个坐标处不是雷,这个坐标的周围有1个雷,那我们需要将排查出的雷的数量信息记录存储,并打印出来,作为排雷的重要参考信息的。那这个雷的个数信息存放在哪⾥呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难。


这⾥我们肯定有办法解决,⽐如:雷和⾮雷的信息不要使⽤数字,使⽤某些字符就⾏,这样就避免冲突了,但是这样做棋盘上有雷和⾮雷的信息,还有排查出的雷的个数信息,就⽐较混杂,不够⽅便。

这⾥我们采⽤另外⼀种⽅案,我们专⻔给⼀个棋盘(对应⼀个数组mine)存放布置好的雷的信息,再给另外⼀个棋盘(对应另外⼀个数组show)存放排查出的雷的信息。这样就互不⼲扰了,把雷布置到mine数组, mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考。

同时为了保持神秘,show数组开始时初始化为字符 '*'为了保持两个数组的类型⼀致,可以使⽤同⼀套函数处理,mine数组最开始也初始化为字符'0',布置雷改成'1'

如下:

对应的数组应该是:

char mine[11][11] = {0};//⽤来存放布置好的雷的信息
char show[11][11] = {0};//⽤来存放排查出的雷的个数信息

文件结构设计

之前在【C语言篇】从零带你全面了解函数(包括隐式声明等)介绍了多⽂件的形式对函数的声明和定义,这⾥我们实践⼀下,我们设计三个⽂件:

⼀般情况下,企业中我们写代码时候,代码可能⽐较多,不会将所有的代码都放在⼀个⽂件中;我们往往会根据程序的功能,将代码拆分放在多个⽂件中。

函数的声明、类型的声明以及使用的库函数所需要包含的头文件都放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中。 如下:

test.c //⽂件中写游戏的测试逻辑
game.c //⽂件中写游戏中函数的实现等
game.h //⽂件中写游戏需要的数据类型和函数声明等

扫雷游戏的代码实现

game.h

其中方法会一一讲到

  • 这里我们先用#define定义了行和列,是为了方便更改游戏难度,要更改棋盘大小只需要更改这几行数据即可
#pragma  once

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

#define EASY_COUNT 10


//初始化棋盘
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][COLS], int row, int col);

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

test.c

  • 测试游戏的主要逻辑,是main函数所在的.c文件
  • 一来先打印菜单
  • 然后使用do-while循环让玩家可以多次玩游戏

#include "game.h"

void menu()
{
  printf("*************************\n");
  printf("*******  1. play    *****\n");
  printf("*******  0. exit    *****\n");
  printf("*************************\n");
}



void test()
{
  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);
}

int main()
{
  test();
  return 0;
}

以上部分在【C语言篇】猜数字游戏(赋源码)都是类似的,这里就不再赘述

  • 接下来就是重点,我们使用game()来分装我们实现的扫雷游戏的逻辑
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);
  //DisplayBoard(mine, ROW, COL);

  //排查雷
  FindMine(mine, show, ROW, COL);
}

这些方法的具体实现都是在game.c文件中实现

最好是写一个方法测试一次,不然找错误的时候会很痛苦😜

初始化棋盘

  • show棋盘是一来打印给玩家看的,保持神秘感使用'*'初始化
  • mine棋盘是用来布置雷的,使用'0'初始化

所以我们在参数中多加了一个char类型的set变量,这样只需使用一个函数就能初始化两个棋盘

棋盘的初始化就是二维数组初始化,遍历即可

//game.h
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);


//game.c
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;
    }
  }
}

打印棋盘

  • 方便我们测试观察,并且需要将show数组每次打印给玩家看,所以写一个打印棋盘的函数
  • 为了方便观察和读坐标,我们希望把坐标也打印出来

注意showmine数组扩大了一圈,有效的下标范围都是1-9

//game.h
void DisplayBoard(char board[ROWS][COLS], int row, int col);

//game.c
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
  printf("--------扫雷------\n");
  int i = 0;
  for (i = 0; i <= col; i++)
  {
    printf("%d ", i);
  }
  printf("\n");

  for (i = 1; i <= row; i++)
  {
    printf("%d ", i);
    int j = 0;
    for (j = 1; j <= col; j++)
    {
      printf("%c ", board[i][j]);
    }
    printf("\n");
  }
}

布置雷

  • 要布置雷,需要是随机的,所以需要用到随机数生成

以下内容在【C语言篇】猜数字游戏(赋源码)有很详细的介绍,这里简略说一下,想更具体的了解的读者可以去看看

C语⾔提供了⼀个函数叫rand,这函数是可以⽣成随机数的,函数原型如下所⽰:

int rand (void);

rand函数会返回⼀个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的⼤⼩是依赖编译器上实现的,但是⼤部分编译器上是32767。

rand函数的使⽤需要包含⼀个头⽂件是:stdlib.h

C语⾔中⼜提供了⼀个函数叫srand,⽤来初始化随机数的⽣成器的,srand的原型如下:

void srand (unsigned int seed);

程序中在调⽤rand函数之前先调⽤srand函数,通过srand函数的参数seed来设置rand函数⽣成随机数的时候的种⼦,只要种⼦在变化,每次⽣成的随机数序列也就变化起来了。

在程序中我们⼀般是使⽤程序运⾏的时间作为种⼦的,因为时间时刻在发⽣变化的。

在C语⾔中有⼀个函数叫time,就可以获得这个时间,time函数原型如下:

time_t time (time_t* timer);

time函数会返回当前的⽇历时间,其实返回的是1970年1⽉1⽇0时0分0秒到现在程序运⾏时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t类型本质上其实就是32位或者64位的整型类型。

time函数的参数timer如果是⾮NULL的指针的话,函数会将这个返回的差值放在timer指向的内存中。

如果timerNULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。

time函数的时候需要包含头⽂件:time.h

所以在test.c我们使用了如下代码初始化rand函数的种子

srand((unsigned int)time(NULL));

好的,回到正题

  • 为了方便更改游戏难度,使用#define定义我们需要布置的雷的个数,这里以10个为例
  • 还是注意x和y的坐标范围
  • 在布置雷前别忘了先判断这里是否已经布置过雷,保证最后一定是布置了是个雷
//game.h



//game.c
//布置雷是在棋盘上随机的找10个坐标布置的
//x: 1~9
//y: 1~9

void SetMine(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] != '1')
    {
      mine[x][y] = '1';//布置一个雷
      count--;
    }
  }
}

排查雷

  • 玩家输入坐标
  • 判断玩家输入坐标范围是否正确
  • 如果是雷,就游戏结束,打印mine棋盘


  • 如果不是,统计mine棋盘此位置周围8个地方的雷的数量,并将show棋盘对应位置更改为雷的个数,打印show棋盘

  • 如何判断玩家是否获得胜利?
  • 以10个雷为例,总共棋盘大小为9*9,所以玩家需要将剩下的71个不是雷的地方找出来,定义win变量循环实现,每次猜中就++,最后如果和需要猜的次数相等,则说明排雷成功
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
  int x = 0;
  int y = 0;
  int win = 0;

  while (win<col*row-EASY_COUNT)
  {
    printf("请输入要排查的坐标:");
    scanf("%d %d", &x, &y);
    if (x >= 1 && x <= row && y >= 1 && y <= col)
    {
      //输入的位置是雷
      if (mine[x][y] == '1')
      {
        printf("很遗憾,你踩雷了,游戏结束\n");
        DisplayBoard(mine, ROW, COL);
        break;
      }
      else  //不是雷
      {
        int count = GetMineCount(mine, x, y);
        show[x][y] = count + '0';
        DisplayBoard(show, ROW, COL);
        win++;
      }
    }
    else
    {
      printf("输入的坐标有误x(1~9),y(1~9),重新输入");
    }
  }
  
  if (win == row * col - EASY_COUNT)
  {
    printf("恭喜你,排雷成功\n");
    DisplayBoard(mine, ROW, COL);
  }
}
  • 为了得到雷的数量,我们实现另一个函数
  • 利用数学关系,依次相加,注意mine数组里面是数字字符,所以我们要减去'0'才能得到真正的数字
  • 使用循环简化一下也可以

//方法一:
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
    return mine[x - 1][y] + 
        mine[x - 1][y - 1] + 
        mine[x][y - 1] + 
        mine[x + 1][y - 1] +
        mine[x + 1][y] + 
        mine[x + 1][y + 1] + 
        mine[x][y + 1] + 
        mine[x - 1][y + 1] - 8*'0';
}
//方法二:
int GetMineCount(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++)
        {
            count += (mine[x + i][y + j] - '0');
        }
    }
    return count;
}

  • 得到了雷的个数,由于show数组也是char类型的,所以也需要给count+'0'才能赋值到show棋盘对应的位置,然后打印即可

以上就是最基本的扫雷游戏的逻辑实现

源码如下:

game.h

#pragma  once

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

#define EASY_COUNT 10


//初始化棋盘
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][COLS], int row, int col);

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

game.c

include "game.h"

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;
  for (i = 0; i <= col; i++)
  {
    printf("%d ", i);
  }
  printf("\n");

  for (i = 1; i <= row; i++)
  {
    printf("%d ", i);
    int j = 0;
    for (j = 1; j <= col; j++)
    {
      printf("%c ", board[i][j]);
    }
    printf("\n");
  }
}

//布置雷是在棋盘上随机的找10个坐标布置的
//x: 1~9
//y: 1~9

void SetMine(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] != '1')
    {
      mine[x][y] = '1';//布置一个雷
      count--;
    }
  }
}

//int GetMineCount(char mine[ROWS][COLS], int x, int y)
//{
//  return mine[x - 1][y] + 
//    mine[x - 1][y - 1] + 
//    mine[x][y - 1] + 
//    mine[x + 1][y - 1] +
//    mine[x + 1][y] + 
//    mine[x + 1][y + 1] + 
//    mine[x][y + 1] + 
//    mine[x - 1][y + 1] - 8*'0';
//}

int GetMineCount(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++)
    {
      count += (mine[x + i][y + j] - '0');
    }
  }
  return count;
}


void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
  int x = 0;
  int y = 0;
  int win = 0;

  while (win<col*row-EASY_COUNT)
  {
    printf("请输入要排查的坐标:");
    scanf("%d %d", &x, &y);
    if (x >= 1 && x <= row && y >= 1 && y <= col)
    {
      //输入的位置是雷
      if (mine[x][y] == '1')
      {
        printf("很遗憾,你踩雷了,游戏结束\n");
        DisplayBoard(mine, ROW, COL);
        break;
      }
      else  //不是雷
      {
        int count = GetMineCount(mine, x, y);
        show[x][y] = count + '0';
        DisplayBoard(show, ROW, COL);
        win++;
      }
    }
    else
    {
      printf("输入的坐标有误x(1~9),y(1~9),重新输入");
    }
  }
  
  if (win == row * col - EASY_COUNT)
  {
    printf("恭喜你,排雷成功\n");
    DisplayBoard(mine, ROW, COL);
  }
}

test.c


#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);
  //DisplayBoard(mine, ROW, COL);

  //排查雷
  FindMine(mine, show, ROW, COL);
}



void menu()
{
  printf("*************************\n");
  printf("*******  1. play    *****\n");
  printf("*******  0. exit    *****\n");
  printf("*************************\n");
}



void test()
{
  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);
}

int main()
{
  test();
  return 0;
}

扫雷游戏的拓展

  • 是否可以选择游戏难度
  • 简单 9*9 棋盘,10个雷
  • *中等 16*16棋盘,40个雷
  • 困难 30*16棋盘,99个雷
  • 如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚
  • 这里需要用递归
  • 是否可以标记雷
  • 是否可以加上排雷的时间显⽰

等等·····,篇幅有限,就不再继续介绍了,各位读者有兴趣可以去实现一下呀👍


以上就是关于【C语言篇】数组和函数的实践:扫雷游戏(附源码)的内容啦,各位大佬有什么问题欢迎在评论区指正,您的支持是我创作的最大动力!❤️

目录
相关文章
|
7月前
|
存储 人工智能 程序员
一文彻底搞明白C语言的数组
本文详细介绍了C语言中的数组,包括定义、初始化(静态与动态)、存储方式、访问方法及常用操作,如遍历、修改元素和作为函数参数传递。数组是C语言中最基本的数据结构之一,掌握它对编程至关重要。下篇将介绍二维数组,敬请期待!
314 0
一文彻底搞明白C语言的数组
|
8月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
370 16
|
8月前
|
算法 C语言
【C语言程序设计——循环程序设计】求解最大公约数(头歌实践教学平台习题)【合集】
采用欧几里得算法(EuclideanAlgorithm)求解两个正整数的最大公约数。的最大公约数,然后检查最大公约数是否大于1。如果是,就返回1,表示。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。作为新的参数传递进去。这个递归过程会不断进行,直到。有除1以外的公约数;变为0,此时就找到了最大公约数。开始你的任务吧,祝你成功!是否为0,如果是,那么。就是最大公约数,直接返回。
204 18
|
8月前
|
Serverless C语言
【C语言程序设计——循环程序设计】利用循环求数值 x 的平方根(头歌实践教学平台习题)【合集】
根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码,求解出数值x的平方根;运用迭代公式,编写一个循环程序,求解出数值x的平方根。注意:不能直接用平方根公式/函数求解本题!开始你的任务吧,祝你成功!​ 相关知识 求平方根的迭代公式 绝对值函数fabs() 循环语句 一、求平方根的迭代公式 1.原理 在C语言中,求一个数的平方根可以使用牛顿迭代法。对于方程(为要求平方根的数),设是的第n次近似值,牛顿迭代公式为。 其基本思想是从一个初始近似值开始,通过不断迭代这个公式,使得越来越接近。
171 18
|
8月前
|
C语言
【C语言程序设计——循环程序设计】统计海军鸣放礼炮声数量(头歌实践教学平台习题)【合集】
有A、B、C三艘军舰同时开始鸣放礼炮各21响。已知A舰每隔5秒1次,B舰每隔6秒放1次,C舰每隔7秒放1次。编程计算观众总共听到几次礼炮声。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。开始你的任务吧,祝你成功!
174 13
|
8月前
|
存储 安全 C语言
【C语言程序设计——选择结构程序设计】预测你的身高(头歌实践教学平台习题)【合集】
分支的语句,这可能不是预期的行为,这种现象被称为“case穿透”,在某些特定情况下可以利用这一特性来简化代码,但在大多数情况下,需要谨慎使用。编写一个程序,该程序需输入个人数据,进而预测其成年后的身高。根据提示,在右侧编辑器补充代码,计算并输出最终预测的身高。分支下的语句,提示用户输入无效。常量的值必须是唯一的,且在同一个。语句的作用至关重要,如果遗漏。开始你的任务吧,祝你成功!,程序将会继续执行下一个。常量都不匹配,就会执行。来确保程序的正确性。
248 10
|
8月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
201 3
|
8月前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
174 2
|
8月前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
145 1
|
算法 编译器 程序员
C语言学习笔记—P11(数组<2>+图解+题例+三子棋游戏<初级>)
C语言学习笔记(数组<2>+图解+题例+三子棋游戏<初级>)
188 0
C语言学习笔记—P11(数组<2>+图解+题例+三子棋游戏<初级>)