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

目录
相关文章
|
26天前
|
C语言
C语言之斗地主游戏
该代码实现了一个简单的斗地主游戏,包括头文件引入、宏定义、颜色枚举、卡牌类、卡牌类型类、卡牌组合类、玩家类、游戏主类以及辅助函数等,涵盖了从牌的生成、分配、玩家操作到游戏流程控制的完整逻辑。
64 8
|
11天前
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
71 14
|
15天前
|
传感器 算法 安全
【C语言】两个数组比较详解
比较两个数组在C语言中有多种实现方法,选择合适的方法取决于具体的应用场景和性能要求。从逐元素比较到使用`memcmp`函数,再到指针优化,每种方法都有其优点和适用范围。在嵌入式系统中,考虑性能和资源限制尤为重要。通过合理选择和优化,可以有效提高程序的运行效率和可靠性。
56 6
|
19天前
|
C语言 开发者
C语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧
本文深入探讨了C语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧,并通过案例分析展示了其应用,展望了未来的发展趋势,旨在帮助读者提升程序质量和开发效率。
35 5
|
19天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
42 5
|
18天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
19天前
|
存储 算法 C语言
用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容
本文探讨了用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容,旨在为开发者提供全面的指导和灵感。
35 2
|
22天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
22天前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。
|
26天前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
48 4