实现从 0 到 1 的双陆棋游戏!
Hello,好久不见宝子们,今天来给大家更一个五子棋的程序~
我们今天要讲的内容如下:
文章目录
- 1.五子棋游戏介绍
- 1.1 游戏玩法介绍:
- 2.准备工作
- 2.1 具体操作流程
- 3.游戏程序主函数
- 4.初始化棋盘
- 4.1.定义宏变量
- 4.2 初始化棋盘
- 5.打印棋盘
- 5.1 改进棋盘
- 6.打印玩家下棋的界面
- 7.打印电脑下棋的界面
- 7.1详解随机生成函数
- 7.1.1 rand函数介绍
- 7.1.2 srand函数介绍
- 7.1.3 time函数介绍
- 7.1.4 设置随机数的范围
- 7.2 基于电脑下棋界面的算法实现
- 8.判断输赢的界面
- 8.1 按所在行判断输赢
- 8.1.1 代码实现
- 8.2 按所在列判断输赢
- 8.2.2 代码实现
- 8.3 按棋盘的正对角线判断输赢
- 8.3.2 代码实现
- 8.4 按棋盘的副对角线判断输赢
- 8.4.2 代码实现
- 8.5 判断棋盘是否下满了
- 8.5.1 代码实现
- 9.最终运行效果展示
- 10.五子棋游戏源代码展示
1.五子棋游戏介绍
1.1 游戏玩法介绍:
五子棋: 又称连珠、连五子、五目、五珠等,是一种两人对弈的棋类游戏。它使用一个棋盘,通常是15×15的方格,并使用黑白两种颜色的棋子。游戏的目标是先在棋盘上形成连续的五个自己颜色的棋子,可以是横、竖、斜线方向的连线。 与围棋类似,五子棋也是一种战略性的游戏,需要玩家在防守对手的同时,寻找机会形成自己的连珠。由于规则简单,易于上手,五子棋在全球范围内都有广泛的普及和流行。
2.准备工作
首先,在写这个五子棋程序的时候,我们得先在VS中创建3个文件,这三个文件的作用如下图所示:
从上图可以知道: 我们整个游戏的测试都是放在
test.c
上面去的,所以我们的主函数也是在test.c
这个文件下写的。
2.1 具体操作流程
如下图:
我们只需在键盘中按下“
CTRL+SHIFT+A
”, 即可弹出上图的界面,然后在C++文件那栏点击以.c
创建两个源文件test.c
和game.c
,然后在头文件那栏以.h
创建头文件game.h
即可。
3.游戏程序主函数
从博主之前写的博客: 扫雷游戏增强版代码实现和详解(超详细~)
可以知道,在编写这类游戏项目的代码中,我们通常都会在test.c
文件写上这个主函数~
代码如下:
#include<stdio.h>
void menu() {
printf("*****************************************************\n");
printf("***************** 0. exit games *****************\n");
printf("***************** 1. play games *****************\n");
printf("*****************************************************\n");
}
int main() {
int input = 0;
do {
menu();
printf("请选择>:");
scanf("%d", &input);
if (input == 1) {
printf("玩游戏\n");
}
else if (input == 0) {
printf("退出游戏\n");
}
else {
printf("该数不在范围,请重新输入\n");
}
} while (input);
return 0;
}
vs运行结果如下
从上图可以看出,玩家可以通过选择
1
或0
玩游戏或者退出游戏,而如果玩家输入的数不在**0
和1
范围内,我们会让他重新输入数字,直到玩家输入0
,该程序就会结束。因为在C语言中,0
为假,非0
为真,因此当我们输入0的时候,它将退出do…while
循环。**
4.初始化棋盘
4.1.定义宏变量
在我们初始化棋盘之前,我们要先在game.h头文件先对行和列进行宏变量操作,如下:
这是因为后续如果我们要改变棋盘的行数和列数,直接在头文件这里修改宏变量就行,就不需要在game.c
实现函数的源文件上一个一个修改行数和列数。
同时,为了方便起见,我们顺便将库函数头文件全部放进game.h
的头文件中,然后分别在test.c
和game.c
源文件中加上#include"game.h
这句话,这样的好处是:
如果之后要用到其他库函数,难免会用到其他头文件,那如果说我们把函数头文件放到game.h
中,就不需要在test.c
和game.c
源文件中重复编写头文件。
4.2 初始化棋盘
如下图所示:
从上两幅图可以得知:
当用户选择1
玩游戏,main
函数将调用game
函数,然后在test.c
的game
函数内部,先将棋盘全部初始化为0。
然后调用InitBoard
函数,我们先在game.h
中声明InitBoard
函数,然后在game.c
实现这个函数功能,将棋盘的每个位置全都初始化为空格。
5.打印棋盘
首先,我们最终想要打印出的效果如下图~
这里可能很多同学都没思路,不知道怎么去打印这个棋盘,没事,这里博主会提供思路~
如下图所示:
分析: 从上图,我们可以把打印数据和打印分割线分别看为一组。然后细心观察的话,我们发现打印数据那行,每打印3
个空格,就带有一个|
。
而因为我们之前是将棋盘元素全部初始化为空格,因此我们这里打印可以在该棋盘元素左右各增添一个空格。
另外,我们需要注意的是: 这里的分割线一共是打印4
行。
好了,分析了那么多,我们直接上代码~
代码如下:
void Displayboard(char board[ROW][COL], int row, int col) {
int i = 0;
for (i = 0; i < row; i++) {
printf(" %c | %c | %c | %c | %c \n", board[i][0], board[i][1], board[i][2], board[i][3], board[i][4]);
if (i < row - 1) {
printf("---|---|---|---|---\n");
}
}
}
我们来看一下VS运行效果图吧~
虽然代码能打印出这个效果出来,但我们认为这个代码还是有缺陷的,为什么呢?
看下图:
这里我们假设将game.h
头文件中的ROW
行和COL
列分别改为10
,然后运行程序,然后发现每行打印的棋盘元素不足10
个。
还是停留在原先一行打印五个棋盘元素这种情况。因此这个代码局限性很大,因此我们要对其进行改进。
5.1 改进棋盘
如果我们要根据行和列的数据,来打印相应大小的棋盘,这个我们该怎么想呢?
这里我们根据之前分析的图,再重新进行加工了一下。
如图:
相信同学们看了博主分析的这个图,会对更加理解这个打印棋盘的原理是什么。
代码如下:
void Displayboard(char board[ROW][COL], int row, int col) {
int i = 0;
for (i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
printf(" %c ", board[i][j]);
if (j < col - 1) {
printf("|");
}
}
printf("\n");
if (i < row - 1) {
for (int j = 0; j < col; j++) {
printf("---");
if (j < col - 1) {
printf("|");
}
}
printf("\n");
}
}
}
vs运行效果图如下:
我们通过VS运行结果,发现运行的结果都确实跟我们预期打印的效果一模一样,无论game.h
头文件中的ROW
和COL
的值怎么变化,它都会根据它的行和列的值打印相应的分割线和棋盘元素,从而控制棋盘的大小。
6.打印玩家下棋的界面
这是我们最终想打印的效果,如图所示:
那我们该怎么打印出这个效果呢,接下来博主给你细细道来~
我们根据预期想打印的效果,经过分析,可以把这个图画出来,如下:
看到这里,想必很多同学都会深刻理解这个图的大部分内容。
但可能还有部分同学会有疑问,数组的下标不是从0
开始吗?为什么玩家输入的坐标为3
,3
,然后对应的棋盘数组元素为board[2][2]
呢?
这样设计代码逻辑有什么意义呢?
这是因为很多玩家并不是程序员啊,他们可不关心数组的下标是否从0
开始,只会凭正常人的逻辑来输入坐标,并根据他们所输入坐标的值达到他们想要输出棋盘的效果哈。
好,那接下来我们就给大家放代码,相信同学们通过我们上面的解析和代码,能够彻底理解这个逻辑。
代码如下:
void playerMove(char board[ROW][COL], int row, int col) {
int x = 0;
int y = 0;
printf("玩家下棋\n");
while (1) {
printf("请输入坐标,中间输入空格:");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
if (board[x - 1][y - 1] == ' ') {
board[x - 1][y - 1] = '*';
break;
}
else {
printf("该坐标已被占用,重新输入\n");
}
}
else {
printf("坐标非法,请重新输入\n");
}
}
}
VS运行效果展示:
这里需要注意的是: 在test.c
源文件中,我们要将playerMove
函数和Displayboard
分别放入while
语句中。这是为什么呢?
来看下图:
如果说我们把这两个函数不放到while
语句中,当玩家输入了一次坐标,就回到主调main
函数里面,又因为1
为真,因此不会退出do…while
循环,会重新回到上面的menu
函数重新进行判断,这里就相当于已经玩了一次游戏,问玩家还要不要玩一次,这显然是不合理的。 因为玩家刚刚的棋局还没赢或输或平局,就直接结束游戏,这显然不符合五子棋游戏逻辑。
因此,我们要将playerMove
和Displayboard
这两个函数放到while
循环里面。
7.打印电脑下棋的界面
这是我们最终想要打印出来的效果,如下图所示:
我们发现,当玩家在3
,3
坐标处下棋了,电脑就随机在棋盘元素为空格的位置下棋。用#
号来填充空格,那我们该如何实现这个电脑随机下棋的算法呢?别急,博主接下来带着你们来分析嘿嘿!
要想让电脑生成随机下棋的坐标,前提是得让它们的坐标生成随机数啊,那接下来,博主给大家详解3
种随机生成数的函数。
7.1详解随机生成函数
7.1.1 rand函数介绍
我们知道,C语言提供了一个函数叫rand
,这个函数是可以生成随机数的。
具体函数介绍可以看下面这两幅图:
这里的rand
函数会返回一个伪随机数,这个随机数的范围是在0
~RAND_MAX
之间
这个RAND~MAX
的大小是依赖编译器上实现的,但是大部分都编译器上都是32767
。
并且rand
函数的使用需要包含一个头文件是:#inlcude <stdlib.h>
好,那这里我们就测试一下rand函数,这里多调用几次,看看效果怎么样~
这里我们会以动图的方法来演示:
通过仔细观察动图,我们发现VS两次运行中出现的数字都一模一样,这就说明有点问题了。
如果我们深入了解,不难发现,其实rand
函数生成的随机数是伪随机的,那伪随机数不是真正的随机数,是通过某种算法生成的随机数。真正的随机数是无法预测下一个值是多少的,所以我们推断,rand
函数是对一个叫种子的基准值进行运算生成的随机数。
之所以前面每次运行程序产生的随机数序列是一样,那是因为rand
函数生成随机数的默认种子是1
。如果要生成不同的随机数,就要让种子变化。
7.1.2 srand函数介绍
同样地,C语言又提供了一个函数教
srand
,它是用来初始化随机数的生成器的,它的函数原型如下:void srand (unsigned int seed);
具体的函数介绍如下:
这里我们简单介绍一下:就是程序在调用rand
函数之前要先调用srand
函数,通过srand
函数的参数seed
来设置生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。
所以: 如果说给srand
的种子是随机的,rand
就能生成随机数;在生成随机数的时候又需要一个随机数,这就矛盾了。
我们这里就把srand
函数中的参数seed
改为2
,看看那个结果是否会变化。
如下图:
事实上: 当我们把seed
种子数改为其他的数字,rand
函数所生成的随机数也会改变的。
7.1.3 time函数介绍
在程序中我们一般是使用程序运行的时间作为种子的,因为时间时刻在发生变化的。
那在C语言中有一个函数叫time
,就可以获得这个时间,time
函数原型如下:
time_t time (time_t* timer);
具体的函数介绍如下:
我们这就简单讲一下吧:
time
函数会返回当前的日历时间,其实返回的是1970
年1月1日0时0分0秒
到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t
类型的,time_t
类型本质上其实就是32
位或者64
位的整型类型。time
函数的参数timer
如果是非NULL
的指针的话,函数也会将这个返回的差值放在timer
指向的内存中带回去。- 如果
timer
是NULL
,就只返回这个时间点差值。time
函数返回的这个时间差也被叫做: 时间戳
需要注意的是: 使用time函数的时候务必加上头文件:#include<time.h>
但如果只是让
time
函数返回时间戳,我们就可以这样写:time(NULL);//调用time函数返回时间戳,这里接受返回值
那我们就可以让生成随机数的代码改写成如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
//使用time函数的返回值设置种子
//因为srand的参数是unsigned int类型,我们将time函数的返回值强制类型转换
srand((unsigned int)time(NULL));
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
return 0;
}
至于说它加上time
函数,它每一次的运行结果是否相同 ,这个我们以动图的形式给同学们展示一下~
如下所示:
细心观察这个动图的话,我们发现VS这两次的运行结果都是不一样的。
另外,我们再提一下:srand
函数是不需要频繁调用的,一次运行的程序中调用一次就够了。
7.1.4 设置随机数的范围
1.如果我们要生成0~99
之间的随机数,方法如下:
rand()%100;//余数的范围是0~99
2.如果要生成1~100
之间的随机数,方法如下:
rand()%100+1;//%100的余数是0~99,0~99的数字+1,范围是1~100
3.如果要生成100~200的随机数,方法如下:
1.100 + rand()%(200-100+1)
2 //余数的范围是0~100,加100后就是100 ~200。
总结: 如果要生成a~b
的随机数,方法如下:
a+rand()%(b-a+1)
好,讲到这里,我们就回到一开始的问题,我们不是要求电脑随机数吗?
那首先我们横坐标要先得到的是0~row-1
的数字,纵坐标得到的是0~col-1
的数字。这是因为数组的下标是从0
开始的,所以我们设计电脑下棋算法的时候,不能让他从1
开始遍历。
我们根据上面的分析,可以把这幅图给画出来,如下:
相信同学们看了博主分析和做的图,应该能够很好地理解随机数函数的用法以及设计电脑算法的原理。
7.2 基于电脑下棋界面的算法实现
如下图所示:
这里我们还需要一下: 就是要在game.h
引入两个头文件分别是#include <stdlib.h>
和#include<time.h>
,然后在test.c
源文件中加上srand((unsigned int)time(NULL));
这行代码,能确保在game.c
中用那个随机生成函数rand
才能够生成真正的随机数。
同时在test.c
源文件中还要把ComputerMove
和Displayboard
这两个函数分别放在while
循环里面,不然当玩家下了一次棋,电脑下了一次棋,这个游戏就提前终止了,这显然是不符合游戏逻辑的。
代码实现:
void ComputerMove(char board[ROW][COL], int row, int col) {
int x = 0;
int y = 0;
printf("电脑下棋\n");
while (1) {
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ') {
board[x][y] = '#';
break;
}
}
}
VS运行效果图
从运行结果来看: 我们发现当玩家输入
3
3
坐标时,此时在棋盘元素2
2
的位置由一个空格变为一个*
。
当玩家输出一个*
的时候,此时电脑就随机在棋盘元素的某个位置下棋。也是当棋盘元素为空格的情况,才将那个点的坐标由空格变为一个#
。
8.判断输赢的界面
在前面,我们已经写了很多行代码了,但是我们发现这个代码没有判断输赢的界面,这会导致什么问题呢?如下图,我们来看一下~
从图中:我们可以直观地发现,当玩家和电脑的输出的元素已经把整个棋盘给塞满了,那么此时游戏应该判断输赢,而不应该卡在电脑下棋的界面,所以这显然也是不符合游戏逻辑的。
因此我们要写一个判断棋盘输赢的算法出来。
那我们该如何设计输赢的算法呢?
我们可以这么想,如下图:
相信大家看了我们这个图,或许会有点思路了,但可能还是不太清楚具体如何实现这个算法,那我们接下来就讲一下代码实现吧~
如下图所示:
代码解读: 我们可以现在test.c
源文件中的game
函数内部创建一个ret
的变量,然后在while
循环内部,
当玩家或电脑落一次子,就调用一次那个Iswin
函数,这个函数专门是根据本场棋局玩家和电脑的情况,返回相对应的字符。然后当Iswin
返回的是字符'C'
,表明棋局还没结束,则不会退出while
循环,玩家和电脑可以继续下棋。
而当Iswin
返回到不是字符'C'
,则表明本次棋局已经分出胜负了,那么就要用break
语句退出while
循环,然后我们用几个if
语句来进行判断即可。
那可能讲到这里,或许有同学已经豁然开朗,但还是不太知道怎么去实现Iswin
函数的游戏代码呢?那接下来博主会分四种情况来进行分析。
8.1 按所在行判断输赢
这里我们画了个图,方便同学们理解,如图所示:
这里我们主要是针对棋局的行来判断输赢,如果所在那行全是同一字符,那么就能判断玩家赢还是电脑赢。
需要注意的是:我们判断某一行的输赢,还要让这行的棋盘元素都不为空格,这是因为空格字符是棋盘元素一开始初始化就有的,因此不能拿它作为判断行输赢的依据。
8.1.1 代码实现
那根据前面的分析,以及二维数组相关的知识,我们也能顺理成章地把这个代码给写出来。
代码如下:
char Iswin(char board[ROW][COL], int row, int col) {
for (int i = 0; i < row; i++) {
int count = 0;
for (int j = 0; j < col; j++) {
if (board[i][0] == board[i][j] && board[i][j] != ' ') {
count++;
}
}
if (count == 5) {
return board[i][0];
}
}
这里我们主要是定义了一个
count
变量,用它来枚举所在行是否为相同元素,且不为空格,如果在内层for
循环中count
的值已经自增到5
时,那么就直接返回这行中的任意一个棋盘元素回去。
8.2 按所在列判断输赢
这里我们画了个图,方便同学们理解,如图所示:
这里我们主要是针对棋局的列来判断输赢,如果所在那列全是同一字符,并且所在那列都不是空白字符,那么就能判断玩家赢还是电脑赢。
8.2.2 代码实现
那根据前面的分析,以及二维数组相关的知识,我们也能顺理成章地把这个代码给写出来。
代码如下:
for (int i = 0; i < row; i++) {
int count = 0;
for (int j = 0; j < col; j++) {
if (board[0][i] == board[j][i] && board[j][i] != ' ') {
count++;
}
}
if (count == 5) {
return board[0][i];
}
}
这里我们主要是定义了一个
count
变量,用它来枚举所在那列是否为相同元素,且不为空格,如果在内层for
循环中count
的值已经自增到5
时,那么就直接返回这列中上一篇: ArcGIS]使用 DEM 进行水文分析:流向/流量等--ArcGIS 示例
下一篇: 使用 Go 语言进行Web开发实战教程(第15节):手把手教你通过 ResponseWriter 创建HTTP响应
推荐阅读