超详解三子棋(优化后)【万字教程包教包会】

详解三子棋

  • 前言
  • 一.游戏规则
  • 二 .游戏流程
  • 三.代码实现
    • 3.1主体部分实现
    • 3.2 game函数的实现
      • 3.2.1创建数组
      • 3.2.2初始化棋盘
      • 3.2.3显示棋盘
      • 3.2.4玩家下棋
      • 3.2.5判断输赢
      • 3.2.6电脑下棋
      • 3.2.7判断输赢
      • 3.2.7给出结论
  • 四.一些锦上添花以及完整代码

前言

本文将介绍三子棋(井字棋)的实现方法,以游戏的流程为主线,运用模块化编程的思想,分为game.c, game.h , test.c三个文件完成代码。先看一下最后的效果吧
超详解三子棋(优化后)【万字教程包教包会】_第1张图片
超详解三子棋(优化后)【万字教程包教包会】_第2张图片

超详解三子棋(优化后)【万字教程包教包会】_第3张图片
超详解三子棋(优化后)【万字教程包教包会】_第4张图片

一.游戏规则

三子棋的游戏大家应该都不陌生,但这里还是有必要介绍一下,毕竟熟悉游戏的流程思路才会清晰。

游戏规则要求:电脑(随机)和玩家一次下一子,双方轮流交替下子,直到电脑和玩家任意一方获胜,或者棋盘满了,结束游戏。

获胜判定:只要电脑或者玩家任意一方的棋子有三子连在一起,可以为横连、纵连、斜连,则该方获胜,游戏结束。若棋盘满了,则判定为平局。
加粗样式

二 .游戏流程

整个游戏流程可分为以下这几步
1.给玩家一个菜单,让其选择进入游戏或者退出游戏

接下来是一个循环的过程


2.玩家选择进入游戏后,接下来呈现一个棋盘

3.系统提示玩家落子,玩家输入一个坐标

4.屏幕上打印棋盘,展现落子情况

5.电脑落子

6.打印棋盘,展现落子情况


7.游戏分出结果(平局,玩家胜,电脑胜)

8.回到第一步

上述流程中我们可以看到,游戏的核心部分是2到7步,因此我们将这些步骤的实现封装在一个game函数里,game函数里需要用到的函数就在game.h文件中声明,在game.c文件中定义。有了这些思考我们就可以开工了。

三.代码实现

3.1主体部分实现

首先打印一个菜单,给出两个选项,玩家的选择值用一个变量接收。

接着使用多分支switch case语句,若玩家选择开始游戏就进入game函数,若选择退出就结束程序,如果选择不符合要求就让他重新输入。

整个过程用循环结构套起来,当一套流程走完后用户又可以重新选择。这里我们选择do while循环,让这个逻辑至少走一遍。

另外这里接收用户输入的值时需要进行合法性判断。比如我期望接收一个数字,但你却输入一个字母,如果没有处理这种情况程序就会陷入死循环(至于为什么下文分析)

上代码:

void menu()
{
	printf("*****************************************\n");
	printf("*******      1. 三子棋游戏          *****\n");
	printf("*******      2. 退出游戏            *****\n");
	printf("***************************************  \n");

}


int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择>:");
		while (scanf("%d", &input) != 1)
		{
			printf("输入非法,请重新输入>:");
			while (getchar() != '\n')
			{
				;
			}
		}

		switch (input)
		{
		case 1:
			printf("三子棋游戏开始\n");
			game1();
			break;
		case 2:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input != 2);

	return 0;
}

这里的do while循环和switch case语句的配套使用基本上可以作为固定写法形成模板,以后执行类似的逻辑时直接套用它就行了。

这里讲一讲用户输入数据的合法性判断。
这里我期望用户输入一个数字,只要是数字就行,要求不高,哪怕不是1或者2都行,因为我的default可以处理。

但总有一些叛逆的玩家不这么干,给你丢一个字母放上面,按下回车,scanf函数于是开始读取数据,但是我要求接收的是%d啊,你咋给我放个字符啊。scanf函数罢工,接收失败,input也没有放入新的值,还是初值0。程序往下走,来到default这儿,提示选择错误。input != 2,再次进入do while循环。

因为上次循环时那个字母读取失败了嘛,所以还留在缓冲区里,这里scanf函数往缓冲区内一瞅,发现里面有东西,所以不会停下来等待用户输入,直接开始读取,结果又读取失败,input的值还是最初的0。所以就这样陷入死循环。

这里的解决方法就是给scanf函数套上一个while循环,当你接收成功就跳出去,如果接收不成功还要刷新缓冲区以便再次接收,关于刷新缓冲区的方法之前有讲过,感兴趣的老铁可以翻看我以前的文章

3.2 game函数的实现

void game1()
{
	//存放数据需要一个3*3的二维数组
	char board[ROW][COL] = { 0 };
	//初始化棋盘
	InitBoard(board, ROW, COL);
	//显示棋盘
	DisplayBoard(board, ROW, COL);

	char ret = 0;
	while (1)
	{
		//玩家下棋
		PlayerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);//显示棋盘
		//判断输赢
		ret = IsWin(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}
		//电脑下棋
		ComputerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);//显示棋盘
		//判断输赢
		ret = IsWin(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}
	}

	if ('*' == ret)
	{
		printf("恭喜你,你赢了\n");
	}
	else if ('#' == ret)
	{
		printf("很遗憾,你输了\n");
	}
	else
		printf("平局,再接再厉\n");
}

这是game函数的实现,可以看到game函数里嵌套了许多函数,前文讲过,这些嵌套函数的定义和声明分别放在game.c和game.h文件中。接下来我将逐步介绍game函数里的每个步骤,同时讲解这些函数在game.c中是如何实现的

3.2.1创建数组

//存放数据需要一个3*3的二维数组
	char board[ROW][COL] = { 0 };

首先我们想到玩家和电脑下棋,那我们需要一个棋盘并记录他们落子的位置。我们创建一个3*3的二维数组来存储数据,玩家落子后就在这个位置放*,电脑落子后就放 #

这里我们使用宏定义(放在game.h中),万一我们以后想做五子棋,想要一个更大的棋盘,只需更改ROW和COL的·数值即可。

3.2.2初始化棋盘

//初始化棋盘
	InitBoard(board, ROW, COL);

什么是初始棋盘?也就是给数组board的每个元素都赋值为空格这个字符

有老铁可能会有疑问,有这个必要吗?不是一开始赋初值为0了,玩家和电脑落子后再把相应的位置替换成#或者*,这不是刚刚好?

回答是有必要的,因为board这个数组不仅是记录落子信息的,后面我们要打印棋盘也要用到它。

如果一开始每个元素都是0,也就是’\0‘,空字符,打印出来是不占位置的,而 *和 #要占据一个位置,到时候棋盘打印出来岂不是会参差不齐?所以我们把每个元素都换成空格,就算这个地方没有落子,打印出来也好歹会占个位置。

//初始化期棋盘
void InitBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			board[i][j] = ' ';
		}
	}
}

3.2.3显示棋盘

//显示棋盘
	DisplayBoard(board, ROW, COL);

超详解三子棋(优化后)【万字教程包教包会】_第5张图片
我们预想的效果就是这个样子,棋盘由横杠,竖杠,空格和数组元素组成,棋盘旁边有行列标号以便玩家更好的输入自己要下的位置。

怎么打印这个图案呢?我们首先想到的肯定是循环,不然一个一个打印岂不是要累成狗。

我们做如下分解:
首先打印列号,一个for循环搞定,格式就是”空格%-2d空格"(注意竖杠也占据一个位置),这里-2d表示域宽为2并且左对齐。为什么这里要使用域宽而不是直接用空格呢,这是为了方便以后打印更大的棋盘时,列号超过了9,出现两位数,如果不用域宽打印效果就没有那么好。当然在for循环前还要打印几个空格以便对齐。

然后我们可以将含有数据的行(简称数据行)和”—|—|—“行(简称分割行)看成一组,那么总共就有三组,其中最后一组没有分割行,那么我们又可以用一个for循环搞定,加上if语句限制分割行打印

接着我们单独看数据行。最先我们需要打印一个行号,这里我们同样使用域宽打印,原因和上面一样。然后可以把“空格%c空格”和‘|’看成一组,总共有三组其中最后一组没有‘|’,这样就可以用一个for循环配套if语句搞定。

分割行思路和数据行类似。
上代码

//显示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;

	//打印列号
	printf(" ");
	for (i = 1; i <= col; i++)
	{
		printf(" %d  ", i);
	}
	printf("\n");

	//数据行和分割行看成一组,共有三组,最后一组没有分割行
	for (i = 0; i < row; i++)
	{
		//打印行号
		printf("%d", i + 1);

		//打印数据行
		//将元素与“|”看成一组,共有三组,最后一组没有“|”
		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);

			if (j < col - 1)
			{
				printf("|");
			}
		}
		printf("\n");
		printf(" ");

		//打印分隔行
		//将“---”与“|”看成一组,共三组,最后一组没有“|”
		if (i < row - 1)
		{
			for (j = 0; j < col; j++)
			{
				printf("---");

				if (j < col - 1)
				{
					printf("|");
				}
			}
			printf("\n");
		}
	}
}

3.2.4玩家下棋

//玩家下棋
		PlayerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);//显示棋盘

玩家下棋,即让玩家选择坐标,也就是行号和列号,因此我们需要两个变量x和y来存储信息。

scanf函数接收时,我们同样要进行判断看它是否接收成功,方法前文提过,这里不再赘述。

数据接收成功后,我们还要对坐标进行判断,看横纵坐标是否超出范围,例如我输入一个4 4,系统就要提示坐标非法并能重新输入。

如果坐标合法我们还要进行一次判断,看该位置是否已经落子。

如果一切条件都满足后,最后才能把这个位置相对应的数组元素改成*,注意这里可不敢直接写成board[x][y]哈,横纵坐标都要减去一。

上代码

//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("该你走了\n");

	while (1)
	{
		printf("请输入坐标落子>:");
		while (scanf("%d %d", &x, &y) != 2)
		{
			printf("输入非法,请重新输入>:");
			while (getchar() != '\n')
			{
				;
			}
		}
		//判断坐标合法性
		if (x >= 1 && x <= 3 && y >= 1 && y <= 3)
		{
			if (' ' == board[x - 1][y - 1])
			{
				board[x - 1][y - 1] = '*';
				break;
			}
			else
			{
				printf("该位置已经落子,请重新选择\n");
			}
		}
		else
		{
			printf("坐标非法,请重新选择\n");
		}
	}

接下来是一段循环的过程,玩家下棋——电脑下棋——玩家下棋,直至分出结果,所以套上while循环

3.2.5判断输赢

//判断输赢
		ret = IsWin(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}

玩家落子之后电脑是否要落子呢?这要看是否已经出了结果,所以我们要进行判断。

我们写一个判断输赢的函数,如果
玩家胜,返回 *,
电脑胜,返回#,
平局返回Q
未分出结果返回C
这里有个巧妙的设计,玩家和电脑胜利返回的符号,和数组里双方棋子代表的符号相同耶!为什么这么设计呢,先卖个关子。

玩家胜利和电脑胜利其实又可以归纳为一种情况,也就是分出了胜负,什么时候会分出胜负?当棋盘上三子一线时。所以我们把所有请情况都列举出来,横着三条线用for循环,竖着三条线再用for循环,斜着的两条线想不到好办法就单独用两个if语句判断。
若分出胜负,我们直接返回那条线上中的任意棋子的符号就行。例如第一行有三个#,就可以直接返回board[0][0]

这就时那个巧妙的设计,若不这样设计,我们还得判断是哪三个棋子连成一线,然后分情况返回。

若没有分出结果,则继续判断是否是平局。什么时候平局呢?当棋盘满了并且仍未分出胜负时
因为就是没有分出胜负才来判断是否平局,所以我们只需看棋盘是否已经满了。可以设计一个IsFull函数,如果棋盘满了就返回1,没满就返回0。

最后,如果上述两种情况都不满足,那就是游戏还要继续,返回C

上代码

//棋盘是否下满
int IsFull(char board[ROW][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;
}
//判断输赢
	//玩家赢返回*
	//电脑赢返回#
	//平局返回Q
	//继续返回C
char IsWin(char board[ROW][COL], int row, int col)
{
	//分出胜负
	int i = 0;
	for (i = 0; i < row; i++)
	{
		if (board[i][0] == board[i][1] && board[i][1] == board[i][2]
			&& board[i][1] != ' ')
		{
			return board[i][0];
		}
	}

	for (i = 0; i < col; i++)
	{
		if (board[0][i] == board[1][i] && board[1][i] == board[2][i]
			&& board[1][i] != ' ')
		{
			return board[1][i];
		}
	}

	if (board[0][0] == board[1][1] && board[1][1] == board[2][2]
		&& board[1][1] != ' ')
	{
		return board[1][1];
	}


	if (board[0][2] == board[1][1] && board[1][1] == board[2][0]
		&& board[1][1] != ' ')
	{
		return board[1][1];
	}

	//平局
	//棋盘满了返回1,没满返回0;
	if (IsFull(board, row, col) == 1)
	{
		return 'Q';
	}

	//继续
	return 'C';
}

3.2.6电脑下棋

//电脑下棋
		ComputerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);//显示棋盘

这里电脑下棋,其实就是随机产生横坐标和纵坐标,因此我们也要两个变量x和y来存储随机值。

产生随机值要用到rand函数,它产生的是一个0~32767之间的数字,而我们需要的是1到3之间的数字,因此我们要限制范围。怎么限制呢?难道像玩家下棋一样,用if语句判断,如果不在1到3就重新生成吗?这个方法不太好,因为生成1到3的概率差不多是万分之一,虽然最后肯定能实现,但效率就大打折扣了。

所以我们用取模的方法,随机数模上3,范围是0到2,再加上1,不就是我们要的1~3吗?

然后就是和玩家下棋一样,还要判断这个地方是否已经落子。

上代码

//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	//判断坐标合法性
	while (1)
	{
		x = rand() % row;
		y = rand() % col;
		if (' ' == board[x][y])
		{
			board[x][y] = '#';
			break;
		}
	}
}

这里顺便提一下,rand函数一般和srand函数搭配是使用,同时还要用到时间戳,这样才能产生“真正的随机数”,所以我们要在main函数开头加上这样一条语句。关于rand函数的具体用法有兴趣的童鞋自行去了解

srand((unsigned int)time(NULL));

3.2.7判断输赢

//判断输赢
		ret = IsWin(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}

Iswin函数的实现上面已经写过,这里不再重复

3.2.7给出结论

if ('*' == ret)
	{
		printf("恭喜你,你赢了\n");
	}
	else if ('#' == ret)
	{
		printf("很遗憾,你输了\n");
	}
	else
		printf("平局,再接再厉\n");

这个地方可以自由发挥,如果玩家输了可以无情嘲讽他,“不是吧不是吧,连人工智障都下不过~”等等

不知道你有没有注意到我这儿if语句里的写法,常量放在左边,变量放在右边,包括前面的一些位置也是,其实这都是刻意为之的。
因为对与新手而言,写着写着就容易把等号写成赋值符号,那么括号里的条件真假取决于复制号右边的量,如果是0就恒为假,非0恒为真,并且这种错误较为隐蔽,编译器不会报错。
我把常量放在左边后,对常量赋值的操作是不允许的,编译器就会报错,这也是一种编程的良好习惯或者技巧。

四.一些锦上添花以及完整代码

我们其实还可以对这个程序做一些优化。例如当用户选择开始游戏后,可以让系统停顿一会并显示加载中…………等字样。

轮到电脑下棋时,玩家还没反应过来电脑就已经下完了并且重新打印了棋盘,所以我们可以让系统停顿机几秒并显示电脑思考中…………等字样增强游戏体验感。

另外整个过程屏幕上会打印很多内容显得不简洁,所以我们可以在适当的位置加上清屏命令。

其实还有很多地方可以优化,比如把双方棋子换成不同颜色打印出来,这样会更醒目一些,这些内容都可以通过查阅资料的方式进行拓展。

完整代码
test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "game1.h"

void menu()
{
	printf("*****************************************\n");
	printf("*******      1. 三子棋游戏          *****\n");
	printf("*******      2. 退出游戏            *****\n");
	printf("***************************************  \n");

}

void game1()
{
	//存放数据需要一个3*3的二维数组
	char board[ROW][COL] = { 0 };
	//初始化棋盘
	InitBoard(board, ROW, COL);
	//显示棋盘
	DisplayBoard(board, ROW, COL);

	char ret = 0;
	while (1)
	{
		//玩家下棋
		PlayerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);//显示棋盘
		//判断输赢
		ret = IsWin(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}
		//电脑下棋
		ComputerMove(board, ROW, COL);
		DisplayBoard(board, ROW, COL);//显示棋盘
		//判断输赢
		ret = IsWin(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}
	}

	if ('*' == ret)
	{
		printf("恭喜你,你赢了\n");
	}
	else if ('#' == ret)
	{
		printf("很遗憾,你输了\n");
	}
	else
		printf("平局,再接再厉\n");
}

int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("请选择>:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			printf("三子棋游戏开始\n");
			printf("玩命加载中……………………\n");
			Sleep(3000);
			system("cls");
			game1();
			break;
		case 2:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input != 2);

	return 0;
}

game.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "game1.h"

//初始化期棋盘
void InitBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			board[i][j] = ' ';
		}
	}
}

//显示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;

	//打印列号
	printf(" ");
	for (i = 1; i <= col; i++)
	{
		printf(" %d  ", i);
	}
	printf("\n");

	//数据行和分割行看成一组,共有三组,最后一组没有分割行
	for (i = 0; i < row; i++)
	{
		//打印行号
		printf("%d", i + 1);

		//打印数据行
		//将元素与“|”看成一组,共有三组,最后一组没有“|”
		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);

			if (j < col - 1)
			{
				printf("|");
			}
		}
		printf("\n");
		printf(" ");

		//打印分隔行
		//将“---”与“|”看成一组,共三组,最后一组没有“|”
		if (i < row - 1)
		{
			for (j = 0; j < col; j++)
			{
				printf("---");

				if (j < col - 1)
				{
					printf("|");
				}
			}
			printf("\n");
		}
	}
}

//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("该你走了\n");

	while (1)
	{
		printf("请输入坐标落子>:");
		while (scanf("%d %d", &x, &y) != 2)
		{
			printf("输入非法,请重新输入>:");
			while (getchar() != '\n')
			{
				;
			}
		}
		//判断坐标合法性
		if (x >= 1 && x <= 3 && y >= 1 && y <= 3)
		{
			if (' ' == board[x - 1][y - 1])
			{
				system("cls");
				board[x - 1][y - 1] = '*';
				break;
			}
			else
			{
				printf("该位置已经落子,请重新选择\n");
			}
		}
		else
		{
			printf("坐标非法,请重新选择\n");
		}
	}
	
}

//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("该电脑走了,思考中………………\n");
	Sleep(2000);
	system("cls");
	//判断坐标合法性
	while (1)
	{
		x = rand() % row;
		y = rand() % col;
		if (' ' == board[x][y])
		{
			board[x][y] = '#';
			break;
		}
	}
}

int IsFull(char board[ROW][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;
}

//判断输赢
	//玩家赢返回*
	//电脑赢返回#
	//平局返回Q
	//继续返回C
char IsWin(char board[ROW][COL], int row, int col)
{
	//分出胜负
	int i = 0;
	for (i = 0; i < row; i++)
	{
		if (board[i][0] == board[i][1] && board[i][1] == board[i][2]
			&& board[i][1] != ' ')
		{
			return board[i][0];
		}
	}

	for (i = 0; i < col; i++)
	{
		if (board[0][i] == board[1][i] && board[1][i] == board[2][i]
			&& board[1][i] != ' ')
		{
			return board[1][i];
		}
	}

	if (board[0][0] == board[1][1] && board[1][1] == board[2][2]
		&& board[1][1] != ' ')
	{
		return board[1][1];
	}


	if (board[0][2] == board[1][1] && board[1][1] == board[2][0]
		&& board[1][1] != ' ')
	{
		return board[1][1];
	}

	//平局
	//棋盘满了返回1,没满返回0;
	if (IsFull(board, row, col) == 1)
	{
		return 'Q';
	}

	//继续
	return 'C';
}

game.h

#pragma once
#define ROW 3
#define COL 3
#include 
#include 
#include 
#include 

//函数声明
//初始化棋盘
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);

以上就是三子棋的全部内容了,码字不易~~如果觉得这篇文章对你有帮助的话,别忘记留下你的哟,文章中有错漏指出欢迎留在下方评论区
超详解三子棋(优化后)【万字教程包教包会】_第6张图片

你可能感兴趣的:(c++,学习,学习方法)