【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!


相关文章
|
2月前
|
C语言
扫雷游戏(用C语言实现)
扫雷游戏(用C语言实现)
104 0
|
19天前
|
C语言 Windows
C语言课设项目之2048游戏源码
C语言课设项目之2048游戏源码,可作为课程设计项目参考,代码有详细的注释,另外编译可运行文件也已经打包,windows电脑双击即可运行效果
30 1
|
2月前
|
编译器 C语言
猜数字游戏实现#C语言
猜数字游戏实现#C语言
85 1
|
2月前
|
存储 C语言
揭秘C语言:泊舟的猜数字游戏
揭秘C语言:泊舟的猜数字游戏
|
2月前
|
C语言
初学者指南:使用C语言实现简易版扫雷游戏
初学者指南:使用C语言实现简易版扫雷游戏
37 0
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
36 3
|
C# C语言 C++
VS2012编写C语言项目
原文:VS2012编写C语言项目 这两天看了一下C语言方面的知识,大学的时候使用的Turbo C对于我来说已经是很久之前的事情了,编写C语言的还有VC++,不过这货在64的表现实现是很让人失望,还是用最熟悉的VS吧,之前没有用VS搞过C语言,今天倒腾了一下,重点分享一下自己的过程吧。
648 0
|
18天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
31 6
|
1天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
19 6