游戏主函数

首先,我们设计三子棋时,要设计选择进入游戏退出游戏的选项,以及输入错误内容时,重新选择的程序。
将游戏标题函数设置为

menu()

将游戏主题函数设置为

game()

若在选择时,输入内容不在程序设定范围内,则要求使用者重新输入。所以程序此处应有一个循环。
因此主函数程序设置如下:

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 2:
			printf("退出游戏/n");
			input -= 2;
			break;
		default:
			printf("输入错误,请重新选择/n");
			break;
		}
	} while (input);
	return 0;
}

当输入1时,进入case 1:运行game()函数;
当输入2时,进入case 2:打印“退出游戏”并跳出switch循环,在dowhile结尾进行检测时,检测结果为0,跳出循环,退出程序;
当输入其他字符时,进入default,打印‘“输入错误,请重新选择”。因为input非零,所以继续进行dowhile循环。

代码实现的初步工作

首先我们创建游戏程序文件game.c,和头文件game.h
为了提高系统的可操作性,我们先将程序中的行与列定义为ROW和COL
所以头文件中预处理指令如下:

#define _CRT_SECURE_NO_WARNINGS 1   //scanf函数防止报错
#include <stdio.h>                  //printf函数       
#include <time.h>                   //time函数,产生随机值(后面讲)

#define ROW 3   //行
#define COL 3   //列

然后在game.c和主函数所在文件test_tic_tac_toe.c中引用头文件:

#include “game.h”

接着设计主函数中的==menu()==函数。因为打印游戏菜单不需要返回值,所以函数类型void即可:

void menu()
{
	printf("************************/n");
	printf("******** 1.play ********/n");
	printf("******** 2.exit ********/n");
	printf("************************/n");
}

游戏实现原理与程序优化

根据主函数的设计,我们继续设计game()函数:
同理不需要返回值,所以函数类型为void:

void game()
{
	char board[ROW][COL];  //存储数据,二维数组
	//初始化棋盘
	//打印棋盘
	while (1)
	{
		//玩家落子
		//判断游戏能否继续进行
		//电脑落子
		//判断游戏能否继续进行(胜、负和平局,若存在三者之一则跳出while循环)
	}
	//判断输赢,打印胜负结果
}

如上文所示,我们先整理出了一个大致的代码设计方向。

初始化棋盘

首先我们需要将数组(棋盘中9个落子点的位置)初始化为“ ”(空格)

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] = ' ';
		}
	}
} 

然后再在头文件game.h中声明,并做好注释:

//初始化棋盘
void InitBoard(char board[ROW][COL], int row, int col);

打印棋盘

我们的棋盘由“|”(回车上面)和“-”(减号)组成
所以依次为

   |   |
---|---|---   //三个---和一个|
   |   |
---|---|---
   |   |

程序可以直接选择直接设置:

void DisplayBoard(char board) //打印棋盘
{
	printf(" %c | %c | %c ", board[0][0],board[0][1],board[0][2]);
	printf("---|---|---");
	printf(" %c | %c | %c ", board[1][0],board[1][1],board[1][2]);
	printf("---|---|---");
	printf(" %c | %c | %c ", board[2][0],board[2][1],board[2][2]);
}

但是缺点很明显,如果棋盘大小不是3×3,那么这个程序就得重新编写了。
所以我们稍微修改亿下:

void DisplayBoard(char board[ROW][COL], int row, int col) //打印棋盘
{
	int i = 0;
	for (i = 0; i < ROW; i++)
	{
		int j = 0;
		for (j = 0; j < COL; j++)
		{
			printf(" %c ", board[i][j]);    //初次打印棋盘都为空格,之后玩家电脑落子后,打印的是落子内容(*或#)
			if (j < COL - 1)    //打印的|次数要少一次
			{
				printf("|");
			}
		}
		printf("/n");
		int k = 0;             
		if (i < ROW - 1)
		{
			for (k = 0; k < COL; k++)
			{
				printf("---");
				if (k < COL - 1)   //打印的|次数要少一次
				{
					printf("|");  
				}
			}
			printf("/n");
		}
	}
} 

然后我们同样在头文件game.h中声明此函数,并做好注释:

//打印棋盘
void DisplayBoard(char board[ROW][COL], int row, int col);

这样我们可以用此函数做到初次打印空棋盘,以及之后每次玩家/电脑落子后的棋盘。

玩家下棋

玩家下棋时,我们可以让玩家输入坐标,以此确定落子位置。
这样具体到程序上,就是对上文初始化的二维数组进行赋值。
但是玩家的人类思维中,坐标是1,2,3这样顺序的,而程序是以0,1,2的次序排序。所以我们要将玩家输入的坐标-1,以此对标数组中的从0开始排序。

此外,我们还要玩家落子点有空位,且存在的问题。所以我们需要一个if的选择语句进行验证,若不满足条件则要求玩家重新输入。因此还要一个while循环
若玩家输入坐标存在,则对二维数组的此项赋值为“*”。
整理思路,我们可以敲出以下代码:

void PlayerMove(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	while (1)
	{
		printf("玩家落子,请输入坐标:/n");
		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("坐标已被占用,请重新输入");
			}
		}
		else
		{
			printf("坐标错误,请重新输入");
		}
	}

同样,在头文件game.h中声明此函数,并做好注释:

//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col); 

电脑下棋(这里挖个坑)

电脑落子,因为编者目前还不会制作更好的程序,做出一个智能的对战AI,所以电脑落子采用随机的方式。
在头文件game.h中引用

#include <time.h>

再在主函数的dowhile循环前加入

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

这样,我们就可以产生随机数了。
但是由于此随机数的范围巨大,所以我们需要将它的范围缩小到0~2,以此对应二维数组中的位置。此处最简单的就是用取模符号了(%)。

(挖坑:以后学到如何做这么一个智能对战的程序后,我再对这里的电脑落子程序进行优化)

其他的整体思路和玩家落子程序的思路类似,但!是!:
因为我们不需要机器报出位置已被占用的信息,所以不需要让它像玩家输入的程序一样打印“请重新输入”,只需要让机器重新选择坐标进行落子(重新选择二维数组的某一项赋值成“#”)

void ComputerMove(char board[ROW][COL], int row, int col)
{
	printf("电脑落子:/n");
	while (1)
	{
		int x = rand() % ROW;
		int y = rand() % COL;
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

同样,在头文件game.h中声明此函数,并做好注释:

//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col);

验证胜负机制与程序的继续进行

根据三子棋的规则,只要连续的三个子相同就可以判断胜负了。
所以对于程序是否需要,能否继续运行,总共存在以下四种情况:

判断输赢
1.玩家胜利
2.电脑胜利
3.棋盘满了,平局
4.仍有空位,游戏继续

所以此处我们设计一个返回类型为char的函数:
首先对每行,每列,两个对角线进行判断是否达成三点一线同一种棋子,且不为空格。
为了减少程序的运行次数,代码的总体量,我们将玩家/电脑的返回值分别设置为“*”和“#”,与玩家/电脑的落子一致(省去了一半的代码)
所以我们设计如下:

判断输赢
1.玩家胜利,返回*
2.电脑胜利,返回#
3.棋盘满了,平局,返回D
4.仍有空位,游戏继续,返回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 < ROW; j++)
		{
			if (board[i][j] != ' ')
			{
				return 0;
			}
		}
	}
	return 1;
}

然后我们就可以设计出判断程序如何继续执行的函数了:

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][0] != ' ')
		{
			return board[i][0];
		}
	}
	//判断列
	for (i = 0; i < COL; i++)
	{
		if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
		{
			return board[0][i];
		}
	}
	//判断对角线
	if ((board[0][0] == board[1][1] && board[0][0] == board[2][2] && board[0][0] != ' ') || (board[0][0] == board[1][1] && board[0][0] == board[2][2] && board[0][0] != ' '))
	{
		return board[1][1];
	}
	//判断平局(棋盘满没满)  平局返回1,还有空位返回0
	int ret = IsFull(board, ROW, COL);
	if (ret == 1)
	{
		return 'D';  //返回值为1,平局
	}
	else
	{
		return 'C';  //返回值为0,游戏仍在继续
	}
}

同样,在头文件game.h中声明函数,并做好注释:

//判断输赢
//1.玩家胜利返回*
//2.电脑胜利返回#
//3.平局返回D
//4.游戏继续返回C
char IsWin(char board[ROW][COL], int row, int col);
//判断平局(棋盘满没满)  平局返回0,还有空位返回1
int IsFull(char board[ROW][COL], int row, int col);

这时候我们回到开始的game()函数:

```c
void game()
{
	char board[ROW][COL];  //存储数据,二维数组
	//初始化棋盘
	//打印棋盘
	while (1)
	{
		//玩家落子
		//判断游戏能否继续进行
		//电脑落子
		//判断游戏能否继续进行(胜、负和平局,若存在三者之一则跳出while循环)
	}
	//判断输赢,打印胜负结果
}

依次将设计好的函数进行代入,完善整理程序,结果如下:

void game()
{
	char board[ROW][COL];  //存储数据,二维数组
	InitBoard(board, ROW, COL); //初始化棋盘
	DisplayBoard(board, ROW, COL); // 打印棋盘
	int 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");
	}
	if (ret == '#')
	{
		printf("玩家胜利/n");
	}
	if (ret == 'D')
	{
		printf("平局/n");
	}
	DisplayBoard(board, ROW, COL); // 游戏结束,再次打印棋盘
}

以上就是三子棋的整体思路了

BUG彩蛋(我也不知道为什么会这样)

本来程序是好好的,按照设计进行输入:
[C语言小白]三子棋小程序_二维数组
所以我自信满满地把它的exe文件发给了校友,让他玩玩看。
结果他输入了一个英文字符,然后敲下回车……
结果就这样了:
[C语言小白]三子棋小程序_c语言_02
没错,无限打印。
这里是啥原理我就不明白了。
有大佬能解答一下咩?