综合练习:三子棋

发布于:2022-12-13 ⋅ 阅读:(495) ⋅ 点赞:(0)

目录

一、前提说明:

二、代码实现:

①test.c文件中:

②game.h文件中:

③game.c文件中:

三、代码讲解:

①test.c文件中:

②game.h文件中:

③game.c文件中:

④注意



一、前提说明:

涉及相关知识:函数、数组、循环

游戏可以放在多个文件中:

test.c     ——>  游戏的测试逻辑

game.c  ——>  游戏的实现逻辑

game.h  ——>  游戏实现函数的声明

game.c文件和game.h文件都被test.c调用;game.c和game.h这两个文件合起来是实现游戏的代码。在VS2019中添加这3个文件。

函数声明和函数实现的第一行代码必须一模一样,否则如少一个void会报错重新定义函数基类型。

二、代码实现:

①test.c文件中:

#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"

void menu()
{
	printf("**************************\n");
	printf("******    1.play    ******\n");
	printf("******    0.exit    ******\n");
	printf("**************************\n");
}
void game()
{
	
	char board[ROW][COL] = { 0 };
	InitBoard(board, ROW, COL);

	DisplayBoard(board, ROW, COL);

	char ret = 0;

	while (1)
	{
		player_move(board, ROW, COL);
		
		DisplayBoard(board, ROW, COL);

		ret = is_win(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}

		computer_move(board, ROW, COL);
		
		DisplayBoard(board, ROW, COL);

		ret = is_win(board, ROW, COL);
		if (ret != 'C')
		{
			break;
		}
	}

	if (ret == '*')
	{
		printf("玩家赢了\n");
	}
	else if (ret == '#')
	{
		printf("电脑赢了\n");
	}

	else
	{
		printf("平局\n");
	}
}
void test()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
}
int main()
{
	test();
	return 0;
}

②game.h文件中:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

#define ROW 3
#define COL 3

void InitBoard(char board[ROW][COL], int row, int col);

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

void player_move(char board[ROW][COL], int row, int col);

void computer_move(char board[ROW][COL], int row, int col);

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

③game.c文件中:

#define _CRT_SECURE_NO_WARNINGS 1

#include "game.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;
	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)
		{
			//printf("---|---|---\n");
			for (j = 0; j < col; j++)
			{
				printf("---");
				if (j < col - 1)
				{
					printf("|");
				}
			}
		}
		printf("\n");
	}
}

void player_move(char board[ROW][COL], int row, int col)
{
	printf("玩家下棋:>");
	int x = 0;
	int y = 0;

	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");
		}
	}
}

void computer_move(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;

	printf("电脑下棋:>\n");

	while (1)
	{
		x = rand() % ROW;
		y = rand() % COL;
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

int is_full(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;
}

char is_win(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][1];
		}
	}

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

	if (1 == is_full(board, row, col))
	{
		return 'Q';
	}
	return 'C';

}

三、代码讲解:

①test.c文件中:

#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"
//主函数调用test()函数,test()函数用来进行相关的测试
void menu()
{
	printf("**************************\n");
	printf("******    1.play    ******\n");
	printf("******    0.exit    ******\n");
	printf("**************************\n");
}
void game()
{
//数据存储到一个字符的二维数组(9*9)中,玩家下棋是'*',电脑下棋是'#'
	//char board[3][3] = { 0 };//棋盘名字(字符二维数组名)是board,先对其初始化为0
//刚开始数组的内容应该全部是空格(棋盘上还没有下棋):用一个初始化函数InitBoard()来初始化这个数组(初始化棋盘)
	//InitBoard(board, 3, 3);//这个函数专门用来初始化棋盘的,所以传的参数是棋盘board,再传过去这个棋盘(数组)的行和列即3和3
//像这里的行3和列3在后来代码中可能会频繁大量使用,而且也可能会被修改,以防到处被修改,所以在game.h头文件中用#define定义一个符号ROW(行)值是3,再用#define定义一个符号COL(列)值是3,如果想要用这两个符号,则在本文件中需要包含这个头文件game.h:#include "game.h"
//这样分开文件写的好处:凡是修改涉及ROW和COL的可以在#define中只改它定义时的值即可——>方便,写出的代码不需要没必要的经常调整

	char board[ROW][COL] = { 0 };//注意:对二维数组的初始化一般用的是大括号{},对一维数组初始化一般用双引号""
	InitBoard(board, ROW, COL);//初始化棋盘
//接下来要实现(定义)这个函数
//函数的声明在头文件中,所以在game.h文件中声明一下InitBoard()函数
//函数的实现在game.c中

//初步初始化好棋盘后,打印棋盘看效果
	DisplayBoard(board, ROW, COL);//用这个函数打印二维数组的内容,这里实参传的是数组名board和行ROW和列COL
//这个是打印函数,要想实现先声明,则在game.h头文件中声明
//在game.c文件中进行实现

//棋盘布置好后,开始玩游戏:玩家下棋,电脑下棋,玩家下棋,电脑下棋……是个循环
//则实现下棋的逻辑:
	
	char ret = 0;

	while (1)
	{
		//player_move();玩家下棋
		player_move(board, ROW, COL);
//玩家下棋,因为整个过程都在操纵棋盘,所以需要把棋盘传进去,表示玩家下棋下的是ROW行COL列的board二维数组,
//这个函数需要在头文件game.h中声明,在文件game.c中实现
	    //这里打印一下棋盘
		DisplayBoard(board, ROW, COL);

		ret = is_win(board, ROW, COL);
		if (ret!='C')
		{
			break;
		}

		//computer_move();//电脑下棋
		//computer_move(board[ROW][COL], ROW, COL);//写法错误
		computer_move(board, ROW, COL);
//电脑下棋是随机下,哪里用空格就在哪里随机下,电脑下棋也需要把数据(琪)下到棋盘中,所以也传进入棋盘和ROW行和COL列
//这个函数同样需要在头文件game.h中声明,在文件game.c中实现
		//再打印一下棋盘
		DisplayBoard(board, ROW, COL);

		ret = is_win(board, ROW, COL);//int ret=is_win(board,ROW,COL)错误,不用再创建ret变量了
		if (ret!='C')
		{
			break;
		}

//玩家下棋,电脑下棋,需要判断输赢才能结束游戏,所以在上面代码中判断完善
//判断输赢:
//⑴玩家赢
//⑵电脑赢
//⑶平局(玩家和电脑都没赢的时候棋盘满了)
//⑷继续(游戏未结束,既不是玩家赢,不是电脑赢,也不是平局)
//所以有这4种状态,创建is_win()函数表示这4种状态
//所以:
//⑴玩家赢-----'*'//玩家赢了,函数的返回值
//⑵电脑赢-----'#'//电脑赢了,函数的返回值
//⑶平局-------'Q'//平局赢了的函数返回值
//⑷继续-------'C'//继续的函数返回值

//用返回值来确定游戏的状态
//is_win()函数是用来检测棋盘中是否有3行,3列或者对角线3个相连并且相同,所以需要传进去棋盘和ROW和COL知道几行几列
//⑴⑵⑶都表示的是游戏结束;只有⑷表明还可以玩游戏

	}
	//这里是在循环外判断的
	if (ret == '*')
	{
		printf("玩家赢了\n");
	}
	else if (ret == '#')
	{
		printf("电脑赢了\n");
	}
	//else(ret == 'Q')
	else
	{
		printf("平局\n");
	}



}
void test()
{
	int input = 0;
	srand((unsigned int)time(NULL));//主函数中调用srand()函数,time()函数的返回值作为时间戳,参数是空指针,返回值强转类型为unsigned int
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
}
int main()
{
	test();
	return 0;
}

②game.h文件中:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define ROW 3
#define COL 3
//#define定义的符号通常习惯上全部大写
//这里ROW和COL专门是用来指定行和列的大小的


//1、初始化棋盘的函数声明(即声明InitBoard()这个函数)
void InitBoard(char board[ROW][COL], int row, int col);//参数是:数组、行、列,注意形参变量的行和列不能和#define定义的符号值冲突

//2、打印棋盘的函数声明
void DisplayBoard(char board[ROW][COL], int row, int col);

//注意:row和col是形参,是传进来的几行几列,是形参变量;
//ROW和COL是用来指定数组大小的符号,是确切的定义的值,只要ROW和COL的值变了则经它们传出,传进的row和col也相应发生变化

//3、玩家下棋的函数声明
void player_move(char board[ROW][COL], int row, int col);

//4、电脑下棋的函数声明
void computer_move(char board[ROW][COL], int row, int col);

//5、判断输赢的函数的声明
//char is_win();//返回类型是4种字符,是char
char is_win(char board[ROW][COL], int row, int col);

③game.c文件中:

#define _CRT_SECURE_NO_WARNINGS 1

#include "game.h"

//1、实现初始化棋盘的函数:
//实现函数:实现InitBoard()这个函数
void InitBoard(char board[ROW][COL], int row, int col)//这里用到了ROW和COL这两个符号,这两个符号来自game.h,所以在本文件中game.h头文件依然被包含
{
//1、首先初始化二维数组(棋盘)为全部空格
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			board[i][j] = ' ';
		}
	}
//已经初步初始化好
}


//2、实现打印棋盘的函数
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
//	//打印二维数组里的所有内容
//	//先打印二维数组的9个位置
//	int i = 0;
//	int j = 0;
//	for (i = 0; i < row; i++)
//	{
//		for (j = 0; j < col; j++)
//		{
//			printf(" %c ", board[i][j]);//打印的是i行j列的元素,格式是:空格+元素+空格
//		}
//		printf("\n");
//	}
//	
//}
//错误,看不到棋盘,只是空出来3行

//需要的是行之间用3个“—”分隔开,即———;列之间用“|”分隔开
//所以调整代码为:

//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
//	int i = 0;
//	int j = 0;
//	for (i = 0; i < row; i++)
//	{
//		//打印数据:
//		printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
//		//打印分割行
//		if (i < row - 1)
//		{
//			printf("---|---|---\n");
//		}
//	}
//}
//这个棋盘被写死了,行可以变动但只能写3列,不便于代码的扩展
//再调整:

void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		//打印数据:
		//printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
//把“ %c |”看成一组数据,打印col组这样的数据,只不过最后一组没有打印“|”
        //printf(" %c |")//这样写又写死了,因为这样写每次打印都必须打印“|”了

		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);//这是数据的打印
			if (j < col - 1)
			{
				printf("|");
			}
		}
		printf("\n");//数据打印完后要换行


//打印分割行也是,把“---|”看成一组,打印col组,最后一组不打印“|”
		//打印分割行
		if (i < row - 1)
		{
			//printf("---|---|---\n");
			for (j = 0; j < col; j++)
			{
				printf("---");
				if (j < col - 1)
				{
					printf("|");
				}
			}
		}
		printf("\n");
	}
}
//即以上代码实现了DisplayBoard()函数,即已经完成了正确地打印出棋盘(二维数组)


//3、实现玩家下棋的函数
void player_move(char board[ROW][COL], int row, int col)
{
	//提示:
	printf("玩家下棋:>");
	//输入坐标
	int x = 0;
	int y = 0;
	
	while (1)
	{
		scanf("%d %d", &x, &y);
		//判断玩家输入数的范围
		if (x >= 1 && x <= row && y >= 1 && y <= col)//输入坐标合法
		{
			//判断所下的琪的位置是否已经有之前下过的琪,因为值能在空格处下棋,不能悔棋
			//if (board[x][y] == ' ')//因为对于二维数组来说行和列都是从0开始的,而玩家会认为行和列都是从1开始的
			if (board[x - 1][y - 1] == ' ')
			{
				//是空格则可以下棋
				//board[x][y] = "*";//错误
				//board[x][y]="*"//错误,若玩家下的棋是星号*则应该用单引号引起来,如果用说双引号则是p
				board[x - 1][y - 1] = '*';
				break;//下棋成功,break,否则代码会死循环
			}
			else
			{
				printf("该坐标被占用,请重新输入!\n");
			}
			
		}
		else
		{
			printf("坐标非法,请重新输入!\n");//坐标非法需要重新输入,则再上去重新输入,是个循环
		}
	}
}
//综上有坐标合法的判断,有坐标是否被占用的判断,然后再下棋(这里是星号标记)


//4、实现电脑下棋的函数
void computer_move(char board[ROW][COL], int row, int col)
{
	//生成行坐标和纵坐标
	int x = 0;
	int y = 0;

	printf("电脑下棋:>\n");

	while (1)
	{
		//生成随机数,并确定生成随机数的范围
		x = rand() % ROW;//生成行,这样写的范围是0~(ROW-1)
		y = rand() % COL;//生成列,这样写的范围是0~(COL-1)
		//所以坐标的合法性不用判断了

		//判断坐标是否被占用
		if (board[x][y] == ' ')//这里不需要进行-1操作,因为已经设置好范围了且不是玩家输入的
		{
			board[x][y] = '#';
			break;//保证下棋成功后不再下棋
		}
		//如果坐标被占用也不需要提示,因为坐标被占用电脑上去重新生成即可,即写成一个循环
	}
}


//5、实现判断输赢的函数
//检测棋盘中是否有3行,3列或者对角线3个相连并且相同

int is_full(char board[ROW][COL],int row,int col)
{
	//遍历一下数组看有没有空格,写2次循环,二维数组创建了2个变量
	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;
}


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

//首先检查一行一行的是否有相同的,每行都要检查,循环row次
	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][1];//返回#或*,随意返回相同的一个值即能说明赢的状态(前面定义的)
		}
	}

//再检查一列一列的是否有相同的,每列都要检查,循环col次
	
	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];
	}

//判断是否平局:棋盘满了,没有空格——>用函数is_full(),判断的是board是否满了,传进去数组,行,列
	if (1 == is_full(board, row, col))//这里用的是小写,因为row和col已经传给上面的row和col了(is_win函数中包含这个函数,是函数中调用了函数)
	{
		return 'Q';
	}
	//实现is_full函数,因为is_full()函数只是为了支持is_win()函数使用,没有必要在头文件中声明
//继续:
	return 'C';

}

④注意:

1、游戏总体的打印效果:

 2、疑问:打印棋盘时代码的实现过程:

 由此写代码:

 (被遮挡的代码是:printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);)

但由于第三行并没有分割行所以调整代码:

 列写死了,拓宽代码调整为:

分割列:

把“ %c |”看成一组数据,打印col组,因为这是列与列之间的分割,但最后一次没有“|”。所以先打印“ %c ”,再打印“|”,打印“|”时要判断一下是不是最后一次,是最后一次就不打印了。

同样,分割行:

把“---|”看成一组数据,打印row组,因为这是行与行之间的分割,但最后一次没有“|”。所以先打印“---”,再打印“|”,打印“|”时要判断一下是不是最后一次,是最后一次就不打印了。 

注意:

两次嵌套循环:外围的for循环是确定打印row行(即循环row次),

在row行里面,又要循环打印“ %c ”和“|”,打印col列(即循环col次),还要确保打印最后一列时没有打印“|”;

代码中间有一行if (row - 1)作为判断条件是否继续打印下面的代码,如果是最后一行就不用打印“---和“|”了,所以就不用走下面打印的代码;

在row行里面,又要循环打印“---”和“|”,打印col列(即循环col次),还要确保打印最后一列时没有打印“|”。

3、注意:先判断坐标的输入是否合法再判断是否是空格

 4、

	while (1)
	{
		player_move(board, ROW, COL);
		DisplayBoard(board, ROW, COL);
		ret = is_win(board, ROW, COL);
		if (ret!='C')
		{
			break;
		}
		computer_move(board, ROW, COL);
		DisplayBoard(board, ROW, COL);
		ret = is_win(board, ROW, COL);
		if (ret!='C')
		{
			break;
		}
	}

理解:

判断输赢的时候,这里是个循环,而循序终止的条件(break)是不等于'C',即不是继续。