目录
一. 引言
三子棋作为许多人童年的回忆,是一款十分火爆的游戏。与五子棋类似,三子棋的游戏规则是参与游戏的两人依次下棋,若某一方在横排、竖列或斜线方向上将三个棋子连成一条线,即获胜,游戏结束。本文讲解了通过C语言来实现三子棋的方法,游戏双方即为电脑和人。
二. 实现过程
为了实现三子棋游戏的C语言编写,提高代码的通用性和可移植性,将整个三子棋项目分为三个小模块进行编写(两个源文件一个头文件)
(1)test.c 测试游戏的逻辑
(2)game.h 关于游戏相关的函数声明、符号声明、头文件的包含
(3)game.c 游戏过程相关的函数实现
2.1 test.c文件的实现
test.c是游戏的测试文件,即整个游戏的进行过程。当三子棋游戏程序开始运行时,首先要由玩家选择是否进行游戏,这里定义了一个输入变量input,玩家输入1则进行游戏,玩家输入0则停止游戏。
程序代码中的srand((unsigned int)time(NULL))是定义计算机时间链,为之后计算机随机生成落子点做准备。使用库函数srand要包含头文件stdlib.h,使用time函数要包含头文件time.h,相关代码在game.h文件中输入。
test.c代码:
#include "game.h"
void meun()
{
printf("****** 1.play ******");
printf("****** 0.exit ******");
}
int main()
{
meun();
srand((unsigned int)time(NULL));
printf("\n");
int input = 0;//输入判断(是否进行游戏)
do
{
printf("你是否要进行游戏: > ");
scanf("%d", &input);
switch (input)
{
case(1):
printf("游戏开始!\n");
game();//玩游戏函数
break;
case(0):
printf("游戏结束\n");
break;
default:
printf("输入数据错误,请重新输入: >\n ");
break;
}
} while (input != 0);
return 0;
}
2.2 game.h代码的实现
game.h文件是整个项目的头文件,要求实现函数声明、常量的定义以及头文件的包含。在头文件中定义棋盘的尺寸有以下优点:
(1)将棋盘行数ROW和列数COL设置为全局变量,避免了在函数值定义局部变量,代码简单,清晰易懂。
(2)可以跨文件调用常量。
(3)只需修改头文件中定义的ROW和COL值,就可以改变整个三子棋项目所有函数值用到的棋盘行列数,使游戏便于更改,提高了通用性,防止修改时产生bug。
game.h代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 4 //定义棋盘行数
#define COL 4 //定义棋盘列数
//游戏执行函数声明
void game();
2.3 game.c 代码的实现
game.c是整个游戏执行的过程文件。要包含以下几个函数部分:
(1)game函数,游戏执行的大逻辑,包含其他游戏执行相关函数。
(2)InitialBoard函数,初始化棋盘,将棋盘数组的每个元素均赋‘ ’(空格字符)。
(3)DisplayBoard函数,打印棋盘。
(4)PlayerMove函数,玩家走棋
(5)ComputerMove函数,电脑走棋
(6)IsWin函数,判断输赢,决定是否继续执行游戏程序
2.3.1 game函数的编写
当玩家开始游戏,首先执行game函数,game函数要首先包含棋盘初始化函数,将棋盘初始化之后马上打印棋盘,以增加游戏过程的可视化程度。随后玩家和电脑依次各走一步棋,这里设置为玩家先走棋,每当玩家或电脑走完一步棋后,要判断输赢,若有一方取胜或双方打成平局,本轮游戏结束。
void game()
{
srand((unsigned int)time(NULL));
char board[ROW][COL];
char ret;
InitialBoard(board, ROW, COL);//初始化棋盘数组
DisplayBoard(board, ROW, COL);//打印棋盘
while (1)
{
printf("玩家走棋: > \n");
PlayerMove(board, ROW, COL);//玩家下棋函数
DisplayBoard(board, ROW, COL);
//判断输赢函数,返回到ret
//1.玩家赢返回*
//2.电脑赢返回#
//3.平局返回E
//4.继续下棋返回C
ret = IsWin(board, ROW, COL);
if (ret == '*')
{
printf("玩家赢了!\n"); //玩家取胜,break终止循环
break;
}
else if (ret == '#')
{
printf("电脑赢了!\n");//电脑取胜,break终止循环
break;
}
else if (ret == 'E')
{
printf("平局\n");//平局取胜,break终止循环
break;
}
else
{
;
}
printf("电脑走棋: > \n");
ComputerMove(board, ROW, COL);//电脑下棋函数
DisplayBoard(board, ROW, COL);
ret = IsWin(board, ROW, COL);
if (ret == '*')
{
printf("玩家赢了!\n");
break;
}
else if (ret == '#')
{
printf("电脑赢了!\n");
break;
}
else if (ret == 'E')
{
printf("平局\n");
break;
}
else
{
;
}
}
}
2.3.2 初始化棋盘函数InitialBoard的编写
InitialBoard为初始化棋盘函数,这里通过循环的方式,将棋盘数组中的每个元素都赋值为 ‘ ’ (空格字符)。
void InitialBoard(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++)
{
board[i][j] = ' ';
}
}
}
2.3.3 打印棋盘函数DisplayBoard的编写
打印棋盘,不止要求要打印棋盘中的每个元素,还要打印横向和竖向上的分割线(如图2.1所示)。这里使用两个for循环进行嵌套,在除了第最后一行以外其他行的下方打印水平分割线,在除了最后一列每列的后面打印竖直分割线。
![](https://i-blog.csdnimg.cn/blog_migrate/e8b3961cbdf2b033b0bf1b4a2097c084.png)
代码:
void DisplayBoard(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++)
{
printf(" %c ", board[i][j]);//打印棋盘数组元素
if (j < col - 1) //判断上方打印的是否为每一行最后一个数组,判断是否打印竖直分割线
{
printf("|");
}
}
printf("\n");
for (j = 0; j < col; j++)
{
if (i < row - 1)
{
printf("___"); //判断上面的for是否打印的棋盘最后一行元素,是否打印水平分割线
}
else
{
printf(" "); //若为最后一行,以空格代替水平分割线,竖直分割线不受影响
}
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
}
}
2.3.4 玩家下棋函数PlayerMove的编写
游戏规定玩家可以在棋盘上任意还没有落子的区域下棋。执行此函数时,首先玩家在键盘上输入落子坐标,这里考虑到在玩家角度棋盘的横纵坐标范围是[1,2, ... , row],纵坐标的范围是[1,2, ... , col],而不是C语言中横纵坐标均从0开始,因此,要求程序实现坐标在玩家视角和C语言规范之间的相互转换。
当玩家输入落子坐标时,首先应判断输入坐标的合法性,若超出棋盘范围,则应重新输入。若玩家输入的坐标通过了合法性检验,则应先判断该坐标位置是否已经落子,若落子,则重新输入坐标,若未落子,则在此落子。
这里用字符 ‘ * ’ 来代替玩家下的棋子。
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x;
int y;
printf("请输入你要落子的坐标(x,y): > ");
while (1)
{
scanf("%d %d", &x, &y);
//坐标的合法性检验
if (x <= 0 || x > row || y <= 0 || y > col)
{
//未通过合法性检验(玩家输入坐标超出棋盘范围)
printf("你输入的坐标超出棋盘范围,请重新输入: > ");
}
else //通过合法性检验
{
if (board[x - 1][y - 1] == ' ') //坐标处未落子
{
board[x - 1][y - 1] = '*'; //落子
break; //终止while循环
}
else //坐标处已落子
{
printf("你输入的坐标已落子,请重新输入P: > ");
}
}
}
}
2.3.5 电脑下棋函数ComputerMove的编写
我们假设电脑随机生成落子坐标。当电脑随机生成坐标(x,y)后,检查(x,y)处是否已落子,若已落子则重新生成落子坐标,指导出现为落子坐标后终止。
这里使用字符‘#’来代替电脑下的棋子。
void ComputerMove(char board[ROW][COL], int row, int col)
{
while (1)
{
int x = rand() % row; //随机生成落子点横坐标
int y = rand() % col; //随机生成落子点纵坐标
if (board[x][y] == ' ') //判断(x,y)是否已落子
{
board[x][y] = '#';
break;
}
else
{
;
}
}
}
2.3.6 判断输赢函数IsWin的编写
为了保证整个游戏程序会在电脑和玩家其中之一取胜后或平局时停止执行并判断对局结果,这里使用了IsWin函数进行判断。每次执行完玩家下棋函数PlayerMove和电脑下棋函数后ComputerMove都要判断输赢,以决定是否继续进行游戏。
设置IsWin的返回类型为char,返回值分四种情况来讨论:
(1)返回#:电脑取胜
(2)返回* :玩家取胜
(3)返回E:平局
(4)返回C:游戏还未结束,继续进行游戏
设置返回#和*分别为电脑取胜和玩家取胜的目的是,可以直接将电脑或玩家的棋子字符#(*)作为返回值,在发现有横向、纵向或斜向上三个相同的棋子相连(即某一方取胜)时,可以直接返回刚刚玩家或电脑落子点棋盘数组对应的元素,从而更加方便快捷的判断哪方获胜。
对于某一方取胜的判断,分为三种情况来讨论:在横向上、纵向上和斜向上。其中,在斜向上的判断又分为四种情况(如图2.2所示):在主对角线对于的上三角上、主对角线对于的下三角上、在副对角线对应的上三角上、在副对角线对应的下三角上。
![](https://i-blog.csdnimg.cn/blog_migrate/3da9359b90aaa473240a2e588ca703bb.png)
若所有取胜的可能都遍历过后,依旧没有发现有获胜方,则判断游戏是否继续。若此时棋盘数组中依旧含有空格字符元素,则游戏继续。
若没有任何一方取胜,且棋盘中没有空格字符元素,则双方平局。
char IsWin(char board[ROW][COL], int row, int col)
{
int i = 0, j = 0;
//判断在横向上是否有三子相连
for (i = 0; i < ROW ; i++)
{
for (j = 0; j < col - 2; j++)
{
if (board[i][j] == board[i][j + 1] && board[i][j] != ' ')
{
if (board[i][j + 1] == board[i][j + 2])
{
return board[i][j];
}
}
}
}
//判断纵向上是否有三子相连
for (j = 0; j < col; j++)
{
for (i = 0; i < row - 2; i++)
{
if (board[i][j] == board[i + 1][j] && board[i][j] != ' ')
{
if (board[i + 1][j] == board[i + 2][j])
{
return board[i][j];
}
}
}
}
//判断斜向上是否有三子相连
//1.主对角线的上三角
for (j = 0; j < col - 2; j++)
{
int k = j;
for (i = 0; i < col - j - 2 && k < col - 2; i++, k++)
{
if (board[i][k] == board[i + 1][k + 1] && board[i + 1][k + 1] == board[i + 2][k + 2] && board[i][k] != ' ')
{
return board[i][k];
}
}
}
//判断斜向上是否有三子相连
//2.主对角线的下三角
for (i = 1; i < row - 2; i++)
{
int k = i;
for (j = 0; j < col - 2 - i && k < row - 2 ;k++, j++)
{
if (board[k][j] == board[k + 1][j + 1] && board[k + 1][j + 1] == board[k + 2][j + 2] && board[k][j] != ' ')
{
return board[k][j];
}
}
}
//判断斜向上是否有三子相连
//3.副对角线的上三角
for (j = 2; j < col; j++)
{
int k = j;
for (i = 0; i < j - 1 && j >= 2; i++, k--)
{
if (board[i][k] == board[i + 1][k - 1] && board[i + 1][k - 1] == board[i + 2][k - 2] && board[i][k] != ' ')
{
return board[i][k];
}
}
}
//判断斜向上是否有三子相连
//4.副对角线的下三角
for (i = 1; i < row - 2; i++)
{
int k = i;
for (j = col - 1; j > i + 1 && i < row - 2; k++, j--)
{
if (board[k][j] == board[k + 1][j - 1] && board[k + 1][j - 1] == board[k + 2][j - 2])
{
return board[k][j];
}
}
}
//判断是否继续游戏
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 'C';
}
}
//平局
return 'E';
}
到此,整个三子棋游戏程序的编写结束。
三. 代码的执行情况
step1:输入1,开始游戏,打印出初始化后的棋盘,并提示玩家落子。
![](https://i-blog.csdnimg.cn/blog_migrate/a2d1cbd03d8bea1d0570b1b717c64163.png)
step2:玩家输入落子坐标,并打印玩家落子后的棋盘。这里我首先输入(5,1),程序可以判断出我输入的坐标超出了棋盘范围。接着我输入(2,2),可以看到棋盘(2,2)处的元素被替换为*。
![](https://i-blog.csdnimg.cn/blog_migrate/c1fc755ecd378f9cab3562cd861eaf45.png)
Step3: 电脑落子,并打印,可以看到此时电脑将棋子下到了(3,3)坐标处。
![](https://i-blog.csdnimg.cn/blog_migrate/d161390d54e07f8e9edd28453e2f3774.png)
Step4:重复Step2和Step3,直到某一方取胜。如图3.4,为了测试玩家下棋程序合法性判断的部分,我故意在(2,2)位置处落子(该位置已落子),系统会正常提醒我重新输入坐标。
![](https://i-blog.csdnimg.cn/blog_migrate/5dda9e7d22e2702aeb949979981232f9.png)
如图3.5 - 3.12所示列出来不同的取胜情况。
![](https://i-blog.csdnimg.cn/blog_migrate/149ab0836c5db826f2b30561fe110be0.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3812ec5bfa9bc736ea1aa7ef45a16d54.png)
![](https://i-blog.csdnimg.cn/blog_migrate/76029c10f7a8802554730173cdcaeea6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8528b35bc40caf0423610386c054f467.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4b889ad5e0728f8623134c693678fd39.png)
![](https://i-blog.csdnimg.cn/blog_migrate/62a1e026902282a147e7be7398305d66.png)
![](https://i-blog.csdnimg.cn/blog_migrate/30ac6f740d179aff885443e7ad5ffeca.png)
![](https://i-blog.csdnimg.cn/blog_migrate/10886a2125fcd3ab3dfe5714befafe2d.png)
Step5:在某一方取胜或平局后,执行区会弹出提醒,需要玩家输入1/0以决定是否再来一轮游戏,输入1则重复进行Step1 -- Step4,输入0游戏结束。
到此,整个三子棋项目结束,感谢大家的阅读,请批评指正。