国内盛行五子棋的游戏,但国外还有一个名字叫“Connect Four”的游戏,两者的规则差不多。在用C语言实现这个游戏的时候会让程序设计的入门新手体验并并掌握一些技能。
说明文档如下:(CSDN资源上传需要经过审核,故审核通过后补充这一部分)
实现代码如下:
/*
* @File CFour.c
* @Author HuoYun
* @Date 2020-11-23
* @Version 1.0.0
* @Copyright HuoYun
*/
#include <ctype.h> // C标准库,提供库函数toupper()
#include <stdio.h> // C标准库,提供库函数printf()和scanf()
#include <stdlib.h> // C标准库,提供库函数malloc()和free()
#include <string.h> // C标准库,提供库函数memset()
#include <stdbool.h> // C标准库,提供类型bool
#include <stdint.h> // C标准库,提供类型uint16_t等
#include <arpa/inet.h> // 提供POSIX接口以及TCP/IP接口
#include <sys/types.h> //
#include <sys/socket.h> //
#include <unistd.h> //
bool IsServer = false; // 标识当前程序是否是服务端部分
#define IPAddress_LEN_MAX 16 // “点分十进制”表示法IP地址的最大长度(包含结束字符)
char ServerAddress[IPAddress_LEN_MAX]; // 保存“点分十进制”表示法IP地址
uint16_t Port; // 保存TCP/IP协议中占用的端口
int ServerFd; // 服务端连接文件描述符
int ClientFd; // 客户端连接文件描述符
int ListenFd; // 服务端监听文件描述符
#define BUF_SIZE 4096 // TCP连接接口中缓冲区大小
#define NAME_LENGTH_MAX 16 // 设定玩家名字的最大长度
#define DEFAULT_WIDTH 7 // 游戏面板的默认宽度为7
#define DEFAULT_HEIGHT 6 // 游戏面板的默认高度为6
int Width = DEFAULT_WIDTH; // 游戏面板的宽度是可变的,但目前为其默认值。
int Height = DEFAULT_HEIGHT; // 游戏面板的搞度是可变的,但目前为其默认值。
char PlayerName[2][NAME_LENGTH_MAX + 1]; // 将两个游戏玩家的名字保存在一个字符数组的数组中,便于管理。
int* Board = NULL; /* 面板是一个“二维”的数组(本质是数组的数组),形象地表示了现实中的游戏面板。
数组中的元素还是一个数组,这个组数组表示面板的每一列。子数组的元素是一个整数,
表示面板上每一列中的那个孔洞。如果子数组的某个元素值为0,则表示其对应的孔洞还
没有落下圆盘;如果值为1,则表示其对应的孔洞已落下属于玩家1的圆盘;如果值为2,
则表示其对应的孔洞已落下属于玩家2的圆盘。子数组的元素值不应该为其他的值,
而当游戏初始化的时候,动态内存空间(数组中的元素)已经设置为0了,故可理解为游戏面板
已被初始化为没有落下任何玩家的圆盘。
*/
int IsPlayer2 = 0; /* 这是一个哨兵变量,监视当前的玩家是不是玩家2。
如果其值为0,则表示当前玩家是玩家1;如果其值为1,则表示当前玩家是玩家2。
在代码的其他地方会依赖这个变量的值,故0和1以外的其他均为该变量的无效值。
*/
/*
* 此处定义三个宏,是设定与玩家相关的标识,在代码的多个地方的计算与玩家标识是紧密相关的。
*/
#define PLAYER_FLAG_NULL 0
#define PLAYER_FLAG_1 1
#define PLAYER_FLAG_2 2
const char DiskSymbol[] = { '.', 'X', 'O' };
#define CURRENT_PLAYER_FLAG (PLAYER_FLAG_1 + IsPlayer2) // 提供一个宏来获取当前玩家的标识。
#define ANOTHER_PLAYER_FLAG (PLAYER_FLAG_1 + !IsPlayer2) // 提供一个宏来获取非当前玩家的标识。
#define CURRENT_PLAYER PlayerName[IsPlayer2] /* 提供一个宏来获取当前玩家的名字。
数组PlayerName中,第一个元素(也是一个数组),存储了玩家1的名字;
第二个元素,存储了玩家2的名字。
*/
#define ANOTHER_PLAYER PlayerName[!IsPlayer2] // 提供一个宏来获取非当前玩家的名字。
#define QUIT 'Q' // 当玩家输入字符'Q'或'q'时程序退出。因为程序在判断玩家输入时已将字母大写,故只判断一种情况即可。
int IsGameOver = 0; /* 一个状态变量。如果计算得出某个玩家已胜出,则设定这个变量的值为非0,表示游戏已结束。
否则,游戏仍然是在进行。
*/
int HasPlayerQuit = 0; // 一个状态变量。如果某个玩家主动退出,则设定这个变量为非0。
int WinnerPlayerFlag = PLAYER_FLAG_NULL; /* 一个状态变量。如果没有玩家胜出(平局),则为PLAYER_FLAG_NULL;
* 否则,其实被设定为赢家标识(PLAYER_FLAG_1 或 PLAYER_FLAG_2)。
*/
int DiskCount = 0; // 落下圆盘的总数
#define GetColumnIndex(columnName) ((columnName) - 'A') // 将列名转换成列序号(从0开始),程序假定,列名最大至'Y'。
#define GetDiskFlag(columnIndex, rowIndex) (*(Board + Height * (columnIndex) + (rowIndex))) // 通过行列序号获取孔洞落下圆盘。
/*
* Function: SetServer
* Description: 在这个函数中,程序设置网络连接并且等待,直到有客户端连接后才返回。
* Arguments: null
* Return: null
*/
void SetServer(void)
{
struct sockaddr_in serverAddress;
struct sockaddr_in clientAddress;
socklen_t clientAddressLen;
if((ListenFd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
memset(&serverAddress, 0, sizeof(serverAddress));
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddress.sin_port = htons(Port);
if(bind(ListenFd, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
if(listen(ListenFd, 1) == -1)
{
perror("listen");
exit(EXIT_FAILURE);
}
if((ClientFd = accept(ListenFd, (struct sockaddr *)&clientAddress, &clientAddressLen)) == -1)
{
perror("connect");
exit(EXIT_FAILURE);
}
}
/*
* Function: SetClient
* Description: 在这个函数中,程序设置并且连接服务端。
* Arguments: null
* Return: null
*/
void SetClient(void)
{
struct sockaddr_in serverAddress;
if((ServerFd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
memset(&serverAddress, 0, sizeof(serverAddress));
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(Port);
if(inet_pton(AF_INET, ServerAddress, &serverAddress.sin_addr) != 1)
{
perror("inet_pton");
exit(EXIT_FAILURE);
}
if(connect(ServerFd, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) == -1)
{
perror("connect");
exit(EXIT_FAILURE);
}
}
/*
* Function: DisplayBoard
* Description: 在这个函数中,程序打印棋盘的当前状态。
* Arguments: null
* Return: null
*/
void DisplayBoard(void)
{
static int isInitBoard = 1;
// 打印两个玩家的名字,同时以'#'来标出当前玩家。
printf("Players:\n");
for (int i = 0; i < 2; i++)
{
if (isInitBoard)
{
if (i + 1 == PLAYER_FLAG_1)
printf(" # ");
else
printf(" ");
}
else
{
if (i + 1 == ANOTHER_PLAYER_FLAG)
printf(" # ");
else
printf(" ");
}
printf("%s\n", PlayerName[i]);
}
// 绘制列名。同时,对于每一行,开头也要加上行号。
printf("\n");
printf(" ");
for (int i = 0; i < Width; i++)
{
printf(" %c", 'A' + i);
}
printf("\n");
for (int i = Height - 1; i >= 0; i--)
{
printf(" %d", Height - i);
for (int j = 0; j < Width; j++)
{
printf(" %c", DiskSymbol[*(Board + j * Height + i)]);
}
printf("\n");
}
isInitBoard = 0;
}
/*
* Function: Initialization
* Description: 在这个初始化函数中,依据当前程序是服务端部分还是客户端部分,
程序获取必要的游戏参数来初始化游戏。在当前的程序中,只获取了玩家的名字。
但在之后的设置中,可能还需要获取面板的参数(宽度和高度)。
* Arguments:
* @isServer: 当前程序是否是服务端部分
* Return: null
*/
void Initialization(bool isServer)
{
printf("***** Setting up the game *****\n");
uint8_t buf[BUF_SIZE];
int count;
if(isServer)
{
// 获取玩家1的名字
printf("Please input the name of PLAYER1(less than %d characters): ", NAME_LENGTH_MAX);
scanf("%s", PlayerName[0]);
// 因为输入缓冲区中还保留一个换行符,可能会影响到后面的读取,故“手动”舍弃这个换行符。
getchar();
printf("Waiting PLAYER2 ...\n");
SetServer();
// 获取玩家2的名字
count = read(ClientFd, buf, BUF_SIZE);
if(count == -1)
{
perror("read");
exit(EXIT_FAILURE);
}
else if(count == 0)
{
fprintf(stderr, "Client has been closed.");
if(close(ClientFd) == -1)
{
perror("close");
exit(EXIT_FAILURE);
}
if(close(ListenFd) == -1)
{
perror("close");
exit(EXIT_FAILURE);
}
exit(EXIT_FAILURE);
}
buf[count] = '\0';
strncpy(PlayerName[1], (char *)buf, NAME_LENGTH_MAX);
// 发送玩家1的名字
strncpy((char *)buf, PlayerName[0], NAME_LENGTH_MAX);
count = write(ClientFd, buf, strlen((char *)buf));
if(count == -1)
{
perror("write");
exit(EXIT_FAILURE);
}
}
else
{
// 获取玩家2的名字
printf("Please input the name of PLAYER2(less than %d characters): ", NAME_LENGTH_MAX);
scanf("%s", PlayerName[1]);
// 因为输入缓冲区中还保留一个换行符,可能会影响到后面的读取,故“手动”舍弃这个换行符。
getchar();
printf("Waiting PLAYER1 ...\n");
SetClient();
// 发送玩家2的名字
strncpy((char *)buf, PlayerName[1], NAME_LENGTH_MAX);
count = write(ServerFd, buf, strlen((char *)buf));
if(count == -1)
{
perror("write");
exit(EXIT_FAILURE);
}
// 获取玩家1的名字
count = read(ServerFd, buf, BUF_SIZE);
if(count == -1)
{
perror("read");
exit(EXIT_FAILURE);
}
else if(count == 0)
{
fprintf(stderr, "Server has been closed.");
if(close(ServerFd) == -1)
{
perror("close");
exit(EXIT_FAILURE);
}
exit(EXIT_FAILURE);
}
buf[count] = '\0';
strncpy(PlayerName[0], (char *)buf, NAME_LENGTH_MAX);
}
// 初始化棋盘
Board = malloc(sizeof(int) * Width * Height);
if (!Board)
{
// 在极端情况下,申请内存空间会失败,此时程序退出。
perror("malloc");
exit(EXIT_FAILURE);
}
memset(Board, 0, sizeof(int) * Width * Height);
// 显示棋盘
DisplayBoard();
}
/*
* Function: AcceptInput
* Description: 在这个函数中,玩家输入面板列名,游戏判断输入是否合法。如果不合法则,重新输入。
* Arguments: null
* Return: 玩家输入的合法字符,面板列名或是退出。
*/
char AcceptInput(void)
{
char input;
int fd;
int count;
if((IsServer && CURRENT_PLAYER_FLAG == PLAYER_FLAG_1)
|| (!IsServer && CURRENT_PLAYER_FLAG == PLAYER_FLAG_2))
{
do
{
/*
* 读取一个字符。因为读取到一个字符后,输入缓冲区中还保留一个换行符,故也应该舍弃。
*/
printf("%s, please input the column(%c - %c, %c for quit): ", CURRENT_PLAYER, 'A', 'A' + Width - 1, QUIT);
scanf("%c", &input);
getchar();
/* 对于玩家来说,可能输入的小写字母,也可能输入的大写字母。
* 如果只允许用户输入大写字母(或小写字母),那么他在输入
* 另一种字母时,程序会发出输入错误的信息。这种情况下,
* 玩家会感到自己的积极性受到打击,从而游戏体验受到影响。
* 为了提高游戏体验,故不论玩家输入的是大写字母还是小写字母,
* 均在此转换成大写字母,以作统一处理,之后程序不再区分。
*/
input = toupper(input);
// 如果玩家输入了字符'Q',则表示玩家退出。
if (input == QUIT)
break;
// 如果玩家输入了面板列名的有效字母,则认可玩家的输入。
if (input >= 'A' && input <= 'G')
break;
// 当玩家输入了非法的字符,则要求玩家重新输入,直到输入正确或退出为止。
printf("Your input is invalid, please input again.\n");
} while (1);
fd = IsServer ? ClientFd : ServerFd;
count = write(fd, &input, sizeof(input));
if(count == -1 || count != sizeof(input))
{
perror("write");
exit(EXIT_FAILURE);
}
}
else
{
fd = IsServer ? ClientFd : ServerFd;
count = read(fd, &input, sizeof(input));
if(count == -1)
{
perror("read");
exit(EXIT_FAILURE);
}
else if(count == 0)
{
input = 'Q';
}
}
return input;
}
/*
* Function: UpdateState
* Description: 在这个函数中,程序依据玩家的输入的列名进行相关计算,并且设定相关的状态值。
* Arguments:
* @input: 玩家输入的列名
* Return: 当玩家输入的列已经落满了圆盘,则是无法落入成功的,故需要要求玩家重新输入。
如果返回1,则表示落入成功;如果返回0,则表示落入失败,需重新输入。
这中间存在一个情况,如果玩家主动退出,也认为是落入成功(虚拟地落下)。
*/
int UpdateState(char input)
{
// 如果当前玩家要求退出,则游戏循环准备结束。
if (input == QUIT)
{
HasPlayerQuit = 1;
IsGameOver = 1;
WinnerPlayerFlag = ANOTHER_PLAYER_FLAG;
return 1;
}
// 依据输入的列名,通过计算,找到该列第一个圆盘在面板数组中的位置。
int* p = Board + (input - 'A') * Height;
// 从该列的第一个孔洞开始,逐个孔洞的查看,尝试找到一个未落入圆盘的孔洞。
int i;
for (i = 0; i < Height && *(p + i) > 0; i++)
;
/* 因数组中第一个元素的下标是0而非1,故如果当前孔洞的下标等于面板高度时,
说明所有该列所有孔洞都已落下圆盘。需要要求玩家重新输入了。
*/
if (i == Height)
{
if((IsServer && CURRENT_PLAYER_FLAG == PLAYER_FLAG_1)
|| (!IsServer && CURRENT_PLAYER_FLAG == PLAYER_FLAG_2))
printf("The column '%c' is full, please input again.\n", input);
return 0;
}
/*
* 如果当前玩家是玩家1,则IsPlayer2的值为0,那么PLAYER_FLAG_1 + IsPlayer2的值为1;
* 如果当前玩家是玩家2,则IsPlayer2的值为1,那么PLAYER_FLAG_1 + IsPlayer2的值为2。
* 把数组中元素值为0(还未落入圆盘的孔洞)的元素设定为1(落入玩家1的圆盘)或设定为2(落入玩家2的圆盘)。
*
* 2020-10-31 19:46 Modify:
* PLAYER_FLAG_1 + IsPlayer2 已被宏 CURRENT_PLAYER_FLAG 替代
*/
*(p + i) = CURRENT_PLAYER_FLAG;
/*
* 落下当前圆盘后,如果横向、纵向或对角线方向4个圆盘连成一条直线时判定当前玩家为赢家。
* 以当前落下圆盘为中心,向上、下、左、右、左上、右上、左下、右下八个方向(横向2个方向、纵向2个方向、对角线4个方向),
* 查看沿线的四个棋子是否都是当前玩家落下的。如果在任意一个方向上满足条件,
* 那么就可以判定当前玩家已经实现“四子成一条线”了。
*/
int columnIndex = GetColumnIndex(input);
int rowIndex = i;
int hasLine = 0;
do
{
/*
* 1、具体在判断的时候,先假定是“四子成线”的,只要四个棋子中有任何一个棋子不是当前玩家的,
* 那么假定失败。
* 2、一旦8个方向的任意方向可“四子成线”,那么剩余情况就不再判断了。
*/
// 判断横向是否可以成直线
// 方向向左
hasLine = 1; // hasLine是一个哨兵变量,其逻辑意义是:假定当前方向可“四子成线”(值为1),如果发现假定失败,那么设置为否(值为0)。
for (int i = 1; i < 4; i++)
{
if (columnIndex - i < 0)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex - i, rowIndex) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 方向向右
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (columnIndex + i >= Width)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex + i, rowIndex) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 判断纵向是否可以成直线
// 方向向下
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (rowIndex - i < 0)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex, rowIndex - i) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 方向向上
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (rowIndex + i >= Height)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex, rowIndex + i) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 判断对角线方向是否可以成直线
// 方向向左下
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (columnIndex - i < 0 && rowIndex - i < 0)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex - i, rowIndex - i) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 方向向右上
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (columnIndex + i >= Width && rowIndex + i >= Height)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex + i, rowIndex + i) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 方向向左上
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (columnIndex - i < 0 && rowIndex + i >= Height)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex - i, rowIndex + i) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
// 方向向右下
hasLine = 1;
for (int i = 1; i < 4; i++)
{
if (columnIndex + i >= Width && rowIndex - i < 0)
{
hasLine = 0;
break;
}
if (GetDiskFlag(columnIndex + i, rowIndex - i) != CURRENT_PLAYER_FLAG)
{
hasLine = 0;
break;
}
}
if (hasLine)
break;
} while (0);
if (hasLine)
{
IsGameOver = 1;
WinnerPlayerFlag = CURRENT_PLAYER_FLAG;
return 1;
}
if (++DiskCount == Width * Height)
IsGameOver = 1;
// 返回圆盘落入成功的状态。
return 1;
}
/*
* Function: DisplayWorld
* Description: 在这个函数中,程序打印游戏相关的状态。
* Arguments: null
* Return: null
*/
void DisplayWorld()
{
// 显示棋盘
DisplayBoard();
// 如果游戏没有结束,那么就返回来继续游戏循环。
if (!IsGameOver)
{
return;
}
// 此时,游戏已经结束了。
if (!HasPlayerQuit)
{
// 如果当前玩家没有主动退出
if (WinnerPlayerFlag == PLAYER_FLAG_NULL)
{
printf("Ties!\n");
}
else
{
printf("%s wins!\n", PlayerName[WinnerPlayerFlag - 1]);
}
}
else
{
// 如果当前玩家主动退出,则判定另一个玩家获胜
printf("%s quit!\n", CURRENT_PLAYER);
printf("%s wins!\n", ANOTHER_PLAYER);
}
}
/*
* Function: TearDown
* Description: 在这个函数中,程序打印游戏正在退出的信息。
随着游戏设计复杂性的提高,在这个函数中设计与游戏退出相关的一些操作。
* Arguments: null
* Return: null
*/
void TearDown(void)
{
printf("***** Destroying the game *****\n");
free(Board);
if(IsServer)
{
if(close(ClientFd) == -1)
{
perror("close");
exit(EXIT_FAILURE);
}
if(close(ListenFd) == -1)
{
perror("close");
exit(EXIT_FAILURE);
}
}
else
{
if(close(ServerFd) == -1)
{
perror("close");
exit(EXIT_FAILURE);
}
}
}
/*
* Function: main
* Description: 程序main()函数,程序不从命令行获取参数。
* Arguments:
* @argc: 命令行参数数目
* @argv: 命令行参数列表
* Return: 程序退出状态
*/
int main(int argc, char * argv[])
{
if(argc == 2)
{
// 服务端程序
IsServer = true;
char * endPtr = NULL;
Port = strtod(argv[1], &endPtr);
if(endPtr - argv[1] != strlen(argv[1]))
{
fprintf(stderr, "String of port is invalid\n");
exit(EXIT_FAILURE);
}
}
else if(argc == 3)
{
// 客户端程序
IsServer = false;
strncpy(ServerAddress, argv[1], strlen(argv[1]));
char * endPtr = NULL;
Port = strtod(argv[2], &endPtr);
if(endPtr - argv[2] != strlen(argv[2]))
{
fprintf(stderr, "String of port is invalid\n");
exit(EXIT_FAILURE);
}
}
else
{
fprintf(stderr, "Usage: CFour [<IP Address>] <Port>\n");
exit(EXIT_FAILURE);
}
// 初始化游戏
Initialization(IsServer);
char input;
do
{
// 当前玩家输入一个有效的输入
input = AcceptInput();
// 如果当前玩家选定的列已经满了,则要求玩家重新选择。
if (!UpdateState(input))
continue;
// 现实当前玩家落下圆盘的孔洞位置。
DisplayWorld();
// 如果游戏没有结束,那么切换当前玩家,游戏进入新的一次循环中。
IsPlayer2 = !IsPlayer2;
} while (!IsGameOver);
// 程序打印游戏正在退出的信息,在这个过程中可以执行一些与游戏退出相关的工作。
TearDown();
// 向系统返回程序成功退出的状态码。
return 0;
}