【C语言实战项目】扫雷游戏

简介: 【C语言实战项目】扫雷游戏

一.了解扫雷游戏

众所周知,扫雷是一项军事行动的代称,是指搜索和清除地雷、水雷及其他爆炸物的行动...哈哈,开玩笑的啦!扫雷使我们快乐:>!我们今天来学习如何使用C语言编写一个简易的扫雷游戏,如果还有不太了解扫雷游戏的同学推荐在https://minesweeper.online/cn扫雷游戏网站上了解扫雷的游戏规则,也可以在上面选择一个难度玩上几局体验一下。那我们废话不多说,下面开始我们扫雷之旅》》》

二.分析游戏实现逻辑

要编写一个游戏程序,首先要明确我们想要达到的效果是什么样,下面我将用vs2022编译器来为大家演示一下游戏运行时的样子:

首先,我们来到菜单界面,提醒用户选择玩游戏还是退出游戏


玩家选择'0'时,退出游戏,结束程序


玩家选择'1'时,开始游戏,并打印地图提醒玩家输入要排查的雷的坐标


玩家输入要排查的坐标时,如果该坐标下没有埋放雷则该坐标被排查并显示周围8个格子中雷的数量,如下图被排查坐标显示‘0’的意思即附近8个格子中雷的数量为‘0’


玩家输入要排查的坐标时,如果该坐标下埋放了雷,则玩家被炸死游戏结束并打印出该局游戏中所有雷的方位


玩家成功排查出所有的雷时,游戏胜利,游戏结束


注意,当玩家输入排查过的坐标时,提醒玩家已排查过,重新输入


玩家输入地图外坐标时,提醒玩家坐标非法,重新输入


三.逐步实现游戏及其逻辑详解

     通过第二部分对流程的介绍,我们已经对游戏的流程有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分布分模块来分析这个游戏的流程,最后再将各各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!


!!!注意,该部分的代码只是为了详细介绍某一部分的游戏实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看完整详细代码可以移步本文第四部分。


1.实现菜单功能:

      菜单部分的逻辑比较简单,就是利用C语言printf函数打印出这个菜单界面即可。基础问题就不过多赘述了,代码如下:

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

2.实现游戏可循环玩:

     由于我们要实现玩不够可以继续玩的游戏逻辑,因此选择do...while的循环语句来实现这一部分的逻辑,每步的详细解释见代码注释:

void menu()//菜单函数实现打印菜单
{
  printf("*********************************************\n");
  printf("*********************************************\n");
  printf("*****************   1.play   ****************\n");
  printf("*****************   0.exit   ****************\n");
  printf("*********************************************\n");
  printf("*********************************************\n");
}
 
void test()//测试游戏运行
{
  //srand((unsigned int) time(NULL));
    //利用时间戳生成随机数以达到电脑可以实现随机布雷
 
  int input = 0;
 
  do//使用do  while语句来实现游戏可以连续一直玩
  {
    menu();//菜单函数,打印菜单供玩家选择
 
    printf("请选择:>\n");//提醒玩家选择
 
    scanf("%d", &input);//用scanf接收玩家选择存入变量input中
 
    switch (input)//利用分支语句实现玩家的选择
    {
    case 1://当玩家输入1,运行游戏
      game();
      break;
    case 0://当玩家输入0,提醒玩家游戏结束
      printf("游戏结束\n");
      break;
    default://当玩家输入了非选项数字时,提醒玩家重新输入
      printf("输入错误,请重新选择\n");
      break;
    }
 
  } while (input);//用变量input的值作为while循环的判定执行条件
                    //当input不为0时,该循环都可一直运行下去。
}

3.初始化地图:

      实现了打印菜单供玩家选择和一直玩后,我们就要来到游戏的核心部分了,即第一步,由于我们是利用二维数组实现在“地图”上排雷的,因此当每局游戏开始时,我们应该先将地图全部初始化为‘0’(注意我们创建的是字符数组,因此0是字符0!),以便在后续埋雷时与雷做区分。

      如图,我们先来分析一下一个地图上的格子在一局游戏中会有几种状态

     ①初始时代表未解密的"*"②埋雷时与‘1’(雷)区别的”0“③安放雷时代表雷的”1“④被排查之后代表周围雷数的数字”n“

     即一个格子最多可能会有四种状态,而我们一个二维数组要写兼顾四种的状态的函数是非常复杂的,并且很容易出错,导致露馅,因此我们不妨创建两个二维数组来分别存放格子的四种状态:首先,第一个棋盘用来存放没埋雷的"0"和埋了雷的"1"。其次,第二个棋盘用来存放未解密的"*"和排查后的数字"n"。这样分别存放恰好可以让我们的后台埋放雷数组和玩家显示数组分开,因此我们先根据设想,将后台埋放雷数组全部初始化为”0“,其次再将玩家显示数组全部初始化为”*“


      这里有一点需要注意:由于我们在排查棋盘最外围的那一圈格子时只能排查到六个,甚至四个角只能排查四个,因此我们不妨将原定的9*9数组上下左右各多加一行(或一列),但不在这一圈埋放雷或显示,仅用来防止我们后续排查雷时越界访问数组

      如图,蓝线所画的格子即为我们加上防止越界的格子

     初始化二维数组的函数很简单,上节三子棋中我们也有提到,但这次我们需要一次性初始化两个同样大小但不同内容的二位数组,可能有些同学会想:如果不好判断的话,要不写两个初始化函数分别初始化地图算了。但既然这两个数组一模一样大,我们不如在传参时多加一个参数达到分别初始化的效果,该部分代码如下:

   //调用函数时传参多传一个参数
   //以下时函数调用示范
    InitBoard(mine, ROWS, COLS,'0');
  InitBoard(show, ROWS, COLS,'*');
 
 
//以下是函数体
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;
    }
  }
}

4.打印地图:

     这里有三个点需要注意:

1.前面创建二维数组时我们为了不使数组出现越界访问因此使用的是11*11大小的数组,但是在向玩家打印时要注意只能打印中间的9*9的地图!。因此我们选择只打印每行每列下标为1-9下标的元素即可。并且只能打印show数组,不能将mine数组也打印出来,否则会露馅。

2.如图,我们还需要在第一行和第一列前面加上序号方便玩家选择,该部分实现逻辑较简单,就是在打印每一行前打印一个数字变量即可,详情见下方代码。

3.如图,为了使棋盘与棋盘间很好的分割辨识,我们会在每次打印棋盘前后打印“--------扫雷---------”的分割线来分割,该部分也较为简单,详见代码。

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
  int i = 0;
  printf("--------扫雷--------\n");
  //控制列号
  for (i = 0;i <= row; 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 ", board[i][j]);
    }
    printf("\n");
  }
  printf("--------扫雷--------\n");
}

5.埋放地雷:

      埋放地雷的实现也非常简单,即利用rand函数随机生成n(雷数)个坐标然后将初始化的字符0改为字符1即可。

如果还有对rand函数不了解的同学,请先移步:rand()函数详解。

//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
  int count = EASY_COUNT;
  while (count)
  {
        int x = rand() % row + 1;
      int y = rand() % col + 1;
      if (board[x][y] == '0')
      {
       board[x][y] = '1';
       count--;
      }
  }
}

6.排查地雷:

   接下来来到最难的排查地雷,输入坐标,然后排查其周围八个坐标有几个“1”即可,代码实现只需将这8个区域的数值相加,然后减去8个字符“0”的大小再返回该数值即可。

八个坐标的代数表示如下:

       但有几点需要注意:

1.我们是利用排查次数来判断玩家是否排查完地雷取得胜利的,因此就需要保证排查过的坐标不能被二次排查,否则可能会导致程序误判玩家胜利

2.其次,由于我们创建的是字符数组,因此放入数组的是字符的“0”和“1”,所以不能使用简单的整形加减法来返回数字,而应该使用周围八个字符“0”和“1”的总值来减去八个字符‘0’的值,最后返回的数字才是周围的含雷数。

3.因为是初级阶段的扫雷,因此暂不增加递归展开的功能,有兴趣的同学可以自己探索。

   代码如下:

//排查1个坐标
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
 
  return (mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1]
    + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0');
}
 
//排查雷
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<(row*col-EASY_COUNT))
  {
    printf("请输入要排查的坐标:>");
    scanf("%d %d", &x, &y);
 
    if (x > 0 && x <=row && y>0 && y <=col)
    {
      if (show[x][y] != '*')
      {
        printf("该坐标已被排查过!\n");
        continue;
 
      }
      if (mine[x][y] == '1')
      {
        printf("很遗憾,你被炸死了\n");
        DisplayBoard(mine, ROW, COL);
        break;
      }
      else
      {
        int n=get_mine_count(mine,x,y);
 
        show[x][y] = n + '0';//数字2怎么变成字符2?
 
        DisplayBoard(show, ROW, COL);
        win++;
      }
    }
    else
    {
      printf("输入坐标非法,请重新输入!");
    }
  }
  if (win == (row * col - EASY_COUNT))
    {
            printf("恭喜你,扫雷成功!\n");
    }
  
}

四.整合代码测试及总结

     我们同样将游戏运行的代码分为三个模块分开书写,完整代码如下:

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)
{
  int i = 0;
  printf("--------扫雷--------\n");
  //控制列号
  for (i = 0;i <= row; 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 ", board[i][j]);
    }
    printf("\n");
  }
  printf("--------扫雷--------\n");
}
 
 
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
  int count = EASY_COUNT;
  while (count)
  {
        int x = rand() % row + 1;
      int y = rand() % col + 1;
      if (board[x][y] == '0')
      {
       board[x][y] = '1';
       count--;
      }
  }
}
 
//排查1个坐标
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
 
  return (mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1]
    + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0');
}
 
//排查雷
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<(row*col-EASY_COUNT))
  {
    printf("请输入要排查的坐标:>");
    scanf("%d %d", &x, &y);
 
    if (x > 0 && x <=row && y>0 && y <=col)
    {
      if (show[x][y] != '*')
      {
        printf("该坐标已被排查过!\n");
        continue;
 
      }
      if (mine[x][y] == '1')
      {
        printf("很遗憾,你被炸死了\n");
        DisplayBoard(mine, ROW, COL);
        break;
      }
      else
      {
        int n=get_mine_count(mine,x,y);
 
        show[x][y] = n + '0';//数字2怎么变成字符2?
 
        DisplayBoard(show, ROW, COL);
        win++;
      }
    }
    else
    {
      printf("输入坐标非法,请重新输入!");
    }
  }
  if (win == (row * col - EASY_COUNT))
    {
            printf("恭喜你,扫雷成功!\n");
    }
  
}
 
 

game.h:

#define _CRT_SECURE_NO_WARNINGS 1
 
#pragma once
#include<stdio.h>
 
#define ROW 9
#define COL 9
 
 
#define ROWS ROW+2
#define COLS COL+2
 
 
#define EASY_COUNT 10
 
 
#include<stdlib.h>
#include<time.h>
 
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols,char set);
 
//打印棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col);
//你传了一个11*11的数组,就要拿一个11*11的数组接收!
 
 
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col);
 
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row,int 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 };//存放排查雷的信息
 
  //初始化棋盘
  InitBoard(mine, ROWS, COLS,'0');
  InitBoard(show, ROWS, COLS,'*');
 
  //打印棋盘
  //DisplayBoard(mine, ROW, COL);
  DisplayBoard(show, ROW, COL);
 
  //布置雷
  SetMine(mine,ROW,COL);
    
  //排查雷
  //DisplayBoard(mine, ROW, COL);
  FindMine(mine,show,ROW,COL);
 
}
 
 
 
void test()
{
  srand((unsigned int)time(NULL));
  int input = 0;
  do
  {
        menu();
      printf("请选择:>\n");
    scanf("%d", &input);
    switch (input)
    {
          case 1:
        game();
            break;
        
      case 0:
        printf("退出游戏\n");
        break;
 
      default:
        printf("输入错误,请重新输入:");
        break;
    }
  } while (input);
 
}
 
 
int main()
{
  test();
 
  return 0;
}
 
 

    结语:

      扫雷与三子棋游戏都是比较经典的电脑游戏,通过这些游戏的设计,我感受到了程序设计的巧思之处,或许我们不会设计好看的界面,或许我们也不会非常厉害的编程技术,但就即使利用最基础的字符打印,随机数生成,顺序,分支,循环,就可以设计组合出这样有模有样的游戏了,编程当真是奇妙。

      当然在这次尝试中我也发现了很多自己的不足,以及因为能力而做的妥协与阉割,但这我还愿意将这次探索认为是一个好的开始,希望以后的自己能就这样变得越来越厉害!

      最后的最后:Hello,world!


相关文章
|
22天前
|
C语言
扫雷游戏(用C语言实现)
扫雷游戏(用C语言实现)
66 0
|
23天前
|
编译器 C语言
猜数字游戏实现#C语言
猜数字游戏实现#C语言
69 1
|
24天前
|
存储 C语言
揭秘C语言:泊舟的猜数字游戏
揭秘C语言:泊舟的猜数字游戏
|
23天前
|
C语言
初学者指南:使用C语言实现简易版扫雷游戏
初学者指南:使用C语言实现简易版扫雷游戏
29 0
|
25天前
|
C语言
C语言扫雷游戏(详解)
C语言扫雷游戏(详解)
32 0
|
22天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
30 3
|
13天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
30 10
|
6天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
12天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
35 7
|
12天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
24 4