C语言实现三子棋游戏学习记录

1. 创建main.c、game.c、game.h三个文件

        本次三子棋游戏的实现包含三个文件,第一个文件是main.c,控制游戏的执行逻辑。第二个文件是游戏头文件game.h,用来声明游戏实现的函数。第三个是game.c,主要是用来存放实现游戏的具体函数。

2. 游戏展示

        下面三子棋游戏展示界面。

3. 主函数main()编写

3.1 开始菜单打印

        首先游戏开始时要打印游戏开始界面,供玩家选择和退出游戏,可以调用一个menu()来打印。menu()具体代码如下:

void menu() {
	printf("**********************************\n");
	printf("****    1. play       0.exit  ****\n");
	printf("**********************************\n");
	printf("请输入:>");
}

3.2 do...while()控制游戏执行

        当主函数main()调用menu()打印出菜单时,玩家选择玩游戏或者退出游戏。当玩家选1时,玩家开始游戏,当游戏结束后,主函数需要再次打印菜单,如果还想再玩,选1,进入游戏,直到玩家不再想玩了,玩家选0,退出游戏。

        上面的实现逻辑可以使用do...while()循环,先打印一个游戏开始菜单,再根据玩家输入的选项决定进入游戏或者是退出游戏。可以用scanf()函数接收用户输入的选项。具体实现代码如下:

int main() {
	//input用来接收用户输入的选项
	int input = 0;	//input初始化
	do {
		menu();	//打印游戏开始菜单
		scanf("%d", &input);	//用scanf()函数在命令行窗口接收用户输入的选项,并传递给input变量
		switch (input) {
			case 1:
				printf("游戏开始!\n");	//选1,游戏开始
				game();
				break;
			case 0:
				break;	//选0,跳出switch语句,然后游戏会退出
			default:
				printf("选项输入错误,请重新输入!\n");	//当选项不是0或1时,跳出switch语句,重新输入
				break;
		}
	} while (input);	//用变量input作为循环的判断条件,input为0,跳出循环退出游戏,非0时循环继续
	return 0;
}

4. game()函数编写

4.1 game()执行流程

        当玩家选1后,开始玩游戏,调用game()函数。下面来分析一下game()函数执行流程:game()函数的执行分为粗略分几个步骤:玩家下棋、棋盘展示 、输赢判断、电脑下棋、棋盘展示、输赢判断。以上步骤需要不断重复,直到一方为赢或者双方平局。玩家下棋、棋盘展示 、输赢判断、电脑下棋可以分别用PlayerMove()、DisplayBoard()、IsWin()、ComputerMove()表示,下面是大概的执行流程:

void game() {
	while (判断条件) {
		PlayerMove();	//玩家下棋
		DisplayBoard();	//棋盘展示
		IsWin();	//判断输赢
		ComputerMove();	//电脑下棋
		DisplayBoard();	//棋盘展示
		IsWin();	//判断输赢
	}
	...
}

4.2 棋盘初始化

        根据上面执行流程,首先玩者想下棋,就需要根据棋盘位置进行落子。所以在落子前需要先打印棋盘。我们观察一下三子棋不同状态的棋盘,即空棋盘、落子后的棋盘,方便我们构思如何打印棋盘和棋盘落子后如何存储落子数据等问题。

        通过上面观察,我们发现,三子棋的棋盘可分为两部分:分隔符和棋盘内容。分隔符是固定不变的(横为虚线,竖为实线),可以在每次打印时加上分隔符,然后打印出来。而且棋盘内容在未落子前为空,落子后变成'*'或者'#'。进一步来说,对于空的棋盘状态,可以看成填满空格字符' '的棋盘,在落子时,只是将空格' '替换成'*'或'#'('*'表示玩家下棋,'#'表示电脑下棋),最后将棋盘内容打印出来。因为这个是三子棋,有三行三列。可以用一个二维数组存储棋盘内容,其二维数组的类型为char,可以存放三种不同状态,即未下棋' '、玩家下棋'*'、电脑下棋'#'。

        经过上面讨论,所以在下棋前,还需要一个步骤,就是将棋盘的内容全部初始化成空格字符,才能打印出空棋盘,然后玩家根据棋盘进行落子。初始化棋盘用InitBoard()表示,实现代码如下:

//初始化棋盘
//ROW、COL分别表示棋盘的行数和列数,在头文件中已定义为常量
//ROW、COL定义为常量,方便后面不把代码写死,方便后面改为四子、五子棋、等n子棋。
void InitBoard(char board[ROW][COL], int row, int col) {    //row表示棋盘行数,col表示棋盘的列数
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++) {
		for (j = 0; j < col; j++) {
			board[i][j] = ' ';	//将棋盘的每个元素初始化为字符空格' '
		}
	}
}

4.3 棋盘展示打印

        初始化棋盘后,需要对棋盘打印,打印棋盘也就是打印分隔符+棋盘内容。这里为了不把程序写死,方便后改成打印四子棋、五子棋、n子棋等。实际上,后面的代码都基于这个理念。实现下面的代码如下:

//棋盘显示
void DisplayBoard(char board[ROW][COL], int row, int col) {
	int i = 0;
	for (i = 0; i < row; i++) {	//按行来打印,总共需要打印row行
		//打印" %c | %c | %c "
		int j = 0;
		for (j = 0; j < col; j++) {
			printf(" %c ", board[i][j]);	//打印每行的%c
			if (j < col - 1) {	//为了使打单行印出的'|'数量比%c少1个
				printf("|");	//打印%c结束后,在后面加一个分隔符'|'
			}
		}
		printf("\n");
		//打印分割信息"---|---|---"
		if (i < row - 1) {	//控制棋盘最后一行不打印分割信息
			int k = 0;
			for (k = 0; k < col; k++) {
				printf("---");	//打印分隔符"---"
				if (k < col - 1) {	//为了使打单行印出的'|'数量比"---"少1个
					printf("|");	//打印"---"结束后,在后面加一个分隔符'|'
				}
			}
			printf("\n");
		}
	}
}

        所以现在game()执行的流程:初始化棋盘、空棋盘显示、玩家下棋、棋盘展示 、输赢判断、电脑下棋、棋盘展示、输赢判断。

void game() {
	InitBoard();    //初始化棋盘
	DisplayBoard();    //空棋盘展示
	while (判断条件) {
		PlayerMove();	//玩家下棋
		DisplayBoard();	//棋盘展示
		IsWin();	//判断输赢
		ComputerMove();	//电脑下棋
		DisplayBoard();	//棋盘展示
		IsWin();	//判断输赢
	}
	...
}

        上面提到,为了不把三子棋程序写死,方便改为四子棋、五子棋等。我将棋盘的行数和列数,即ROW和COL定义为常量,写在头文件game.h上,同时main.c文件上再包含头文件"game.h"。如果后续想改为四子棋、五子棋等,只需要将game.h文件中ROW和COL改为特定的值即可,具体定义常量如下。

#define ROW 3
#define COL 3

        并且在main.c文件中包含game.h的头文件,具体如下:

#include "game.h"

        现在game()函数上定义char类型二维数组表示,然后进行棋盘初始化和棋盘展示。

void game() {
	char board[ROW][COL] = {0};    //定义一个char类型的二维数组
	InitBoard(board, ROW, COL);    //初始化棋盘
	DisplayBoard(board,ROW,COL);    //棋盘展示
    while (判断条件) {
		...
	}
	...
}

        测试一下棋盘的展现效果!

4.4 玩家下棋

        完成棋盘打印后,下面开始写玩家下棋的代码,玩家由PlayerMove()函数控制。玩家下棋时,需要输入落子行数和列数,如输入坐标(3,1),表示落子在第3行、第1列。

        当玩家输入坐标(3,1)时,只需要将玩家输入的坐标转化为board数组的坐标索引,即board[2][0],然后该位置空格字符‘ ’替换成字符'*'即可。

        当然,玩家也有可能输入的坐标超出棋盘范围,需要增加一个输入坐标的合法判断,也就是输入的坐标要大于等于1并且小于等于行数或列数。

        而且,当输入的坐标是合法后,如果之前这个坐标有落子,位置被占用了,也是不能落子的,需要增加落子位置是否占用的判断。PlayerMove()具体实现如下:

//玩家下棋
//玩家下棋为标记为*
void PlayerMove(char board[ROW][COL], int row, int col) {
	int x = 0;	//用来接收玩家下棋输入的行数
	int y = 0;	//用来接收玩家下棋输入的列数
	while (1) {
		printf("玩家下棋:>");
		scanf("%d %d", &x, &y);			//用scanf()函数在屏幕上接收玩家输入的行数和列数
		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");
		}
	}
}

4.5 电脑下棋

        实现了玩家下棋,下面开始写电脑下棋代码,和玩家下棋代码大同异,用ComputerMove()表示。因为暂时没有学过复杂的电脑下棋代码,知识有限,暂时用随机生成值代替电脑下棋,哈哈,简单粗暴。

        因为电脑下棋需要随机生成值,用rand()进行随机值生成,将生成随机值模上行数或列数,就能得到棋盘二维数组的索引坐标,这样生成的坐标是合法的,又减少了坐标合法判断的代码,只需判断位置是否被占用就可以了,具体如下:

//电脑下棋
//电脑下棋标记为#
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;
		}
	}
}

        在调用rand()函数时,需要设置一个随机数序列的起始点,即随机生成序列的"种子",可以通过获取当前系统时间的秒数作为"种子"。放在main函数上调用一次可以了。然后包含上头文件time.h。

int main() {
	srand((unsigned int)time(NULL));	//将当前系统时间的秒数作为随机生成序列的"种子"传给srand()
	...
	return 0;
}

        现在测试一下电脑随机下棋的效果,成功!

4.6 输赢判断

        当执行完玩家下棋/电脑下棋、棋盘展示,需要进行输赢判断。如果有一方赢了,另一方就不需要继续下棋了,直接宣布输赢的结果。如果没有赢,游戏需要继续,不断重复:玩家下棋——输赢判断——电脑下棋——输赢判断。

        根据上面执行流程,可以设置一个while(1)的循环,当一方赢或者平局时,break跳出这个循环。可以让IsWin()判断后返回一个值,如果电脑赢返回字符'#',如果玩家赢返回'*',如果是平局,返回'Q',如果以上情况都不是,则游戏继续,返回'C'。下面是game()游戏的执行流程框架。

void game() {
	char ret = 0;	//设置输赢返回标记ret
	char board[ROW][COL] = {0};
	InitBoard(board, ROW, COL);
	DisplayBoard(board,ROW,COL);
	while (1) {
		PlayerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);
		ret = IsWin(board, ROW, COL);	//玩家下棋后,进行输赢判断,返回一个值
		if (ret != 'C') {	//如果返回的不是继续游戏,即'!C'
			break;	//返回的不是继续,需要跳出循环
		}
		ComputerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);
		ret = IsWin(board, ROW, COL);	//玩家下棋后,进行输赢判断,返回一个值
		if (ret != 'C') {		//如果返回的不是继续游戏,即'!C'
			break;	//返回的不是继续,需要跳出循环
		}
	}
	//玩家赢、电脑赢、平局的判断
	if (ret == '*') {
		printf("玩家赢\n");
	} else if (ret == '#') {
		printf("电脑赢\n");
	} else {
		printf("电脑赢\n");
	}
}

        通过IsWin()返回的结果和if进行判断结果决定是否跳出循环,就可以让游戏正常运行下去。

        接下来进行输赢结果代码实现分析,用IsWin()表示。三子棋的输赢判断有三个维度:列成三子,横成三子,对角线成三子。如果其中一个维度赢了,就不需要继续判断了。

        下面简单描述一下判断代码,以判断行为例,首先先判断第一行,再判断下一行,一行一行地判断。对于每一行,从第2列的值开始,分别和第1列值比较,如果两者相同,计数标志count就加1,当count值等于row - 1时,说明已经三行成子了,其中一方已经赢了,此时只需要返回该行任意一个值即可,当返回的是'*',说明是玩家赢了,当返回的是'#',说明电脑赢了。

        对于对角线判断也是同样思路,分为反斜杠对角线‘\’和正斜杠对角线‘/’两种,对于反斜杠对角线‘\’,只需要比较棋盘board[0][0]、board[1][1]...board[row-1][col-1]的值;对于正斜杠对角线‘/’,只需要比较棋盘board[0][col-1]、board[1][col-2]...board[row-1][0]的值。对角线的判断,不需要像判断列或者行一样,需要设置两层循环,只要设置一层循环即可。

        下面是判断输赢判断IsWin()代码的实现,同样也是从三子棋扩展到n子棋的判断,代码如下:

//判断棋盘是否为满
//返回1为棋盘落子为满
//返回0为棋盘落子不满
int IsFull(char board[COL][COL], int row, int col) {
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++) {
		for (j = 0; j < col; j++) {
			if (board[i][j] == ' ') {
				return 0;
			}
		}
	}
	return 1;
}

//判断输赢
//IsWin返回'*'表示玩家赢
//IsWin返回'#'表示电脑赢
//IsWin'Q'表示平局
//IsWin返回'C'表示游戏继续
char IsWin(char board[ROW][COL], int row, int col) {
	//判断行是否为赢	
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 0; i < row; i++) {	//判断一行,再判断下一行
		for (j = 1; j < col; j++) {	//从每行的第二个值开始比较
			if (board[i][j] == ' ') {	//如果该值等于空,说明该行没有赢,需要跳出循环
				break;		//跳出循环
			}
			else if (board[i][0] == board[i][j]) {	//从每行的第二个值开始,和每行第1个值比较
				count++;	//如果发现两者值相同,count就加1
			}
			else if (board[i][0] != board[i][j]) {	//如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
				break;	//跳出循环
			}
		}
		if (count == row - 1) {	//当count累积的数量为row -1时,说明有一方已经赢了
			return board[i][0];	//返回是赢方的标记,返回行的其中一个值即可
		}
		else {
			count = 0;	//判断一行结束后,需要count进行清零,方便下一行判断
		}
	}
	//判断列是否为赢
	int k = 0;
	int l = 0;
	count = 0;
	for (k = 0; k < col; k++) {	//判断一列,再判断下一列
		for (l = 1; l < row; l++) {	//从每列的第二个值开始比较
			if (board[l][k] == ' ') {	//如果该值等于空,说明该列没有赢,需要跳出循环
				break;
			}
			else if (board[0][k] == board[l][k]) {	//从每列的第二个值开始,和每列第1个值比较
				count++;		//如果发现两者值相同,count就加1
			}
			else if (board[0][k] != board[l][k]) {	//如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
				break;	//当count累积的数量为count -1时,说明有一方已经赢了
			}
		}
		if (count == col - 1) {	//当count累积的数量为col -1时,说明有一方已经赢了
			return board[0][k];	//返回是赢方的标记,返回列的其中一个值即可
		}
		else {
			count = 0;	//判断一列结束后,需要count进行清零,方便下一行判断
		}
	}
	//判断对角是否为赢
	//反斜杠对角线判断(\)
	int d = 0;
	for (d = 1; d < row; d++) {	//从第二值开始,分别和对角线的第一个值比较
		if (board[d][d] == ' ') {	//当该值为空时,说明该对角线棋子未满,需要跳出循环
			break;
		}
		else if (board[0][0] == board[d][d]) {	//如果要比较的值与第一个值相同,count就加1
			count++;
		}
		else if (board[0][0] != board[d][d]) {	//如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
			break;
		}
	}
	if (count == row - 1) {	//当count累积的数量为row -1时,说明有一方已经赢了
		return board[0][0]; //返回是赢方的标记,返回该对角线的其中一个值即可
	}

	//斜杠对角线判断(\)
	int n = 0;
	count = 0;
	for (n = 1; n < col; n++) {	//从第二值开始,分别和对角线的第一个值比较
		if (board[n][col - n - 1] == ' ') {	//当该值为空时,说明该对角线棋子未满,需要跳出循环
			break;
		}
		else if (board[0][col - 1] == board[n][col - n - 1]) {	//如果要比较的值与第一个值相同,count就加1
			count++;
		}
		else if (board[0][col - 1] != board[n][col - n - 1]) {	//如果要比较值不相等,也就是是'*'和'#',这种情况也需要跳出循环
			break;
		}
	}
	if (count == col - 1) {	//当count累积的数量为col -1时,说明有一方已经赢了
		return board[0][col - 1];//返回是赢方的标记,返回该对角线的其中一个值即可
	}
	//	如果上面没有判断出哪一方赢了,就需要判断是否为平局
	if (IsFull(board, row, col)) {	//如果没有哪一方赢,并且此时棋盘满了,说明已经平局了
		return 'Q';	//平局返回Q
	}
	return 'C';		//如果没有哪一方是赢,棋盘又没有满,游戏需要继续,返回'C'
}

4.7 输赢判断测试  

        写完IsWin()后,测试一下是否符合要求。

        测试行是否赢,OK!

        测试列是否赢,也OK!

        测试对角线是否赢,都OK!

        上面都输赢测试都通过!

5. 四子棋、五子棋等测试

        写到这里,游戏程序实现的代码基本写完了,接下来测试一下,除了三子棋,四子棋是否能够成功运行起来,只需要将头文件game.h中常量定义的ROW和COL改成4即可,修改如下。

#define ROW 4
#define COL 4

        测试一下四子棋运行效果:

        测试一下五子棋运行效果:

#define ROW 5
#define COL 5

6. game.h内容  

       最后附上头文件game.h的内容

#pragma once
#define ROW 3
#define COL 3
#include <stdio.h>
#include <time.h>

//初始化棋盘
void InitBoard(char board[ROW][COL],int row,int col);
//显示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col);
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col);
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col);
//判断输赢
char IsWin(char board[ROW][COL], int row, int col);

7. game.c内容

       game.c文件中包括相关函数

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
//初始化棋盘
void InitBoard(char board[ROW][COL], int row, int col) {
	...
}
//棋盘显示
void DisplayBoard(char board[ROW][COL], int row, int col) {
	...
}
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col) {
	...
}
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col) {
	...
}
//判断棋盘是否为满
int IsFull(char board[COL][COL], int row, int col) {
	...
}
//判断输赢
char IsWin(char board[ROW][COL], int row, int col) {
	...
}

8. main.c内容

main.c文件中包括相关函数如下

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu() {
	...
}
void game() {
	...
}
int main() {
	...
}

        正在学习C语言,文章有写不正确的地方,欢迎多多指正,谢谢!上面的代码会打包上传码云上gitte。链接:https://gitee.com/yiqixuexiba/Tic-Tac-Toe

注:文章中“五子棋”并非我们平常玩的五子棋,只是由三子棋扩展出来五子棋,哈哈!

相关参考:比特鹏哥的C语言的学习视频。

  • 39
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值