N子棋:从井字棋到五子棋再到任意阶数的拓展(C语言版)

讲在前面:

个人认为:最耗时间也是最值得一看的是判断函数部分

本文为新手自己琢磨debug若干天后终于写出的一版,原是看@鹏哥C语言 课程时写的,但是里面对于获胜条件的判断仅仅适用于3×3的井字棋,且网上也没有很好的代码,于是在兴趣驱动下,自己写了一个一百多行的判断函数,适用于任意大棋盘任意数目(即n子棋)的获胜判断

可能有很多可以优化的地方,如有错误还请指出纠正,十分感谢!

一、整体总览

我们先做如下规定

ROW——代表行数;

COL——代表列数;

WIN——代表赢需要连成线的子的数目;

' * '——代表玩家所下的棋子;

' # '——代表电脑所下的棋子;

我们不妨在头文件中直接定义

#define ROW 3
#define COL 3
#define WIN 3

 以后想要拓展为n子棋,只需在此修改三个数值即可

对于一个N*N的棋盘来说,最好的方式就是用二维数组来创建

现就可以在游戏主体函数中创建一个二维数组

char board[ROW][COL];

再来看这个游戏应有的部分

一、菜单

二、游戏函数主体

  1. 打印棋盘(&&初始化)
  2. 执子操作
  3. 判断输赢
    //初始化棋盘
    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 Judge(char board[ROW][COL], int row, int col);

    (↑我的game.h头文件最终效果)(↓我的game()函数总体效果)

void game()
{   //创建变量
	char 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 = Judge(board, ROW, COL);//得到返回值
		//if (ret == '*')   判断是否结束
		//………………
		ComputerMove(board, ROW, COL);//电脑下棋
		DisplayBoard(board, ROW, COL);//展示棋盘
		ret = Judge(board, ROW, COL);//得到返回值
		//if (ret == '*')   判断是否结束
		//………………
	}
}

接下来就是分步实现这些步骤

二、内容实现

1.菜单

这部分没什么好说的,创建一个menu()函数即可

void menu()
{
	printf("****************三子棋****************\n");
	printf("**************************************\n");
	printf("***************1.  开始***************\n");
	printf("**************************************\n");
	printf("***************0.  退出***************\n");
	printf("**************************************\n");
	printf("请输入数字进行下一步操作:>");
}

这种1开始、0退出的做法主要是方便快速地进行or退出循环

我们可以在main() 函数里写一个循环,方便一轮结束后继续下一轮

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

(上来先干事再判断适合用do while循环)

根据写一点测试一点的原则,可以先在case 1中把game()换成printf(),看是否出现问题

一般而言,这一步没什么难度,不过后期可以接着嵌套switch()将游戏丰富为可以选择人机or双人、井字棋or五子棋

2.打印棋盘(&&初始化)

在打印之前应该先初始化棋盘,即让board[ROW][COL]数组内所有元素均为空格‘ ’,用两层for循环即可轻松实现

void InitBoard(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++)
			board[i][j] = ' ';
	}
}

再来看一下3 * 3棋盘的预览图

为了图省事当然可以一行一行把九个格子都打印下来,但别忘了我们是为了能够推广到任意大小的n子棋,所以最好是利用两个for()循环语句逐个打印

* ’处就是我们应该下的棋,可以看出来,一般而言,比较规矩的棋盘的一小格应该是

“空格 %c 空格|”,下一行是“---|”

但是仍有特殊的两处:

  1. 最后一行没有换行后的“---|”;
  2. 最右边一列没有“|”,取而代之的应该是换行

所以我们可以写出如下的代码 :

void DisplayBoard(char board[ROW][COL], int row, int col)
{

		for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);
			if (j != row - 1)
				printf("|");
		}//括号中是一行的东西
		printf("\n");//这一句表示下打印完一行、在打印下一行前换行
		if (i != row - 1)
		{
			for (j = 0; j < col; j++)
			{
				printf("---");
				if (j != col - 1)
					printf("|");
             }
			printf("\n");
		}	
	}
	printf("\n\n");//单纯想让打印出来的排版更好看
}

当然,在后续如果想改成更大的棋盘时,繁多的格子会让人眼花缭乱,最好是对这一部分进行优化

可以在最上面一行就进行一个for循环打印列数,然后再在每一行开头再进行打印行数

void DisplayBoard(char board[ROW][COL], int row, int col)
{
    //补充:打印列数
	int i = 0;
	printf(" ");//因每一行开头多打一个数字,多打出一个空格进行对齐
	for (i = 0; i < row; i++)
	{
		if(i < 10)
		printf("  %d ", i + 1);
		if(i >= 10)//当i是两位数时,空格需要少打一个来保证不错位
			printf(" %d ", i + 1);
	}
	printf("\n");
    
    //开始打印棋盘
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			if (j == 0)//在每一行的第一列打印行数
				printf("%2d %c ", i+1, board[i][j]);
              //选用%2d是为了防止行数大于10后出现错位,故1~9都用“ i”表示
			if (j != 0)
				printf(" %c ", board[i][j]);
			if (j != row - 1)
				printf("|");
		}
		printf("\n");
		if (i != row - 1)
		{
			for (j = 0; j < col; j++)
			{
				if (j == 0)//同理,在每一行的第一个---前打印两个空格,防止错位
					printf("  ---");
				if (j != 0)
					printf("---");
				if (j != col - 1)
					printf("|");

			}
			printf("\n");
		}	
	}
	printf("\n\n");
}

里面的一些细节都在注释里注明了

这样优化后的DisplayBoard()就完成了,在15*15的棋盘里下五子棋比原来容易了许多

 3.下棋操作

①玩家执子

首先要声明的一点是,玩家不是程序员,所以棋盘的开始不是(0,0),而是(1,1),所以要注意玩家输入的坐标值都要 -1

逻辑很简单,玩家输入坐标,将这个坐标对应的‘   ’改为‘ * ’

同时注意对应坐标不是空格的、超出范围的都要报错

void PlayerMove(char board[ROW][COL], int row, int col)
{
	
	printf("轮到玩家下棋\n");
	printf("请输入坐标:>\n");
	while(1)
	{
		int x = 0;
		int y = 0;
		scanf("%d %d", &x, &y);
		if (x > row || y > col || x < 1 || y < 1)
			printf("坐标超出范围,请重新输入\n\n");
		else if (board[x - 1][y - 1] != ' ')
			printf("该处已有棋子,请重新输入\n\n");
		else
		{
			board[x - 1][y - 1] = '*';
			break;
		}
	}
}

十分简单

②电脑执子 

目前我的技术力不够……只能完成随机下棋,等我学到算法再来考虑优化出更加智能的电脑

对于随机函数,需要先在main()函数里面整一个

#include <time.h>
​srand((unsigned int)time(NULL))

 接下来就是在我们的ComputeMove()里调用rand()函数了

大体上基本与PlayerMove()一致,但是电脑输错了不需要报错,默默地执行到下一步即可

void ComputerMove(char board[ROW][COL], int row, int col)
{
	printf("轮到电脑下棋\n\n");
	Sleep(300);//慢一点点,让人也有反应的时间
	while (1)
	{
		int x = rand() % row;
		int y = rand() % col;//这里对row、col取余,可以得到0~row - 1的数,完美符合我们的预期
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

 总而言之,这两个步骤操作起来算是最简单的了

4.判断函数

这一部分是耗费精力最多、但又是整个棋局中相当关键的一步,我将会使用函数

char Judge(char board[ROW][COL], int row, int col)

用char类型返回值,从返回结果上分为四种情况

  1. 返回' * ',代表玩家获胜;
  2. 返回' # ',代表电脑获胜;
  3. 返回'C',代表无事发生,进行下一步执子;(不过我后续也没有用到这个C)
  4. 返回'Q',代表平局;

除此之外,还应考虑函数内部分为三大模块:

  1. 判断行、列;
  2. 判断斜线;
  3. 判断平局or继续; 

①判断行、列 

首先讲清楚判断是否多子连成一条连续的直线的逻辑

开始先用两个for()循环嵌套,方便在判断完一个格子之后,接下来对所有的格子重复判断操作,直到找到多子连成线的情况

//判断行
int i1 = 0;
int count_row = 0;
for (i1 = 0; i1 < row; i1++)
{
	int j1 = 0;
	for (j1 = 0; j1 < col; j1++)
	{
		if (board[i1][j1] != ' ' && j1 < col - 1)//注意j的范围,防范数据溢出!!!
		{
				if (board[i1][j1] != board[i1][j1 + 1]) 
					count_row = 0;//如果中间有一次连不上,就要把计数器归零
				else
					count_row++;//能连上就把计数器+1
				if (count_row == WIN - 1)
					return board[i1][j1];//可以得到连成线的棋子类型
		}
	}
	count_row = 0;//注意每一行循环结束之后要把计数器归零
}

一定一定要注意i、j在行、列、斜线的判断中都是有范围限定的!防范数据溢出!否则会出现跨行、跨列连成线的bug!

列与行是同理,只是改一改i、j的位置和变量名称罢了

//判断列
int j2 = 0;
int count_col = 0;
for (j2 = 0;j2 < col; j2++)
{
	int i2 = 0;
	for (i2 = 0; i2 < row; i2++)
	{
		if (board[i2][j2] != ' ' && i2 < row - 1)
		{
			if (board[i2][j2] != board[i2 + 1][j2])
				count_col = 0;
			else
				count_col++;
			if (count_col == WIN - 1)
				return board[i2][j2];
		}
	}
	count_col = 0;
}

②判断斜线☆☆☆ 

与别的只能死板地判断[i][i] == [i+1][i+1]的代码不同,我经过两三天思考修改的这版代码可以判断在任意区域连成的斜线,因此可以实现推广到五子棋乃至n子棋(自夸一下,因为新手自己想并写出来后真的很自豪)

首先还是强调注意i、j的取值范围,否则会出现跨行、跨列、首尾连线的bug

原理依旧是采取计数器,board[a][b] == board[c][d]且不为空格就计数器+1,当count == WIN - 1时就代表获胜

注意:每次处在一个循环结束,和下一个循环开始之间,就代表着没有一方获胜,所以要把计数器清零

余下的讲解放在代码的注释中自行体会

//判断斜
	int i3 = 0;
int count_dia1 = 0;//计数器1
int count_dia2 = 0;//计数器2
//左上右下
for (i3 = 0; i3 < row; i3++)
{
	int j3 = 0;
	for (j3 = 0; j3 < col; j3++)
		if (board[i3][j3] != ' ')
		{
			count_dia1 = 0;//每次循环开始前要把计数归零
			int x = i3;
			int y = j3;
            //这里选择使用x、y来代替i、j是怕后续+1的操作影响到了i、j循环的进行
			while (x < row -1 && y < col - 1)
            //注意到后面的判断有[x+1][y+1],故x、y不能是代表最后一行、列的,防数据溢出
			{
				if (board[x][y] != ' ')
				{
					if (board[x][y] != board[x + 1][y + 1])
						count_dia1 = 0;//与判断行列同理,中间有断就计数器归零
					else
						count_dia1++;//连续两格相等,则计数器+1
					if (count_dia1 == WIN - 1)
						return board[x][y];//得到连成线的棋子类型
				}
				if (board[x][y] == ' ')
					count_dia1 = 0;
				x++;
				y++;
//在while循环中重复运行,从一开始的board[i][j]一路找下去
//直到找不到再退出,进入到i、j的循环,从下一个格子开始重新用while循环开始找
			}
		}
}
//左下右上
int i5 = 0;
for (i5 = 0; i5 < row; i5++)
{
	int j5 = 0;
	for (j5 = 0; j5 < col; j5++)
		if (board[i5][j5] != ' ')
		{
			count_dia2 = 0;//每次循环开始前计数器归零
			int x = i5;
			int y = j5;
			while (x < row - 1 && y > 0)
            //原理和上面一样,因为[x+1][y-1],所以不能是最后一行或者第一列的
			{
				if(board[x][y] != 0)
				{
					if (board[x][y] != board[x + 1][y - 1])
						count_dia2 = 0;
					else
						count_dia2++;
					if (count_dia2 == WIN - 1)
						return board[i5][j5];
				}
				if (board[x][y] == ' ')
					count_dia2 = 0;
				x++;
				y--;
			}
		}
}

毕竟是新手,写成这样自己已经很满意了,但嵌套了好多循环和条件语句,不知能否优化优化

③判断平局or继续

当判断函数从上到下一直进行到这里的时候,就代表着还没有人胜出,那么就需要判断是平局了or还能接着下

原理依旧简单,两个for循环逐个判断即可

//判断平局
int i4 = 0;
for (i4 = 0; i4 < row; i4++)
{
	int j4 = 0;
	for (j4 = 0; j4 < col; j4++)
	{
		if (board[i4][j4] == ' ')//如果检测到有任意空位,就还可以接着下
			return 'C';
	}
}
return 'Q';//到最后了发现实在没有空位,只得返回'Q',等待后续操作使其退出

小结:game()函数总览

自此,Jugde()函数内部已经全部完成,接下来只需要再回到main()函数中将这些东西合理摆放、写好循环和判断即可

 

void game_pvc()
{
	char ret = ' ';//用来存储Judge()的返回值
	char board[ROW][COL] = { 0 };
	InitBoard(board, ROW, COL);
	DisplayBoard(board, ROW, COL);
	

	while (1) 
	{
		PlayerMove(board, ROW, COL);//玩家执子
		DisplayBoard(board, ROW, COL);//打印棋盘
 		ret = Judge(board, ROW, COL);
        //判断输赢平
		if (ret == '*')
		{
			printf("玩家获胜\n\n");
			Sleep(1200);
			break;
		}
		if (ret == '#')
		{
			printf("电脑获胜\n\n");
			Sleep(1200);
			break;
		}
		if (ret == 'Q')
		{
			printf("平局\n\n");
			Sleep(1200);
			break;
		}
        //下面是如法炮制
		ComputerMove(board, ROW, COL);//电脑执子
		DisplayBoard(board, ROW, COL);//打印棋盘
		ret = Judge(board, ROW, COL);
        //判断输赢平
		if (ret == '*')
		{
			printf("玩家获胜\n\n");
			Sleep(1200);
			break;
		}
		if (ret == '#')
		{
			printf("电脑获胜\n\n");
			Sleep(1200);
			break;
		}
		if (ret == 'Q')
		{
			printf("平局\n\n");
			Sleep(1200);
			break;
		}
	}
}

这个游戏总体上就算完成啦!后续可以自行增加双人对战、人机大战、三五选择等功能

三、结语

能耐心看到最后或者看完我最煎熬的判断部分的,真的十分感谢,虽然想出一套逻辑、敲代码、断点调试很累,但是最后程序成功跑起来的时候还是成就感满满的

愿与诸位共勉,共同进步 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值