大家好,很高兴又和大家见面啦!前面两个篇章我们将汉诺塔问题和青蛙跳台阶的问题详细的探讨了一下,这两个问题更多的是运用函数的相关内容进行解题,今天我们将开始探讨第二个小游戏三子棋,编写这个游戏又会涉及哪些知识点呢?下面我们将开始今天的内容。
三子棋是一种民间传统游戏,又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战、双方依次在9宫格棋盘上摆放棋子,率先将自己的三个棋子走成一条线就视为胜利,而对方就算输了,但是三子棋在很多时候会出现和棋的局面。
胜利方式如图示一样,横向,纵向或者斜向只要有3颗棋子能像这样连成一条线,那就算胜利,另一方就算失败。
如果要编写游戏的话,我希望通过两个模块来实现——游戏模块和主程序模块。在前面的学习中我们知道了游戏模块可以通过创建game.c
和game.h
来实现,主程序模块也就是我们编写main函数的地方,所以我们首先要将这些内容给创建好:
创建好这些模块后,现在我们要开始编写代码了。在编写代码前,我们先理一下编写思路:
以上差不多就是这次游戏编写的大致方向了,文字比较多,还需要朋友们耐心阅读。现在我们也理清了设计思路,下面就要开始编写咱们的第二个游戏了;
这个功能的实现比较简单,我们在主模块中实现就可以了,下面我们直接编写代码:
//功能一——菜单栏
void menu()
{
printf("##############################\n");
printf("####1.开始游戏 0.退出游戏####\n");
printf("##############################\n");
}
运行效果如下所示:
但是这里就有一个问题,光有菜单栏也不行呀,我们是不是还应该给用户提供输入提示来让用户做出选择呀,这里我们可以借助选择语句和输入函数共同实现:
//定义选择变量
int choose = 0;
//打印输入提示语
printf("请输入(0/1)>:");
//输入数字存储在选择变量中
scanf("%d", &choose);
//通过switch语句进行判断
switch (choose)
{
//用户选择0,则退出游戏
case 0:
break;
//用户选择1,则开始游戏
case 1:
break;
//用户输入除0和1以外的其它数字,则报错并提示重新输入
default:
printf("输入错误,请重新输入\n");
break;
}
这样我们就完成了第一个功能,菜单栏;
这里我们可以通过循环语句来实现,重复的内容有菜单栏的全部内容,这里选择变量可以不需要重复定义,可以放在循环外:
//定义选择变量
int choose = 0;
do
{
//菜单界面
menu();
//打印输入提示语
printf("请输入(0/1)>:");
//输入数字存储在选择变量中
scanf("%d", &choose);
//通过switch语句进行判断
switch (choose)
{
//用户选择0,则退出游戏
case 0:
break;
//用户选择1,则开始游戏
case 1:
break;
//用户输入除0和1以外的其它数字,则报错并提示重新输入
default:
printf("输入错误,请重新输入\n");
break;
}
} while (choose);
现在我们来看一下运行效果:
那现在我们也成功实现了咱们的功能二,重复运行。到这里游戏的一个整体框架就搭建完毕了,接下来就要开始编写游戏的内容了。
想要实现三子棋的游戏,我们在前面也说过,要有棋盘,要能下棋。那现在我们想要去实现这两个功能,又应该如何实现呢?
这时候有朋友就会说了,棋盘嘛,我们可以通过printf将其打印出来,下棋嘛,通过scanf输入进去就可以了。
那现在问题来了,我们输入的内容要存放在哪里,才能让这些内容与棋盘对应?下面我们来回想一下我们学过的哪些知识点能将输入的数据进行像九宫格一样的摆放?
我相信现在肯定有朋友脱口而出——二维数组。没错我们在二维数组的篇章曾提到过二维数组是由两个下标构成的,我们刚开始的理解是横纵坐标,经过后来的学习我们搞清楚了,这并不是横纵坐标,而是分区数量与分区大小。但是在这里我们可以将这两个下标继续理解成横纵坐标来帮助我们完成三子棋的实现:
从图中我们可以看到,如果要获得胜利的话就有下面几种情况:
这也就是说,我们先需要创建一个二维数组,数组的分区数量为3,分区的大小为3,即char arr[3][3];
,然后通过输入的值来进行下棋,这个值应该是棋盘对应的横纵坐标,并通过输入的3个值来判断是否能获得胜利。
现在我们要实现游戏的话就需要按照上述思路从生成棋盘->定义数组存储棋子信息->判断游戏胜利的这么一个编写流程去完成,下面我们一步一步去进行编写;
棋盘的生成我先先要简单理解一下生成原理,有朋友可能会说,不需要,我只用printf打印出来就可以了,代码如下:
printf(" | | \n");
printf("--- --- ---\n");
printf(" | | \n");
printf("--- --- ---\n");
printf(" | | \n");
通过这个代码就能将棋盘打印出来了呀:
那下面问题来了,如果只是这样打印我们如何将棋子的信息给输入进去呢?考虑到这个问题,我们接下来是不是要先解决棋子信息的存储问题啊,没错,如果现在棋子信息都没有的话,我们棋盘及时打印出来了也无法将棋子放入棋盘,下面我们来看看这个棋子信息该如何解决;
在前面的分析中,我们考虑到了要定义一个二维数组来存放棋子信息,那是不是定义一个char board[3][3]
就可以了呢?
现在我们的思考方式不能被三子棋这个内容给局限起来,如果说我想下五子棋我是不是还要定义一个char board[5][5]
的二维数组呢?
那我希望我的五子棋棋盘大一点,我是不是要定义一个char board[10][10]
的二维数组呢?
从上面的分析我们可以看到,我们需要改变的其实就是棋盘的横纵坐标,所以我们不妨将棋盘的横纵坐标用一个可修改的标识符来表示,在前面的学习中,我们知道数组的下标只能是常量,不能是变量,这里我们通过#define来定义两个表示符常量是不是就能很好的解决了呢?
接下来我们可以在游戏模块的头文件中来定义行和列的两个表示符row——行
和col——列
,然后在主模块中进行引用:
在game.h中定义好行和列的标识符;
在test.c中通过#include进行引用,此时注意,因为game.h是我们自己定义的,所以需要用双引号引用,C语言自己提供的才是尖括号引用。
像这样定义有什么好处呢?
如果以后我们需要修改与行和列的数值,我们只需要在头文件中更改即可,就不需要在代码中将行和列的数值一个一个修改过去,这样大大提高了编码效率。
现在数组已经被我们创建好了,那我们要开始打印棋盘了,但是我们在打印棋盘前,我希望打印出来的效果如下:
| |
--- --- ---
| |
--- --- ---
| |
但是此时如果我们直接打印的话出来的效果就会是:
0 | 0 | 0
--- --- ---
0 | 0 | 0
--- --- ---
0 | 0 | 0
既然这样,那我们首先是不是应该先将棋盘初识化呀,所以这里我们需要在游戏模块中先定义一个初始化程序,并在头文件中声明,最后再到主程序模块中调用就可以了。
我们初始化的内容是什么呢?
现在数组里放入的元素都是为0,那我们就需要将里面的元素全部初始化为‘ ’就可以了,后面在下棋时我们可以将这些空格替换成我们想要的字符,下面编写代码:
//定义初始化棋盘函数
void Initboard(char board[ROW][COL], int row, int col)
{
int i = 0;//行下标
int j = 0;//列下标
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
这样我们就完成了我们的初始化;
棋盘初始化完成后,我们就要开始打印棋盘内容了,这个棋盘内容可不能像前面用几个printf就解决了,我们希望的是通过修改我们的行和列,棋盘也能跟着修改,所以在打印的时候我们就需要借助循环来实现。
下面我们就来探讨一下打印的具体内容是什么?
在前面探讨中我们有分析过,我们要打印的棋盘是一个九宫格,而且这个棋盘还有下棋的功能,也就是说,我们仅仅打印九宫格是不够的,我们还需要在九宫格内插入字符,所以准确来说我们要打印的完整内容应该是:
%c | %c | %c
----|----|----
%c | %c | %c
----|----|----
%c | %c | %c
我们又该如何打印?
我们有几种方式可供选择——1.一将内容行一行的打印出来,2.将内容按行和列的方式打印出来。
下面我们先来探讨第一种打印方式;
1.一将内容行一行的打印出来
我们要将内容一行一行打印出来需要打印哪些东西呢?
%c | %c | %c ————第1行
----|----|----————第2行
%c | %c | %c ————第3行
----|----|----————第4行
%c | %c | %c ————第5行
从这个图像展示中我们可以得到以下几条信息:
%c
与|
的交替,有两行是----
与|
的交替;----
与|
组成的行起到了一个分割线的作用,将 %c
与|
组成的行分割开;了解了这些信息,那我们只需要先打印一行 %c | %c | %c
再打印一行----|----|----
然后循环两次是不是就可以了,下面我们就开始编写代码:
打印效果如下所示:
这时我们可以看到分割线此时是不是多了一行呀,所以我们需要在打印分割线的时候给它加一个条件:
//定义打印棋盘函数
void PrintBoard(char board[ROW][COL], int row, int col)
{
int i = 0;//行下标
for (i = 0; i < row; i++)
{
//打印一行的内容
printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
//打印分割线
if (i < row - 1)
printf("---|---|---\n");
}
}
现在我们再来看一下打印效果:
现在就是我们需要的棋盘内容了,我们需要下棋的话只需要将数组的元素进行替换就可以了。
但是这种编写方式不是特别好,因为如果我们想修改棋盘大小时,列是固定的,我们要进行修改只能手动修改打印内容,这个我就不做演示了,有兴趣的朋友可以自己去试一下,将ROW和COL的值改成其它值,去体会一下。
下面我们来探讨第二种方式:
2.将内容按行和列的方式打印出来
在编写前,我们还是先分析一下我们要打印的内容,继续看下面这个图像展示:
%c | %c | %c ————第1行
----|----|----————第2行
%c | %c | %c ————第3行
----|----|----————第4行
%c | %c | %c ————第5行
前面我们按照一行一行的内容来分析了,现在我们换一种观察方式,将它的行和列组合起来一起分析,我们可以得到以下信息:
%c
加上|
这个内容组成;----
加上|
这个内容组成; %c
的个数与数组列数相同,而|
的个数比列数少1;----
的个数与数组列数相同,而|
的个数比列数少1; %c
加上|
这个内容组成的行数与数组行数相同,而由----
加上|
这个内容组成行数比数组行数少1;在了解到上面这些信息后,那我们就可以对打印内容进行一些调整,我们分成四块内容进行打印,打印方式如下:
%c
和|
这两个内容,在完成一行打印后换行打印;----
和|
这两个内容,在完成一行打印后换行打印;|
这个符号会比数组列数少1,所以我们在打印时需要加入打印条件;----
加上|
这个内容组成行数比数组行数少1,所以我们在打印分割线这一行时也需要加入打印条件;到这里我们的思路就已经很清晰了,接下来就是需要编写代码了:
//定义打印棋盘函数
void PrintBoard(char board[ROW][COL], int row, int col)
{
int i = 0;//行下标
int j = 0;//列下标
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
//打印一行的内容
printf(" %c ", board[i][j]);
if (j < col - 1)
{
printf("|");
}
}
//打印完一行的内容,进行换行
printf("\n");
//打印分割线
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
//打印完一行的内容,进行换行
printf("\n");
}
}
}
这里有一点我需要提一下,博客中展示的分割线是由----
和|
组成的,但是在编码的过程中经测试发现,代码中只需要---
和|
就可以了,会导致这种差异的原因是因为在博客中%和c是两个字符,而在编译器中%c
是一个字符,所以希望大家能够理解一下,博客中为了排版好看,我用了四个-
,vs中只需要用到三个-
。
下面我们来看一下打印效果:
我们可以看到,此时棋盘很好的打印了出来,下面我们来将行和列改为5,再来看一下打印效果:
棋盘此时也按我们的要求很好地打印了出来;
这里我之所以将引用头文件这里圈出来是因为<stdio.h>
这个头文件我们不仅在test17.c
这个项目中需要引用,在game.c
这个项目中同样也需要引用,所以我们直接在头文件<game.h>
中引用一下,这样我们在test17.c
和game.c
中只需要引用我们自己编写的"game.h"
就可以了。
同理,其它需要引用的头文件都可以在"game.h"
中引用一下,这样我们在test17.c
和game.c
中就可以正常使用了。
到这里我们也完成了打印棋盘与生成棋子的功能,接下来我们就要开始下棋了;
老规矩,在实现这个功能前,我们还是需要先理清思路,我们首先要明确几个点:
在明确了以上6点后,我们将开始一步一步来解决我们的问题;
既然要编写玩家下棋的功能,我们还是一样要在游戏模块game.c
中定义相关的函数PlayerMove
,并在game.h
中声明,最后再到主程序模块test17.c
中调用。
下面开始来一步一步解决问题:
首先,我们下棋的动作是重复执行的,包括下完棋后的打印,也是重复执行的,也就是说,在主函数模块我们需要使用循环语句来完成;
其次,游戏功能实现部分,我们需要给玩家提示,这里通过printf
来实现,坐标信息的接收通过scanf
来实现,信息的存储通过定义坐标变量int i = 0, j = 0;
来实现;
随后,在输入完坐标信息后,我们需要先判断坐标的合理性,不合理,则提示玩家坐标不合理,请重新输入,所以这里需要有一个循环语句来实现不合理时的重复输入;
最后,坐标信息合理后,我们需要判断该坐标是否为空,是空则将棋子放入该坐标并结束循环,非空则提示玩家该坐标已被占用,请重新输入;
经过上述步骤,那我们的思路就很清晰了,下面开始编写代码:
//定义玩家下棋函数
void PlayerMove(char board[ROW][COL], int row, int col)
{
//定义坐标变量来存放坐标信息
int i = 0, j = 0;
//提示轮到玩家的回合
printf("玩家走>:\n");
while (1)//如果坐标输入不合理,需要重新输入
{
//提示玩家输入坐标信息
printf("请输入坐标信息>:");
//通过输入函数接收坐标信息
scanf("%d%d", &i, &j);
//判断坐标的合理性
if (i >= i && i <= row && j >= 1 && j <= col)
{
//判断坐标是否被占用
if (board[i - 1][j - 1] == ' ')
{
//如果坐标未被占用,这将*放入该坐标
board[i - 1][j - 1] = '*';
//下完棋子,跳出循环
break;
}
else
{
//如果坐标被占用,则提示用户坐标被占用
printf("该坐标以被占用请重新输入:>\n");
}
}
else
{
printf("坐标不合理,请重新输入:>\n");
}
}
}
我们来测试一下:
这里我们可以看到,程序能很好的运行。那玩家下棋的代码就编写完成了,接下来我们要开始编写电脑下棋的部分了;
电脑部分下棋相比于玩家部分会有一些差异:
首先,电脑部分的下棋需要通过随机数来完成,也就是我们在猜数字游戏中使用过的rand
函数;
其次,我们还需要借助起点生成器srand
函数以及时间戳time
函数来实现坐标的随机化;
然后,我们可以通过将生成的数字与横纵坐标分别取模并存放在横纵坐标变量里来使坐标合理化;
最后,我们也同样需要判断坐标点是否被占用,如果被占用则重新生成,如果未被占用则将棋子下入棋盘;
现在我们的思路也有了,我们要开始编写函数了,顺序一样,先定义,再声明,最后调用,函数部分代码如下:
//定义电脑下棋函数
void ComputerMove(char board[ROW][COL], int row, int col)
{
//定义下标变量存放生成下标值
int i = 0, j = 0;
//提示轮到电脑下棋
printf("电脑回合:>\n");
while (1)
{
//将随机数与横坐标取模并存放在横坐标变量i中
i = rand() % row;
//将随机数与横坐标取模并存放在纵坐标变量j中
j = rand() % col;
//判断坐标是否被占用
if (board[i][j] == ' ')
{
//未被占用,则将棋子放入棋盘
board[i][j] = '#';
//下完棋后跳出循环
break;
}
}
}
我们测试一下:
我们可以看到,现在已经实现了人机对战了,下面就只剩最后一个功能,那我们的三子棋就全部完成了;
胜负判断的条件还是比较容易实现的,我们可以通过判断3个横纵坐标的棋子是否相同来决出胜负,判断的结果就会出现以下几种情况:
这里程序如何运行,我们可以通过胜负判断函数的返回值来决定:
判断胜负函数代码如下:
//定义判断棋盘是否已满函数
int IsFull(char board[ROW][COL], int row, int col)
{
int i = 0, j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
//如果棋盘中存在空格,则棋盘未满
if (board[i][j] == ' ')
return 0;
}
}
//如果棋盘中没有空格,则棋盘已满
return 1;
}
//定义判断胜负函数
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0;
//判断获胜
for (i = 0; i < row; i++)
{
//横坐标相等的元素
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
{
return board[i][0];
}
}
for (i = 0; i < row; i++)
{
//纵坐标相等的元素
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
{
return board[0][i];
}
}
//横纵坐标相等
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ')
{
return board[0][0];
}
//横纵坐标分别为0/1/2
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2] != ' ')
{
return board[0][2];
}
//判断是否平局
if (1 == IsFull(board, ROW, COL))
{
//棋盘已满则为平局,返回'D'
return 'D';
}
//若棋盘未满,则游戏继续,返回'C'
return 'C';
}
下面我们来测试一下游戏运行:
可以看到,游戏能够按照我们所期望的那样正常运行,经过我的多次测试,每一种情况都是能够正常运行的。
这个游戏是一个综合性很强的游戏,游戏的每个功能我们都能通过不同的方式去实现,但是就目前所学来看,我们在这次编写的过程中运用到了以下知识点:
除了以上知识点,还有我们在编写猜数字游戏时探讨过的随机数的生成以及时间戳的相关知识点,为了编写完这个游戏,上述的知识点都是需要我们牢牢掌握的,如果在编写的过程中有遇到困难,大家可以根据相关知识点进行针对性的复习哦!
(PS:大家感兴趣的可以尝试在胜负判断的这个功能内部尝试着不用额外编写IsFull函数而使用循环嵌套以及goto语句去编写不同的结果,以此来复习一下相关的知识点,对咱们的运行结果不会产生影响,但是可以给我们提供新的编写思路,功能的实现其实是有多种方式进行实现的,大家可以多多尝试)
咱们本章的内容到这里就全部结束了,希望这些内容能够帮助大家更好的理解和运用涉及到的这些相关知识,如果在阅读完本篇文章后,各位能够自己独立编写三子棋游戏的话,那就更完美了。接下来随着学习的深入,我会继续给大家分享我在学习过程中的感受。如果各位喜欢博主的内容,还请给博主的文章点个赞支持一下,有需要的朋友也可以收藏起来反复观看哦!感谢各位的翻阅,咱们下一篇见。