大话C语言游戏(基础篇)——三子棋游戏实现超详细剖析及优化建议

目录

文章目录

一.概要

二.整体架构流程&相关知识点介绍

1.流程图

2.相关知识点介绍

三.三子棋实现

1.分文件创建项目

图例

2.整个游戏项目完整代码展示

头文件部分

被调函数文件

主函数部分

游戏各个部分设计细节思路

主函数模块分析

被调函数模块

1.login()函数:登录界面的设置

2.game()函数:游戏的接口函数,整个函数调用的核心

3.InitialBoard(InitialBoard(char chessBoard[ROW][COL], int row, int col)):棋盘初始化函数

4.Display(char chessBoard[ROW][COL], int row, int col):显示对弈过程函数

5.PlayerMove玩家下棋和RobotMove电脑下棋

6.IsFull(char chessBoard[ROW][COL], int row, int col):判断棋盘是否被棋子占满

7.IsWin(char chessBoard[ROW][COL], int row, int col):游戏输赢机制的设定

最终游戏运行过程展示

总结


一.概要

hello,各位对编程感兴趣的小伙伴们大家好鸭!首先很开心大家能够读到我写的文章,同时殷切的希望能对大家能带来一定的帮助!

本篇文章是大话C语言游戏(基础篇)专栏中创作的第一篇文章,在本篇专栏中所涉及都是一些基础的语法内容,对初学者来说是非常容易理解的,相信很多朋友在初学C语言那些枯燥语法的过程中,很容易产生排斥心理,而且对于如何将他们实践运用起来,做一些有意义的事往往无从下手,在这个专栏里由小风带着大家自己动手实现一些有趣的小项目吧,带着兴趣学习将会使得我们学起来产生事半功倍的效果!

废话不多说,我们直接进入主题吧!今天将带着大家实现我们小时候经常玩的一个简单小游戏,虽然玩起来简单,但对于初学者的我们来说还是很有挑战性的,特别是游戏当中所涉及的逻辑关系,还是有点复杂的哈哈哈,看到这里小伙伴们是不是产生了一些畏难心里?

其实完全不用过于担心,学习本就是逆水行舟,而且当我们能亲手实现一个小项目不仅能提升我们自身综合运用和实践能力,我们也会充满无比自豪和满足感,这就是我们编程的乐趣所在!并且小风会带着大家抽丝剥茧逐层理解,让小伙伴们看完后一定会有收获的,主打的就是一个干货满满哈哈!

二.整体架构流程&相关知识点介绍

1.流程图

首先当我们在进行开始着手一个项目的的时候,并不是直接上手直接编程,这样的话效率不是很高,而且如果是想到那些到哪的话,这样将会是我们的程序逻辑感很差,也难以理解,甚至到后可能不得不终止。因此在开始之前,我们的首要任务是对整个项目的框架建设及,就像盖房子一样,后面在对其不断的加工,使得整个项目条理清晰、完整而美观。

下图就是我们所要搭建的一个整体的框架了,虽然简单但却很有必要。

整个游戏我们将拆分成三大部分,登录界面,进入游戏进行对弈,最后便是退出游戏了。

2.相关知识点介绍

在该项目中,我们将会使用哪些知识来进行搭建呢?

首先最基础的便是选择分支结构。在我们项目中存在各种条件的判断及选择,因此选择分支结构使我们程序当中必不可少的一环,其中涉及的有:if...else语句、if...else if...else语句、switch...case基本都涉及,在什么样的场合下,该使用那种分支结构更加合适,相信大家学完之后一定会深有体会的哈哈。

其次便是我们的循环结构了。在游戏当中,登录界面的重置,数组的初始化,以及一些双层循环的控制等等,使用循环结构对整个程序的实现的帮助真的不容忽视。在程序当中:像while循环、for循环以及do...while三大循环均有使用,说到这大家是不是非常期待呢?

然后就是我们的函数了。整个程序当中封装了很多的函数,函数的特点在这就不多说了,但我们设计函数的时候最好是本着高内聚低耦合的特点来设计(即我们设计函数要求独立性强,模块化程度高,每一个函数的分工明确)。向我们项目所涉及的函数有:

  • void Login():登录函数,打印出游戏的登录界面
  • void game():游戏进入窗口函数,调用该函数,开始进入对局之中
  • void InitialBoard(char chessBoard[ROW][COL], int row, int col):棋盘初始化函数,打印棋盘
  • void Display(char chessBoard[ROW][COL], int row, int col):显示函数,将对弈过程中的器具显示出来
  • void PlayerMove(char chessBoard[ROW][COL], int row, int col):玩家下棋函数,让玩家能够操作下棋的位置
  • void RobotMove(char chessBoard[ROW][COL], int row, int col):电脑下棋函数,模拟电脑下棋,其中下的位置设置成为随机值
  • char IsWin(char chessBoard[ROW][COL], int row, int col):判断输赢函数,通过返回特定的字符代表输、赢、平局的各种情况、‘*’表示玩家获胜、‘#’表示电脑获胜,‘Q’表示平局,‘C’表示并未分出输赢并继续进行下棋、
  • int IsFull(char chessBoard[ROW][COL], int row, int col):判断棋盘的位置是否已经全部落有棋子 

这样看下去是否思路已经比较清晰了呢,现在我们只需要完善每个函数的内容就可以了。相信细心地的小伙伴发现了我们的函数名的命名都会尽可能的表示其功能,这样也更能方便我么理解和维护

最后我们涉及的最后知识点便是数组。因为棋盘是一个二维的界面,因此我们想通过二维数组对其进行打印和填充值

三.三子棋实现

1.分文件创建项目

首先介绍一下我们的使用的编辑软件是Visual Studio 2019(其他版本同样可以运行),比较推荐初学者通过这个软件进行学习C语言(当然也有很多其他好用的软件),目前很多高校仍然使用非常古老VC++6.0等一些比较落后的软件,完全跟不上实际的应用而且有些育发液已经过时了,因此我们需要有自己的判断和选择。

为什么推荐读者开发项目,采取份文件的方式?

分文件开发是目前主流开发方式,基本上所有的工程都是这样开发的。分文件可以将将我们的整个项目大致分为三大部分:

  1. 自定义头文件:用于封装整个项目所需调用的头文件、常量的定义、自定义函数的声明。
  2. 函数封装文件:通常需要引用我们自定义的头文件,在这个文件中主要包含的是实现各种功能的函数的定义(也可以将单独一个函数进行封装成一个文件,根据具体的需求)
  3. 主函数文件:整个项目的入口,即只包含了一个main函数

分文件操作的好处:

  1. 便于复用代码。通用性强的重复的功能只要写一遍就可以了,下次要用在其它程序上时只要更改很小的部分或者可以不用更改。
  2. 便于多人协作。在设计软件之初就可以很清楚地分配各个开发部门的任务。模块的编写者本身只要关注他所写的东西,清楚这一部分的功能,留出接口就可以了。另外,对于整个工程的负责人而言,这样会方便浏览全局的工作进度,统筹人员安排。
  3. 便于修改和维护。如果能确定只是某个模块有问题,在模块内解决即可,不需要牵一发而动全身。要升级某一部分的功能,可以只针对具体的模块重新开发,节约成本。

图例

通过上面的图例,我们不仅会产生一个疑问:他们之间是如何建立联系的?

首先,对于这个问题我们不难发现不管是在主函数文件还是封装其他调用的函数文件中他们的头文件中都是引用的我们自定义的头文件:“game.h”

其次就是这个函数文件和主函数文件时通过主函数的调用建立联系,这样三个部分就有机的连成了一个整体

2.整个游戏项目完整代码展示

头文件部分

#define _CRT_SECURE_NO_WARNINGS  //这个是VS2019特定的调用,否则使用scanf函数将会报错
#include<stdio.h>
#include<stdlib.h>  //随机函数调用的头文件
#include<time.h>   //随机种子所包含的time(NULL)

#define ROW 3  //行常量
#define COL 3  //列常量


//登录函数
void Login();

//游戏进入窗口函数
void game();

//游戏棋盘初始化
void InitialBoard(char chessBoard[ROW][COL], int row, int col);

//显示对弈过程
void Display(char chessBoard[ROW][COL], int row, int col);

//玩家下棋
void PlayerMove(char chessBoard[ROW][COL], int row, int col);

//电脑下棋
void RobotMove(char chessBoard[ROW][COL], int row, int col);

//判断输赢
char IsWin(char chessBoard[ROW][COL], int row, int col);

//判断平局
int IsFull(char chessBoard[ROW][COL], int row, int col);

被调函数文件

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"

//登录函数
void Login()
{
	printf("|-------------------------------------------|\n");
	printf("|*******************************************|\n");
	printf("|***********     1-->进入游戏      *********|\n");
	printf("|***********     0-->退出游戏      *********|\n");
	printf("|*******************************************|\n");
	printf("|-------------------------------------------|\n");
}

//游戏进入窗口函数
void game()
{
	char flag = 0;
	//棋盘设计
	char chessBoard[ROW][COL];
	
	//初始化棋盘
	InitialBoard(chessBoard, ROW, COL);

	//显示对弈过程棋盘
	Display(chessBoard, ROW, COL);

	//进行下棋,定义玩家先下
	//由于下棋是一个循环反复地过程,因此在这里需要使用循环进行博弈过程
	while (1)
	{
		//玩家下棋
		PlayerMove(chessBoard, ROW, COL);

		//每下完一步,显示对应的棋盘信息
		Display(chessBoard, ROW, COL);

		//判断输赢
		//当所有格子都被棋子占据后,并且没有分出输赢则为平局
		flag = IsWin(chessBoard, ROW, COL);
		if (flag != 'C')
		{
			break;
		}

		//电脑下棋
		RobotMove(chessBoard, ROW, COL);
		Display(chessBoard, ROW, COL);

		//判断输赢
		flag = IsWin(chessBoard, ROW, COL);
		if (flag != 'C')
		{
			break;
		}
	}
	if (flag == '*')
	{
		printf(">>>恭喜你,获得了胜利!\n");
	}
	else if (flag == '#')
	{
		printf(">>>电脑获胜!\n");
	}
	else if (flag == 'Q')
	{
		printf(">>>平局!\n");
	}
	printf(">>>游戏结束!\n");
}

//棋盘初始化函数
void InitialBoard(char chessBoard[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	//为了显示的对局前的空棋盘,一开始棋盘中的元素都是空格
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			chessBoard[i][j] = ' ';
		}
	}
}

//显示棋盘
void Display(char chessBoard[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		//先打印数据
		for (j = 0; j < col; j++)
		{
			if (j < col - 1)
			{
				printf(" %c |", chessBoard[i][j]);
			}
			else
				printf(" %c ", chessBoard[i][j]);
		}
		//换行
		puts("");
		//先打印数据
		//再打印分割行
		for (j = 0; j < col; j++)
		{
			//只需要中间打印两行分割行
			if (i < row - 1)
			{
				if (j < col - 1)
					printf("---|");
				else
					printf("---");
			}
		}
		//换行
		puts("");
	}
}

void PlayerMove(char chessBoard[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;

	while (1)
	{
		//提示信息
		printf("玩家下棋,请输入下棋位置>>>");
		scanf("%d %d", &x, &y);
		
		//清空缓存区的数据,修复了输入特殊符号时,读取不了数据所造成的死循环
		//假设输入的是x j--->缓存区中的内容是'x' 'j' '\n'三个字符
		/*getchar();
		getchar();*/
		//检查输入位置是否合法
		if ((x >= 1) && (x <= row) && (y >= 1) && (y <= col))
		{
			//位置没有错误时,还需检测该位置上是否有棋子
			//注意数组下标是从0开始的
			if (chessBoard[x - 1][y - 1] == ' ')
			{
				chessBoard[x - 1][y - 1] = '*';
				//如果输入的是有效合法的位置,则跳出循环,轮到电脑下棋
				break;
			}
				
			else
				printf(">>>该位置存在棋子,请输入其他位置!\n");
		}
		else
			printf(">>>输入位置信息错误,请重新输入!\n");
			
	}
	
}

void RobotMove(char chessBoard[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("电脑下棋\n");
	while (1)
	{
		//假设电脑下棋的方式,是通过随机函数来制定其位置的
		x = rand() % row;  //范围是 0 到 row-1
		y = rand() % col;  //范围是 0 到 col-1
		if (chessBoard[x][y] == ' ')
		{
			chessBoard[x][y] = '#';
			break;
		}
	}
	
}

//判断数组中是否还存在空格,不存在则返回1,否则返回0
int IsFull(char chessBoard[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			if (chessBoard[i][j] == ' ')
				return 0;
		}
	}
	return 1;
}

char IsWin(char chessBoard[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	//判断行、列以及对角线是否满足赢的条件
	for (i = 0; i < row; i++)
	{
		int count1 = 0;  //行
		int count2 = 0;  //列
		int count3 = 0;  //主对角线
		int count4 = 0;  //次对角线
		//判断
		for (j = 0; j < col - 1; j++)
		{
			//行
			if (chessBoard[i][j] == chessBoard[i][j + 1] && chessBoard[i][j] != ' ')
			{
				count1++;
				if (2 == count1)
					return chessBoard[i][j];
			}
			//列
			if (chessBoard[j][i] == chessBoard[j + 1][i] && chessBoard[j][i] != ' ')
			{
				count2++;
				if (2 == count2)
					return chessBoard[j][i];
			}
			//主对角线
			if ((i==j) && (chessBoard[i][j] == chessBoard[i + 1][j + 1]) && (chessBoard[i][j] != ' '))
			{
				count3++;
				if (2 == count3)
					return chessBoard[i][j];
			}
			//次对角线
			if ((chessBoard[i][col - j - 1] == chessBoard[i][col - j - 2]) && (chessBoard[i][col - j - 1] != ' '))
			{
				count4++;
				if (2 == count4)
					return chessBoard[i][col - j - 1];
			}
			//判断是否平局
			if (IsFull(chessBoard, row, col))
			{
				return 'Q';
			}
		}
	}
	return 'C';
}

主函数部分

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"


//三子棋游戏开发
int main()
{
	int choose = 0;
	srand((unsigned int)time(NULL));
	do
	{
		Login();
		printf("请输入你想进行的操作>>>");
		scanf("%d", &choose);
		switch (choose)
		{
		case 1:
			//进入游戏
			game();
			break;
		case 0:
			printf("\n>>>游戏已退出!\n");
			break;
		default : 
			printf("\n>>>选择错误,请重新选择!\n");
			break;
		}
	} while (choose);
	return 0;
}

四、游戏各个部分设计细节思路

主函数模块分析

这里我们采用的是do...while和switch...case嵌套来设计实现我们登录界面的进入,通过choose的值巧妙地将分支和循环实现了一个联动,通过下图的一个展示我们发现主函数还是很容易理解的。

被调函数模块

1.login()函数:登录界面的设置

通过微调printf函数的输出细节,最终实现上图的一个界面效果

2.game()函数:游戏的接口函数,整个函数调用的核心

这一部分在整个游戏项目中可以说是相当重要的,影响着整个游戏能否正常的运行。

首先让我们来观察这一部分的代码,不难发现基本上整个项目中的大部分函数都被集成在这个函数中,相当于是游戏核心,调度游戏中各种功能。

//游戏进入窗口函数
void game()
{
	char flag = 0;
	//棋盘设计
	char chessBoard[ROW][COL];
	
	//初始化棋盘
	InitialBoard(chessBoard, ROW, COL);

	//显示对弈过程棋盘
	Display(chessBoard, ROW, COL);

	//进行下棋,定义玩家先下
	//由于下棋是一个循环反复地过程,因此在这里需要使用循环进行博弈过程
	while (1)
	{
		//玩家下棋
		PlayerMove(chessBoard, ROW, COL);

		//每下完一步,显示对应的棋盘信息
		Display(chessBoard, ROW, COL);

		//判断输赢
		//当所有格子都被棋子占据后,并且没有分出输赢则为平局
		flag = IsWin(chessBoard, ROW, COL);
		if (flag != 'C')
		{
			break;
		}

		//电脑下棋
		RobotMove(chessBoard, ROW, COL);
		Display(chessBoard, ROW, COL);

		//判断输赢
		flag = IsWin(chessBoard, ROW, COL);
		if (flag != 'C')
		{
			break;
		}
	}
	if (flag == '*')
	{
		printf(">>>恭喜你,获得了胜利!\n");
	}
	else if (flag == '#')
	{
		printf(">>>电脑获胜!\n");
	}
	else if (flag == 'Q')
	{
		printf(">>>平局!\n");
	}
	printf(">>>游戏结束!\n");
}

在上图的代码中,为了方便大家理解,基本上每一步的细节都有注释。在这其中,我们会经常看见ROW和COL两个参数总是重复出现,其实这两个参数使我们在头文件中用#define宏定义的两个常量,至于为什么这样来操作呢,小风认为主要有如下几个优点:

  1. 方便程序的修改使用:简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时,我们可以用较短的有意义的标识符来写程序,这样更方便一些。
  2. 提高程序的运行效率:使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至于只完成一点操作。
3.InitialBoard(InitialBoard(char chessBoard[ROW][COL], int row, int col)):棋盘初始化函数

在这里我们通过二维数组进行设置棋盘的内容,在程序的开始会为我们展示出一个空棋盘,并不代表数组中没有元素,而是是数组的值是空格,具体如何设计,各位小伙伴们可以参考总代码中的该部分函数。

4.Display(char chessBoard[ROW][COL], int row, int col):显示对弈过程函数

这部分的主要注意的细节还是对整个棋盘外观的微调,只要将这部分设置好,通过循环打印值即可

5.PlayerMove玩家下棋和RobotMove电脑下棋

之所以将这两个函数放在一起,其实它们的核心设计都是一样的:那就是判断所要下的位置是否放有棋子,如果是空格则没有棋子,如果是‘#’和‘*’都表示被占用,因此需要重新输入位置,所以需要采用循环来控制。

PlayerMove函数需注意的细节:数组的元素下标是从0开始的,而玩家输入的位置是从1开始的,因此需要将位置进行(x-1,y-1)转换一下

RobotMove函数需注意细节:主要是如何能让电脑自动下棋,因此这里我们需要调用随机函数进行位置的自动选择

6.IsFull(char chessBoard[ROW][COL], int row, int col):判断棋盘是否被棋子占满

这部分的函数还是很好理解的,我们只需要遍历二维数组中的元素是否存在空格,如果被占满则返回1,否则返回0

7.IsWin(char chessBoard[ROW][COL], int row, int col):游戏输赢机制的设定

首先我们得对三子棋的游戏规则进行设计,我们规定:

  1. 行、列、主对角线或次对角线中的任意一个中满足存在三个相同的值,那么判断这个棋子符号的玩家获胜(需将空格排除在外,否则游戏无法进行)
  2. 这里我们设置了很多标记函数,count1、count2、count3和count4,这是用来观测是满足游戏结束条件
  3. 主对角线和次对角线在数组中下标的逻辑关系需要理清楚,这是比较难理解的
  4. 函数返回值的设定:‘*’玩家赢,‘#’电脑赢,‘Q’平局,‘C’未分出输赢并且棋盘仍有位置

五、最终游戏运行过程展示

六、优化建议(希望大家能深入思考解决办法)

其实本次游戏还是有一些不足之处的,同时希望大家能自己探索一下,如果能解决的话一定要私信小风哦,哈哈!

  • 在PlayerMove玩家下棋游戏中,存在着输入数据不合法的问题,如果我们输入的位置不合法,比如说输入两个英文字符时,scanf函数将无法读取数据,那么这些字符将会永远停留在缓存区中,导致程序将会陷入死循环,这也是一个很严重bug。小风想到的是使用getchar函数来清除,但同样存在着一些问题,因为还需考虑错误输入字符的个数等问题

  • 还有就是吐槽一下,电脑下棋的水平实在太low了,如果有能力的小伙伴可以设计一个比较高级的算法,提升一下电脑下棋的水平,哈哈

七、总结

以上便是整个代码实现的整个流程分析讲解,当然啦,还需要各位小伙伴们自己的不断尝试和实践,这中间可能会杯出现各种bug卡主,需要我们进行调试分析,但是只要我们能够坚持不懈得去克服,最终我们的能力一定会有长足的进步!最后希望小风的这篇文章能对大家有所帮助!

下期小风将会为大家分享如何使用Visual Studio 2019的下载安装教程以及如何对我们的代码进行调试,最后希望大家能关注一波!谢谢大家!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

whelloworldw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值