目录
前言
一、三子棋实现的逻辑
二、三子棋的实现
2.1文件的创建添加
2.2 test文件基本逻辑
2.2.1菜单的实现
2.2.2菜单的选择
2.2.3game函数棋盘的实现
2.3game.c文件的编写
2.3.1初始化函数的模块
2.3.2棋盘打印的模块
2.3.3实现棋盘界面的打印
2.3.4实现玩家下棋模块
2.3.5实现电脑下棋模块
2.3.6实现判断输赢(三个相连为例)
判断谁赢逻辑
is_win函数实现
总结
前言
我们理解并知道了数组等一些知识的使用后就可以适当进行自己的一些创作,本篇文章基于数组学习之后的实例练习,教你如何写一个三子棋的简单小游戏。
环境依然是基于VS2022的集成开发环境,项目创建可以参考之前的文章—创建空项目。
一、三子棋实现的逻辑
我们通过test.c文件来编写逻辑的实现,玩家怎么下棋,电脑怎么下棋,都在这里面写,之后game.h和game.c来编写游戏的实现,通过测试的逻辑来调用游戏的实现。
二、三子棋的实现
2.1文件的创建添加
我们要先分别创建这三个文件,方便后期的代码的编写。这里就不给出怎么创建了,就是添加文件,,h放进头文件里面,.c放进源文件里面进行实现。
由于VS2022对于一些函数有一些安全问题,会导致写出来的代码与在其它编译器上的不一样,所以会在开始define一下这个问题,这样在其它编译器上也可以使用这些代码。接下来我们开始编写代码。
2.2 test文件基本逻辑
当我们要玩游戏的时候,肯定是需要游戏一直进行的,所以游戏逻辑是当达到某个条件后游戏继续或者停止(代表你输了或者赢了),这里使用的是do-while循环,如下图。
这里先写出一个大致框架,通过主函数来调用test函数(游戏实现的逻辑)。
2.2.1菜单的实现
我们知道,一个游戏必须有一个菜单,来实现游戏的开始和退出,就像之前的猜数字小程序一样,这里用比较简陋的菜单来实现(可以自己设计自己想要的样式)。
通过一个函数menu来实现菜单的创建,而这个函数在test函数里的do-while循环里面完成。
菜单里面说明了1.play(游戏开始),0.eixt游戏结束,玩家通过选择这两个来实现游戏的游玩和节数。通过定义一个input临时变量来接收选择的是几。
2.2.2菜单的选择
当我们的input接收了值后,要进行判断游戏的执行和结束,所以在这里用上switch-case语句来完成。
当我们选择 1 的时候,就会进入game函数里面,实现游戏,选择 0 就会退出游戏,其它数字就会重新选择。而我们要保证do-while循环的运行和结束,这里我们通过while里面的条件进行限制,里面传入input就可以实现这一功能,当input传入0的时候,就判断为假,循环就结束,当传入为非0的数字的时候,程序循环不会结束。
这里面case 1中的打印三子棋来代替game函数的实现(因为game函数没有编写和定义声明,先注释掉),先运行一下是否可以得到想要的效果,如下图所示:
选择1:
选择0:
选择其它数字:
这样测试一遍发现没有错误,这样基本的test逻辑就实现了。
2.2.3game函数棋盘的实现
我们知道三子棋的棋盘就是一个井字,总共3*3个格子:
要储存数据,就需要一个3*3的数组,这时候就可以利用学过的二维数组来实现这一棋盘 ,棋盘内每个格子的位置都是空格,因为是空数据,所以当棋盘(二维数组)创建后需要初始化一下棋盘,初始化后还需要打印出棋盘。
所以这里封装俩函数(尽管没有实现)来实现棋盘的初始化和打印,参数分别是棋盘的地址和行和列。但这样写有一点不足,就是以后如果我们想要扩大棋盘,那么这里就都要改,所以推荐一种方法,就是在game.h文件中规定行和列,这里大写:
当想用3的时候使用ROW和COL就可以了,由于这是头文件,所以需要在test文件上面包含一下这个头文件就可以使用这两个了。
这时候行列就可以写成这样了,直接调用这两个就可以,以后想改成大一些的棋盘只需要修改一下game.h里的数字就可以了。
2.3game.c文件的编写
2.3.1初始化函数的模块
我们要实现init_board这个函数,这个函数是属于游戏模块内的,所以对于这个函数的声明和实现分别在game.h和game.c中编写,函数先声明这个函数,声明的时候要告诉函数参数是什么,返回参数是什么,函数名是什么:
由于初始化函数传入了一个棋盘数组,还有行和列,所以传入的三个参数入上图所示,其中行和列传入的是形参,所以用小写表示,不要和ROW和COL冲突。
这样初始化函数的声明就完事了,接下来编写这个函数的实现代码,在game.c里编写代码的实现。
首先包含一下game.h的头文件,因为头文件中我们有很多东西是需要用的,包括ROW,COL等等。
通过遍历来实现行列都给上空格,因为有形参row,col,所以这里遍历的条件就是这俩,把每一个格子的数据全变成空格。这就是棋盘的初始化。
2.3.2棋盘打印的模块
这里又是和之前一样,首先先在头文件中声明函数,包括参数类型,参数和返回类型。
接下来开始在game.c文件中写函数的实现:
一行打印完后换行就可以实现每一行都打印出来。要记住printf需要头文件stdio.h才能使用,所以需要在game.c里面包含一下stdio.h头文件才可以。
这里有一个小技巧,因为game.c和test.c里都需要game.h,所以只要把stdio.h在game.h里包含就可以了。
2.3.3实现棋盘界面的打印
我们假如要实现下面的棋盘样式(这里拿3*3举例)
我们可以给它拆分成一个个这样的:
把这个打印三行三列,但是最后一组这个分割行不用打印,即可满足这个棋盘的样式。
这时候之前写的输出二维数组的行和列就没有用了,把这部分改成打印棋盘。
我们把打印棋盘这个模块里面的代码改成下面:
打印三组数据,每组数据打印两行,第一行打印数据和竖线,第二行打印横杠和竖杠,将这两行看为一组,这样就可以打印出来如图所示的内容,但是我们发现最后一行出现了横杠,与我们画的理想图形不怎么符合,所以最后一行让它不输出就可以,这时候就可以加上一个限制条件:
当i < row-1 的时候,才打印横杠,所以就输出了两行就不用输出了。所以输出就会与理想的一样非常的美观哈。我们可以通过改变以下初始化的数组里面放的数据来看一下是否正确。
上面有一个缺陷,就是当我们的行数和列数改变的时候,就会发现它打印的行数会发生变化,但是列数由于我们给出了打印的内容,所以是会出现三列,例如我们把ROW和COL变为10,来看看棋盘输出的数据是什么样:
这并不是我们想要的结果,我们想要的是一个10*10的棋盘,所以我们将代码改成下面这样的:
这样还是一组内输出两行,但是这两行都是用循环来进行输出,第一个数据先输出空格+字符+空格然后紧接着是竖杠,由于最后一个竖杠不要,所以这里有个限制条件就是竖杠只输出col-1个。
同理,第二行横线和竖杠一样。
注意每行都有一个换行符,要不然打印出来的就是一个连一起的东西。
这样这两行看为一组,因为这一组在一个大的循环里面,这一粗输出row行,就打印出来棋盘了。执行结果如下图(10*10):
是不是很美观哈哈。
2.3.4实现玩家下棋模块
我们将玩家下棋的这个模块命名为player_move( );我们知道玩家下棋还是下到这个棋盘里,而且还要知道下棋的坐标,通过坐标来进行下到哪里。
在game模块的里面写下这个模块函数。
要调用这个模块需要声明和定义,所以跟之前的操作一样,在头文件里面声明,在game.c文件里进行定义和逻辑的编写。
声明的时候还是要把相关内容都写上,返回类型,参数类型,还有形参。
这时候开始进行玩家下棋的逻辑实现,在game.c文件里面进行编写:
我们知道下棋的话需要知道坐标,根据坐标来进行下棋,所以这时候定义x和y的临时变量,来接收横纵坐标, 再确定坐标之前,还需要判断坐标的合法度,也就是是否在这个范围之内,通过一个if和与连接起来。同时希望选择坐标错误的时候这个过程可以重新进行,所以套用了一个while循环来实现这一功能。
这里有一个拐弯的地方,就是 玩家和我们对于坐标的看法是不同的,我们认为这是个二维数组,第一个元素的坐标是(0,0),而玩家认为是(1,1),所以这里需要注意。
所以玩家输入的坐标横纵都减去1就是这个数据对应的二维数组的下标,这里判断如果里面为空格(也就是初始化成功了)就把这个坐标的值变为*(相当于下棋了),当不符合这个条件的时候,说明这个坐标已经被占用了,就重新进行输入,只有下棋成功后才会跳出循环,这样就实现了玩家下棋的功能。
我们希望下完棋后程序还会打印出下完后的棋盘。所以在玩家下棋的模块后再调用一下打印棋盘的模块,这样就好了:
我们来测试一下:
这就没有任何问题了,这里由于是只是下一次棋,所以只会出现一次。如果想让玩家一直下棋,就可以把玩家下棋的两个函数外套入一个while循环就可以实现:
游戏中玩家是不能一直下棋的,所以玩家下完棋后是电脑下棋。
2.3.5实现电脑下棋模块
将这个模块命名为computer_move,电脑下棋还是下到数组中,行列都一样,这一系列操作和之前的一样,声明定义(这里省略了声明)。
直接从定义开始编写:
这里电脑下棋是随机生成坐标,只要坐标没有被占用就下棋,这里就用到了随机数的概念,我们知道随机数生成用rand函数。
rand( )函数
在调用rand()函数之前,可以使用srand()函数设置随机数种子,如果没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,即rand()函数进入的入口相同,则每次产生的随机数也会相同。所以srand种子可使用传入时间戳的方式来确定入口。这样就可以保证随机性,因为时间在一直变化。
在C语言中,rand()函数用于生成伪随机数,这些随机数的范围在0到RAND_MAX之间,其中RAND_MAX通常被定义为32767
这里随机生成一个数,这个数对行和列取模,那么就能保证它的坐标在范围之内。我们想要用rand( )函数,所以要先在test.c中调用srand(),这个调用一次就可以,参数写成time()函数,将time函数内设为空指针,因为时间在不断改变,这个time函数返回的值传入srand( ),这样就保证了随机数的值的随机性。
如果要使用time和rand函数,则需要包含一下头文件,这两个文件都用到了game.h,所以直接在这个里面定义就行:
接下来实现电脑下棋的逻辑:
首先先提示一下电脑下棋,随机生成x,y的坐标,如果这个坐标没有被占用,那么就输入#,代表电脑下的棋,如果被占用了,就用一个循环来重新随机生成一个坐标,重复操作,当真正的下棋成功后循环就结束。
现在就可以测试一下,运行:
这就可以实现这个玩家下一次,之后电脑再下一次的功能。
2.3.6实现判断输赢(三个相连为例)
判断谁赢逻辑
因为三子棋需要三个相连就赢了,所以需要再写一个赢了的逻辑,要不然程序一直运行,下满了还停不下来而且有了三个相连的棋子也不会宣布谁赢。所以就在电脑下棋和玩家下棋的循环里面判断一下谁赢了。由于玩家下完了玩家有可能会赢,电脑下完了电脑有可能会赢,所以分别在他们两个之后判断一下就可以了。
判断输赢的代码要告诉我们:电脑赢 ?玩家赢?平局?谁都没赢游戏继续?
我们命名这个模块为is_win( ),里面的参数还是这个棋盘还有行和列;
这里定义一个字符变量ret,用来接收谁赢了。玩家赢就返回 * ,电脑赢就返回#,平局就返回C,谁都没赢,游戏继续就返回D。
所以写成这样,如果等于D,游戏就继续进行,如果不等于D了就跳出循环,与其它的字符进行比对,来找出对应的字母来打印谁赢了。
is_win函数实现
我们要通过is_win来获取返回的字符,才能对比出谁赢,所以首先还是先在game.h中进行声明,在game.c里进行编写。
我们知道判断三个连续的相等就是要么是横着的三个、要么是竖着的三个、要么是对角线三个:
这里先判断前三行,要是同时满足第一个元素等于第二个和第三个,并且里面不是空格,就返回第一个的元素,因为之前定义的是玩家赢是 * ,电脑赢是 # ,而我们在下棋的时候,往棋盘里面下的就是这俩字符,所以就直接可以返回。
接下来判断3列,同样的原理,列是固定的,但行时不同的:
接下来判断对角线:
接下来还剩最后一种情况,就是是否这个数组都满了,所以这里用一个函数来判断一下,我们将这个函数命名为is_full,这里面的参数还是这个棋盘和行和列,如果慢了就返回C,就是平局。
我们来写这个函数:
直接写在上面的位置就可以,根据遍历来判断棋盘中是否还有空格,如果没有,就一直循环到结束,之后返回1,这就代表已经满了。
之后就可以用来判断,如果为1,返回值相等,就返回C这个字符,也就是平局,如果都没有,就返回D,游戏继续。
这里面is_full这个函数只是为了支持is_win函数的,只是在is_win函数内部使用。所以就没必要去在头文件中声明。也可以在is_full函数前面加上static,这样这个函数只能在这个文件查看和使用,外部就看不到了。
这样运行一下,分别测试这几个功能好不好使:
玩家赢:
电脑赢:
平局:
这样就完成了。
总结
要保证之前知识的连贯性,这样学着才比较轻松,只要把逻辑想明白,实现就很好实现了。
三子棋的源码我放进仓库中了三子棋: C语言实现三子棋 (gitee.com)
可以点开这个查看。