C语言实现三子棋游戏个人小结

实现一个程序个人理解:从一个入口到另一个入口重复这个过程直到回头看最初的入口程序结束。

能把每一个入口设计的合理而且不冲突就像每一个故事线都对应着不同的结局。

话不多说直接上代码

#include"game.h"
void game()
{
	char board[ROW][COL] = { 0 };
	InitBoard(board, ROW, COL);//初始化表格
	Print(board, ROW, COL);//打印表格
	printf("例如\"1 2\"选择的是第一行第二列的位置\n");
	while (1)
	{
		Player(board, ROW, COL);//玩家下棋
		Print(board, ROW, COL);//打印表格
		if (Win(board, ROW, COL) == '@')//判断输赢返回 J-继续 @-玩家胜利 #-电脑胜利 C-平局
		{
			printf("恭喜玩家获得胜利\n");
			break;
		}
		printf("电脑选择后\n");
		Sleep(1000);
		CPTR(board, ROW, COL);//电脑下棋
		Print(board, ROW, COL);//打印表格
	    if (Win(board, ROW, COL) == '#')
		{
			printf("你失败了电脑胜利\n");
			break;
		}
		else if (Win(board, ROW, COL) == 'C')
		{
			printf("平局\n");
			break;
		}
	}
}
void menu()
{
	printf("#######################\n");
	printf("####### 1.play ########\n");
	printf("####### 0.exit ########\n");
	printf("#######################\n");
}
int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("请选择:>\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("速速来把棋\n");
				game();
				break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

在这里自己编辑的game.h头文件引用了<stdio.h><<time.h><stdlib.h><windows.h>

引用<time.h>和<stdib.h>主要是想设置随机值,因为之后电脑是在棋盘上随机生成一枚棋子。

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

为了之后调用rand()设置随机值,而且每次程序执行时生成的随机值并不相同。

所以将srand()放入在主函数中并用time()函数的返回值作为随机数的生成器。

do
{
	menu();
	printf("请选择:>\n");
	scanf("%d", &input);
	switch (input)
	{
	case 1:
		printf("速速来把棋\n");
			game();
			break;
	case 0:
		printf("退出游戏\n");
		break;
	default:
		printf("选择错误请重新选择\n");
		break;
	}
} while (input);

这一步就是设置最开始的入口了,想留下来试一试的扣个1,不想留下来也不强求,而为了保证每个结局都有它的故事,所以你莫得选择。

那么是怎么实现的呢,先打印菜单,这个可以自定义随机更改,但由字符组成的小小菜单怎么改都那味儿。之后根据菜单的提示选择玩还是不玩,所以输入输出是必不可少,而这里的点不是这里,而是在怎样选择后跳转到下一个入口,并且能够在达到目的后终止这一切。这种操控剧情的大杀器要是放在小说里又能水个几章。

这里就用到了switch语句,当选择1时跳转到自定义的游戏函数game(),并且每个情况后加break跳出,也就意味着这些情况不会一起发生。而要是在玩完之后不想玩就可以刚好选择0退出,所以在外部又套了个do while循环,当选择零时input即控制了switch语句又控制了while语句,两边都判断跳出程序就结束了。

三子棋的实现

这就是game()函数里的内容

char board[ROW][COL] = { 0 };
InitBoard(board, ROW, COL);//初始化表格
Print(board, ROW, COL);//打印表格
printf("例如\"1 2\"选择的是第一行第二列的位置\n");
while (1)
{
	Player(board, ROW, COL);//玩家下棋
	Print(board, ROW, COL);//打印表格
	if (Win(board, ROW, COL) == '@')//判断输赢返回 J-继续 @-玩家胜利 #-电脑胜利 C-平局
	{
		printf("恭喜玩家获得胜利\n");
		break;
	}
	printf("电脑选择后\n");
	Sleep(1000);
	CPTR(board, ROW, COL);//电脑下棋
	Print(board, ROW, COL);//打印表格
    if (Win(board, ROW, COL) == '#')
	{
		printf("你失败了电脑胜利\n");
		break;
	}
	else if (Win(board, ROW, COL) == 'C')
	{
		printf("平局\n");
		break;
	}
}

首先要大体的逻辑是用一个二维数组来实现三子棋数据的存储。 

再通过下棋的过程改变数组中的数据达到下棋的目的,直到达成了某种规则条件,游戏结束。

所以先创建一个二维数组

char board[ROW][COL] = { 0 };

ROWCOL是宏定义的常量,为什么这里不用两个3来创建,这就要提到数组的一个很严格的要求数组的创建[  ]里只能放常量表达式,那么也就意味着如果想要进行更改,用变量已经是死路一条,这点真的很头疼,自己做了很多题逻辑上都默认了数组空间可更改,后来才回过味儿来,但确实必须要这样,不然数组会变得更麻烦。而用了宏去定义虽然放进去的仍是常量,但是给了可更改的可能。可更改也就以为着能够不仅能够实现三子棋,其他子也能做到。

在头文件game.h定义:

#define ROW 3
#define COL 3

值得注意的是创建这一步操作,如果不把数组里的元素设置成空格,就不会产生原本是空的一块空间把棋下进去的效果。数组在不完全初始化时放置的都是'\0'。所以我们需要可以用一个函数来专门进行数组的初始化。

InitBoard(board, ROW, COL);//初始化表格

在game()函数中创建数组后调用,便可达到效果。

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

初始化函数,逻辑就是用了一个for循环中嵌套另一个for循环的形式,这样可以掉出每一个数组中的元素对其进行赋值操作,如图全部赋值成空格即达成目的。之后自定义的打印函数也是同理。

初始化之后要做的事就是打印,可以先让玩家看到棋盘的分布方便后续选择,也可以验证自定义的InitBoard函数到底有没有被初始化成功。

Print(board, ROW, COL);//打印表格

值得注意的是若给棋盘的每个元素加上分割线,就需要在原有的逻辑下进行一些微调

void Print(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++)
		{
			printf(" %c ", board[i][j]);
			if(j != col - 1)
			printf("|");
		}
		printf("\n");
		if (i != row - 1)
		{
			while (j)
			{
				printf("---");
				if (j != 1)
					printf("|");
				j--;
			}
			printf("\n");
		}
	}
}

在每一次for循环最后一次循环的时候,不再进行表格的打印才会出来这种效果

 打印的是5行另外两行是分割线,单独出来打印,如果不设置最后一次循环终止打印那么最后一行打印的时候还会再打印一次分割线,看起来就显得身子很大头很小了,列也是同理。

玩家(电脑)下棋:

同样创建一个函数来实现玩家(电脑)对游戏的互动

Player(board, ROW, COL);//玩家下棋
CPTR(board, ROW, COL);//电脑下棋

对游戏的互动,也就是对所创建数组元素的改变,然后再通过再次打印数组的方式使玩家看到更改。

void Player(char board[ROW][COL], int row, int col)
{
	
	int i = 0;
	int j = 0;
	again:
    printf("请输入您选择的坐标\n");
    scanf("%d %d", &i, &j);
	if (i > row||j > col)
	{
		printf("无法在该位置落子\n");
        goto again;
	}
	if (board[i-1][j-1] == ' ')
	{
		board[i-1][j-1] = '@';
	}
	else
	{
		printf("该格子以被占用请重新选择\n");
		Player(board, ROW, COL);
	}
}

还是用输入输出函数来告知和确定,然后确定玩家输入值的准确性,如果超出棋盘识别的位置用goto语句回溯到输入的位置再次输入。然后下边再简单判断一下格子有没有被占用,如果被占用再重新调用Player函数进行选择。第一次错误回溯使用goto语句实现,另一次错误回溯是用重新调用函数来实现。这样就能够保证程序能够执行到下一步,一定是这里玩家成功选择后,空格被改成@才能继续执行。

电脑下棋不会有其他的可能,值都是可控的,所以不会有一些不必要的选择语句。

void CPTR(char board[ROW][COL], int row, int col)
{
	while (1)
	{
		int i = rand() % row;
		int j = rand() % col;
		if (board[i][j] == ' ')
		{
			board[i][j] = '#';
			break;
		}
	}

相较于Player函数,CPTR函数用rand()生成随机值然后再用row和col进行取模运算,这样值就可控在0~row-1(col-1)之间,再判断一下随机值生成的空间有没有占用就可以,外部再套个while循环在成功选择#后跳出循环同时函数结束。

游戏规则(输赢判断)

三子棋的输和赢无非就是有无三个连成线的符号,这里我觉得我用的方法就很笨了

Win(board, ROW, COL)//判断输赢返回 J-继续 @-玩家胜利 #-电脑胜利 C-平局
char Win(char board[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	int e = 0;
	int a = 0;
	int b = 0;
	int c = 0;
	for (i = 0; i < row; i++)
	{
		a = 0;
		for (j = 0; j < col; j++)
		{
			
			if (board[i][j] == '@')
			{
				a++;
			}
			else if (board[i][j] == '#')
			{
				a--;
			}
		}
		if (a == row)
		{
			return '@';
		}
		else if (a == - row)
		{
			return '#';
		}
		a = 0;
		for (j = 0; j < col; j++)
		{
			if (board[j][i] == '@')
			{
				a++;
			}
			else if (board[j][i] == '#')
			{
				a--;
			}
			if (board[j][i] != ' ')
			{
				e++;
			}
		}
		if (a == row)
		{
			return '@';
		}
		else if (a == - row)
		{
			return '#';
		}
		if (board[i][i] == '@')
		{
			b++;
		}
		else if (board[i][i] == '#')
		{
			b--;
		}
		if (b == row)
		{
			return '@';
		}
		else if (b == - row)
		{
			return '#';
		}
		if (board[row - i -1][i]=='@')
		{
			c++;
		}
		else if (board[row - i + 1][i] == '#')
		{
			c--;
		}
		if (c == row)
		{
			return '@';
		}
		else if (c == -row)
		{
			return '#';
		}
	}
	if (e == row * col)
	{
		return 'C';
	}
	else
	{
		return 'J';
	}
}

这段是纯粹的一个一个去判断当前数组符合哪个条件,基本逻辑还是通过for循环去调数组的值再用一个值去记录次数,最后由判断语句判断那个值的次数是否符合要求。例判断是否有字符连成一行,就可以用两个for循环嵌套如下:

for (i = 0; i < row; i++)
	{
		a = 0;
		for (j = 0; j < col; j++)
		{
			
			if (board[i][j] == '@')
			{
				a++;
			}
			else if (board[i][j] == '#')
			{
				a--;
			}
		}
		if (a == row)
		{
			return '@';
		}
		else if (a == - row)
		{
			return '#';
		}
    } 

需要注意的是每次一行调用结束时,也就是其内的for循环结束时,需要重置a的值。这样才能保证每次的结果都是独立的。而判断是否有字符连城一列在再以上基础上,在判断语句if的条件中将j和i置换位置即可排查列。两个对角线的判定也是同理。在判断平局还是继续上,也是用判断语句和一个值在循环中不断记录数组中每个元素是否为空格的次数。最后再根据此条件判断,棋盘满还是没满,如果都被选择过并且不再是空格,则返回符号C平局,反之则返回符号J继续。这个判断也应该放到最后,因为有可能最后一次下棋过程棋盘下满,但是最后这一次扭转乾坤使符号连成了线,那么一定是要先执行获胜的语句让其返回,而不是先判断是否为平局。

最后再来俯瞰一下整个game()函数

void game()
{
	char board[ROW][COL] = { 0 };
	InitBoard(board, ROW, COL);//初始化表格
	Print(board, ROW, COL);//打印表格
	printf("例如\"1 2\"选择的是第一行第二列的位置\n");
	while (1)
	{
		Player(board, ROW, COL);//玩家下棋
		Print(board, ROW, COL);//打印表格
		if (Win(board, ROW, COL) == '@')//判断输赢返回 J-继续 @-玩家胜利 #-电脑胜利 C-平局
		{
			printf("恭喜玩家获得胜利\n");
			break;
		}
		printf("电脑选择后\n");
		Sleep(1000);
		CPTR(board, ROW, COL);//电脑下棋
		Print(board, ROW, COL);//打印表格
	    if (Win(board, ROW, COL) == '#')
		{
			printf("你失败了电脑胜利\n");
			break;
		}
		else if (Win(board, ROW, COL) == 'C')
		{
			printf("平局\n");
			break;
		}
	}
}

在这个过程要一定要清楚每一个函数的功能,然后为了达成目的进行合理的布局。之前我们写的函数都只能执行一次,下棋每次也只能下一个,这是规矩,而让整个游戏你下一次我下一次进行起来就需要一个大循环,这个循环中包括双方下棋和胜负的判定。而判定到可以让游戏结束的条件就跳出循环,这也是为什么win()函数需要返回字符,那个返回的字符就是判定的结果,用判断语句判断是否为那个平局,获胜所对应的字符,是的话直接break就可跳出整个游戏循环,游戏结束。中间用了Sleep()函数(需要引用<windows.h>)来延长了电脑表格打印的时间,这样运行起来更有在下棋的感觉,不然每次一输入完自己想下的棋,电脑所选择的棋几乎是同时也打印出来了,跑起来的时候不太舒服。

这段代码是自己在4.19号完成的,5.3号的时候自己又编写了一个。逻辑跟这代比较基本相同,一个想拿出来说说的不同点是,自己在函数传参的时候没有再传行数和列数。

InitBoard(board, ROW, COL);//初始化表格
Initarr(arr);//初始化表格

具体逻辑也是一样的,不过是把参数该放的地方替换成了宏定义的常量,比较之前传进去的也是宏定义的常量,那我头文件引用都引用了为啥不能直接拿过来用呢。事实尝试后确实直接拿过来用好像没什么影响。

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 Initarr(char arr[ROWS][COLS])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < ROWS; i++)
	{
		for (j = 0; j < COLS; j++)
		{
			arr[i][j] = ' ';
		}
	}
}

这样做我觉得应该能节约一点函数传参时压栈操作的内存空间,不过也只是一次尝试,具体要怎样还得要看自己之后的学习了。

这次的分享就到这里了 。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值