两百行代码实现简易三子棋小游戏(详解)

自学习C语言来第一次面对"大工程",首先我们得好好的进行规划。

一.菜单

作为一个游戏,玩家进去后首先应该有一个简易的界面,对玩家进行选项提示,这里简单的编写一个菜单函数,代码实现如下:

void menu()
{
	printf("************三子棋小游戏************\n");
	printf("************   1.play   ************\n");
	printf("************   0.exit   ************\n");
	printf("************   V1.0.0   ************\n");
}

四行打印,简单的提示了相关信息,并且选项为输入1进入游戏,输入0结束游戏

二.菜单选项基本运行逻辑

即打印界面后,玩家输入1开始游戏,且游戏结束后不应直接退出,可让玩家再次进行选择,输入0直接结束游戏,若输入其他数字,则玩家输入错误,应提示重新输入,同样可让玩家再次进行选择。

因此,代码实现,首先定义变量接收输入值,然后用switch语句进行分支选择,在最外层套一个循环以达到使玩家重复输入的目的,代码实现如下:

int input = 0;
	do
	{
		printf("请输入选项>:\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("对局开始    玩家方:* 电脑方: #\n");
			game();//游戏实现
			break;
		case 0:
			printf("游戏结束,欢迎下次游玩\n");//exit
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	}
	while (input);

玩家输入1,则提示开始游戏,进入game()函数,这也是随后应重点实现的内容。玩家输入0,则while条件为假,游戏结束。至此基本运行逻辑已经实现,值得一提的是,此处比较容易犯的错误是将scanf语句放在循环外面,使得不能达到再次输入的目的,形成死循环

三.game()函数-游戏的实现

第三部分需实现游戏的运行,较为复杂,是程序的重中之重,为此,我们最好创建新的源文件game.c和对应头文件game.h进行编写

来到game()函数内部,经过简单的思考,游戏的实现大概分为以下几个部分: 

1.棋盘 2.玩家/电脑输入 3.判断正负

1.棋盘实现

1.三子棋的棋盘,可抽象为二维的坐标,因此可用二维数组实现。

#define ROW 4
#define COL 4
char board[ROW][COL] = { 0 };

简单的一行代码,就是之后我们处处都需要使用到的三子棋棋盘。这里ROW,COL为标识符常量,应放在头文件里用#define定义,这里为便于理解将其放在一起。这样做的好处是,对于后面处处需要使用的棋盘,当我们想要更改棋盘大小时,只需更改ROW,COL的定义,而非一个个的进行修改。虽然三子棋限定了3*3棋盘,但不妨想想五子棋和扫雷,能自由方便的修改棋盘大小自然是更好的。

2.接着我们对棋盘进行初始化,在一开始棋盘上并未有人落子,每个点位在棋盘打印时最好打印的是' ',因此我们通过遍历将二维数组元素全部初始化为' '。

void Init_board(char board[ROW][COL])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < ROW; i++)
	{
		for (j = 0; j < COL; j++)
		{
			board[i][j] = ' ';
		}
	}
}

比较简单的实现了一个初始化函数,只需进行调用即可。

3.接下来着手实现棋盘的打印,棋盘由数组元素和分割线组成,分割线可以使让棋盘看起来更美观,下面先给出代码实现:

void print_board(char board[ROW][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)
		{
			for (j=0;j<COL;j++)
			{
				printf("---");
				if (j < COL - 1)
					printf("|");
			}
			printf("\n");
		}
	}
}

效仿二维数组的打印,棋盘的打印由两个变量控制,外层循环变量i控制行,内层变量j控制列

                                 图1.        图2.

行的打印,包含棋盘列和分割线,可将其分成i-1行的图1打印和第i行的图2打印,因此在最外层循环的内部,用两个for循环,第一个实现棋盘列的打印,第二个实现分割线的打印,又因当最后一行时不需打印分割线,因此第二个for循环用if语句控制少打印一行

                                  图1.                         图2.

列的打印,则两个for循环内部进行实现。首先是棋盘列,行的打印思路一致,可将其分成j-1行列图1打印和第j列的图2打印。

for (j=0;j<COL;j++)
		{
			printf(" %c ", board[i][j]);
			if (j<COL-1)
				printf("|");
		}
		printf("\n");

 即可,记住打印完后进行换行

分割线同理

for (j=0;j<COL;j++)
			{
				printf("---");
				if (j < COL - 1)
					printf("|");
			}

 至此,棋盘的打印已然实现,第一部分的棋盘实现结束

2.玩家/电脑落子       玩家:'*'     电脑:'#'

思路很简单,就是玩家输入坐标,然后对数组相应元素进行修改,接着打印修改之后的棋盘即可。需要注意的是,在玩家看来的1-3列,实际上对应着数组下标的0-2。

对于玩家输入,需要判断输入的合法性。即 1.输入值越过棋盘范围,此时为无效输入,应让玩家重新输入。 2.输入坐标已有棋子,此时也为无效输入,需重新输入。直到输入正确,执行相应操作,基本逻辑与菜单选项逻辑相似,下面为代码实现:

void player_input(char board[ROW][COL])
{
	int x = 0;
	int y = 0;
	printf("请玩家落子>:   (输入坐标:x,y):\n");

	while (1)
	{
		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("该位置已有棋子,请重新输入\n");
		}
		else
			printf("输入坐标非法,请重新输入\n");
	}
}

 对于电脑输入,则用rand()函数产生随机数,然后判断产生坐标是否已有棋子即可,注意需用srand函数进行随机数发生器的初始化,可放在程序开始位置,注意前文代码中没有写出

有人可能就疑惑了:电脑随机落子,那这不成了人工智障了吗?

没办法,目前水平有限,简易实现只能想到随机数生成的方法

void computer_input(char board[ROW][COL])
{
	printf("电脑落子>:  \n");
	while (1)
	{
		int x = rand() % ROW + 1;
		int y = rand() % COL + 1;
		if (x >= 1 && x <= ROW && y >= 1 && y <= COL)
		{
			if (board[x - 1][y - 1] == ' ')
			{
				board[x - 1][y - 1] = '#';
				break;
			}
		}
	}
}

至此玩家输入/输出已经实现,第二部分结束

3.输赢判断

第三部分也是最复杂的一部分。首先应明白,当每一次玩家/电脑输入后,只有以下几种结果:

1.玩家赢/电脑赢,即满足胜利条件(存在横行or竖列or斜对角全为*/#),游戏结束

2.平局,即无任何一方满足胜利条件,但棋盘已满,游戏结束

3.不属于前两种情况,只是单纯的落子,游戏继续

下面先给出我的代码实现:

char is_win(char board[ROW][COL], char ch)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < ROW; i++)//横三行
	{
		int flag = 0;
		for (j = 0; j < COL-1; j++)
		{
			if (board[i][j] != board[i][j + 1] || board[i][j] != ch)
			{
				flag = 1;
				break;
			}
		}
		if (flag == 0)
			return ch;
	}
	for (j = 0; j < COL; j++)//竖三列
	{
		int flag = 0;
		for (i = 0; i < ROW - 1; i++)
		{
			if (board[i][j] != board[i + 1][j] || board[i][j] != ch) 
			{
				flag = 1;
				break;
			}
		}
		if (flag == 0)
			return ch;
	}
	int flag = 0;
	for (i = 0; i < ROW - 1; i++)//右斜
	{
		for (j = 0; j < COL - 1; j++)
		{
			if (i == j)
			{
				if (board[i][j] != board[i + 1][j + 1] || board[i][j] != ch)
				{
					flag = 1;
					break;
				}
			}

		}
	}
	if (flag == 0)
		return ch;
	flag = 0;
	for (i = 1; i < ROW; i++)//左斜
	{
		for (j = 0; j < COL-1; j++)
		{
			if (i + j == ROW - 1)
			{
				if (board[i][j] != board[i - 1][j + 1] || board[i][j] != ch)
				{
					flag = 1;
					break;
				}
			}
		}
	}
	if (flag == 0)
		return ch;
	 else if (is_full(board) == ' ')
		return 'C';
	else
		return 'E';
}

函数参数部分除了必要的棋盘传参,还定义了ch接收字符,这是为了让玩家和电脑都能通过调用这个函数判断输赢,返回类型定义字符类型,针对不同情况返回不同信息

可以看到,我把它分为了: 1.行的判断 2.列的判断 3.右下对角线的判断 4.左下对角线的判断 5.平局的判断 6.不满足前面任意一种,游戏继续

这里没有简单的用3行/3列/对角线3个元素进行判断,而是通过循环变量实现,主要是为了当棋盘大小改变时也能应用三子棋的规则游戏,即每行,每列....(虽然说完全不对吧,就算是五子棋也不应该是这个下法啊,虽然姑且想实现一下,但是中间写出bug调试的时候简直要命啊!!!)

横行的判断

for (i = 0; i < ROW; i++)//横三行
	{
		int flag = 0;
		for (j = 0; j < COL-1; j++)
		{
			if (board[i][j] != board[i][j + 1] || board[i][j] != ch)
			{
				flag = 1;
				break;
			}
		}
		if (flag == 0)
			return ch;
	}

首先是最基本的两层循环,我的思路是,对每一行的元素两两之间进行判断如果1.存在两元素不相等,胜利条件不成立  2.即便两元素相等,但存在元素不等于'*',可能全为' '或是其他情况,总之胜利条件不成立

遍历每行,如果条件不满足直接跳出,定义一个flag变量判断跳出后的情况,如果上述1,2全部不成立,即能确保这一行全部都为*,玩家胜,返回字符ch

同理,列的判断

for (j = 0; j < COL; j++)//竖三列
	{
		int flag = 0;
		for (i = 0; i < ROW - 1; i++)
		{
			if (board[i][j] != board[i + 1][j] || board[i][j] != ch)
			{
				flag = 1;
				break;
			}
		}
		if (flag == 0)
			return ch;
	}

接下来是对角线的判断:

斜右下对角线 ,我的思路是遍历数组,然后判断(行数==列数)的元素是否满足条件,即(1,1),(2,2).....(n,n),这些元素便是斜右下对角线元素,其余思路与行的判断一致

int flag = 0;
	for (i = 0; i < ROW - 1; i++)//右斜
	{  
		for (j = 0; j < COL - 1; j++)
		{
			if (i == j)
			{
				if (board[i][j] != board[i + 1][j + 1] || board[i][j] != ch)
				{
					flag = 1;
					break;
				}
			}
		}
		if (flag == 1)
			break;
	}
	if (flag == 0)
		return ch;

此处flag定义位置必须放在循环外部,因为循环必须遍历完对角线后进行if判断

斜左下对角线同理,依旧遍历数组,假如给n*n的二维坐标,容易发现,左斜下对角线的坐标满足(i+j==n+1),但由于是数组,故最终条件为(i+j==ROW-1)

flag = 0;
	for (i = 1; i < ROW; i++)//左斜
	{
		for (j = 0; j < COL-1; j++)
		{
			if (i + j == ROW - 1)
			{
				if (board[i][j] != board[i - 1][j + 1] || board[i][j] != ch)
				{
					flag = 1;
					break;
				}
			}
		}
		if (flag == 1)
			break;
	}

需要注意,对角线遍历时变量并不是从0开始循环到第n个元素,而是经过了相应的处理,这是为了防止数组越界。如果斜右下对角线循环时循环到第n个元素,那么n+1显然越界

胜利条件判断完毕,接下来是平局条件

if (is_full(board) == ' ')
		return 'C';

用is_full函数判断棋盘是否已满,若未满则设定返回字符'C'

下面实现is_full函数,遍历即可

char is_full(char board[ROW][COL])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < ROW; i++)
	{
		for (j = 0; j < COL; j++)
		{
			if (board[i][j] == ' ')
				return ' ';
		}
	}
	return 'E';
}

若未满返回字符' ',此时上文条件成立,返回'C'

否则上述情况均不满足,设定返回字符'E',表示已满,即平局

胜负判断至此已完成,现在有了返回值之后,我们需要在game()函数内部进行情况判断,首先如果继续进行,则重复输入,循环实现,否则跳出循环,和菜单逻辑实现依然类似

while (1)
	{
		player_input(board);
        //玩家输入
		print_board(board);
		ret = is_win(board, '*');
		if (ret != 'C')
			break;
		//电脑输入
		computer_input(board);
		print_board(board);
		ret = is_win(board, '#');
		if (ret != 'C')
			break;
	}

若上文函数返回值为C,游戏继续,则进行下一次循环,否则跳出后,依靠胜负情况判断输出对应结果

//3.判断正负    1.棋盘未满,胜负未分,继续! 2.胜负已分 - 玩家或电脑  3.棋盘满,平局 return game
	//输出结果
	if (ret == 'E')
		printf("平局!\n");
	else if (ret == '*')
		printf("玩家胜!\n");
	else
		printf("电脑胜!\n");

至此胜负判断实现,第三部分结束

将这些部分组合起来,你便得到了一个极其简单的三子棋小程序(虽然你可以也能玩规则离谱的四子棋)


void game()//三子棋游戏实现 1.棋盘 2.玩家/电脑输入 3.判断正负
{
	char ret = 0;
	//1.棋盘实现-二维数组
	char board[ROW][COL] = { 0 };
	//初始化棋盘
	Init_board(board);
	//打印棋盘
	print_board(board);
	//2.玩家/电脑输入
	while (1)
	{
		player_input(board);
		print_board(board);
		ret = is_win(board, '*');
		if (ret != 'C')
			break;
		//电脑输入
		computer_input(board);
		print_board(board);
		ret = is_win(board, '#');
		if (ret != 'C')
			break;
	}
	//3.判断正负    1.棋盘未满,胜负未分,继续! 2.胜负已分 - 玩家或电脑  3.棋盘满,平局 return game
	//输出结果
	if (ret == 'E')
		printf("平局!\n");
	else if (ret == '*')
		printf("玩家胜!\n");
	else
		printf("电脑胜!\n");
}

以下是最后对菜单打印进行的一点的修改,可以打开为n子棋小程序,虽然毫无意义,但是胜负判断里实现都实现了,总得全面一点吧

char* tran()
{
	switch (ROW)
	{
	case 1:
		return "一";
	case 2:
		return "二";
	case 3:
		return "三";
	case 4:
		return "四";
	case 5:
		return "五";
	case 6:
		return "六";
	case 7:
		return "七";
	case 8:
		return "八";
	case 9:
		return "九";
	default:
		return "&&&";
	}
}
void menu()
{
	printf("************%s子棋小游戏************\n", tran());
	printf("************   1.play   ************\n");
	printf("************   0.exit   ************\n");
	printf("************   V1.0.0   ************\n");
}

最后不得不吐槽,这种"大型程序"要是写出来bug,找bug真是让人头皮发麻啊啊啊

最后附上头文件以便观察需实现的函数:

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 3 //行
#define COL 3 //列
void game();
void Init_board(char board[ROW][COL]);
void print_board(char board[ROW][COL]);
void player_input(char board[ROW][COL]);
void computer_input(char board[ROW][COL]);
char is_win(char[ROW][COL],char ch);
char is_full(char board[ROW][COL]);

                     效果图展示:

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

牧濑红莉栖^U

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值