三子棋游戏(保姆级讲解

       今天来向大家分享利用数组实现的一个小游戏,也就是我们平常玩的井字棋,我将全程为大家详细讲解,如果大家觉得博主分析的比较好的话,希望给我店上一个小小的赞和关注,你们的支持会加强我前进的动力,同时对博客有什么建议或者意见的话也可以提出来,因为我不是特别熟悉,但是一定会努力改进。(尤其是博客排版方面的建议,我有很多不足,希望大佬指点)

一.游戏介绍

    三子棋,又称为井字棋(Tic-tac-toe),是一种两人对弈的纯策略型棋类游戏。游戏的目标是将三个相同的标记(通常是“X”和“O”)连成一条直线,可以是水平、垂直或对角线。游戏在一个3x3的网格上进行,玩家轮流在网格的空位上放置自己的标记。

        相信大家都对这个游戏有一定的了解,大多都玩过,而且逻辑也很简单。

二.游戏设计逻辑

虽然逻辑比较简单,那么我们怎么来具体实现呢?

        首先,任何游戏都需要一个菜单,我们需要告诉计算机我们想干什么,方便计算机执行相应的操作,其次,我们需要一个棋盘,这在计算机中可以用一个二维数组来储存。然后,我们可以在相应的坐标来落子,接着电脑进行下棋,重复操作,直至平局或者一方胜利。

如下所示:

1.打印菜单

2.初始化棋盘

3.玩家下棋                             

4.棋盘更新

5.电脑下棋

6.棋盘更新

7.输赢判定

基本的逻辑就是这样,大家可以先思考如何让计算机一步步来实现这个功能。

三.文件的拆分

        不同于之前的猜数字游戏,三子棋游戏写的代码较多,为了方便后续对代码的优化和维护,尽量把代码拆分一下。如下:

                       test.c                  测试游戏的逻辑

                      game.c                游戏核心代码的实现

                      game.h                游戏代码的实现(函数声明,符合定义)

如左图所示
我知道大家会有一定的疑问,为什么要这么拆分?

        非常了解大家的感受,大多数人一开始都会想我把代码写到一个.c文件里,哪里错了我改哪里不就行了吗?

我非常愿意相信看到我的这篇博客的人将来都是互联网大厂中的大佬,既然是当大佬,那么以后肯定写的代码的代码量会很高,全写一个文件里是不方便寻找和修改的,而且主函数只有一个,进大厂后不可能单独完成一个大项目,更不可能每个人都去写主文件,譬如变量的修改,如果公用一个文件的话,你改完可能对其他人的代码是会有影响的,所以在将来在公司里更多的是模块化功能的设计,最后整合到一起实现完整功能

同时,把代码写入不同的文件也可以起到很好的保护作用,当你拥有足够的水平后,你是可以靠出售自己的代码赚钱的,但是我们不可能直接把所有的源代码全发给买家,需要通过对部分文件的转译或者加密使别人只可以使用你的代码但是不能修改你的核心文件。

这里补充说明一下,game.h不仅可以放函数,还可以把一些常用头文件也放里面比如#include<stdio.h>,引用的时候直接引用“game.h”就行,切记一定是双引号,我在test.c和game.c中都引用了game.h。

//在test.c和game.c中加上一行这个就可以完成引用
#include "game.h"

总之在写代码时一定要记得,方便自己更方便他人。让别人快速清楚的知道你在写什么是大佬必备,但是学会优化,维护自己的代码也是非常重要的。

 

四.代码详解

 1.菜单的打印

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

    因为游戏比较简单,我们的菜单设计的也比较简单,可以直接效仿我之前发的猜数字游戏,同时我们对于电脑操作设计的非常简单,只会随机下棋,所以如果你连他都下不过的话,我只能说     菜就多练  祝你们好运!!!  

2.switch实现游戏进入与退出

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

因为我们的菜单必须先打印出来,所以要用do while循环,先执行再判断条件。Case 1里的game()是我们定义的游戏的函数(下面会讲别急)。

3.棋盘的初始化

        我们需要初始化一个3 * 3的二维数组用来当做棋盘存放我们的棋子。请注意,这个数组单纯只能用来存储数据,并不是实际的棋盘,实际的棋盘应该还要另外打印出来。

步骤如下

在game.h中声明初始化函数Initboard().

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

 在game.c中写入初始化函数Initboard()的具体内容

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

4.棋盘的打印

在game.h中声明打印棋盘的函数Dilayboard()

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

在game.c中加入打印函数的具体内容

这里我写了两种打印方法,大家可以找一下区别

//打印棋盘(基础版)
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");
	}
}
//打印棋盘(改进版)
void Displayboard(char board[ROW][COL], int row, int col) {
    int i, j;
    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");
        }
    }
}

虽然这两种打印出来的效果一模一样,但是第二种是明显优于第一种的,因为第一种是固定了列数为3的,相当于把打印的大小写死了,而第二种可以根据具体的行列值答应出相应的棋盘,尽管我们在此处只要3*3的棋盘,但是第二种明显功能更强。 这就是一种优化。

5.玩家下棋操作

在game.h中声明玩家下棋函数PlayerMove()

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

game.c输入玩家下棋函数内容 

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

	while (1)
	{
		printf("请输入要下棋的坐标:>");
		scanf("%d %d", &x, &y);//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的位置,不能放错
		else
		{
			printf("坐标非法,请重新输入\n");
		}
	}
}

6.电脑下棋操作 

因为我们电脑是随机坐标下棋,所以要用到srand()和rand()函数,因此要用<time.h><stdlib.h>着两个头文件不清楚的可以看我之前发的猜数字游戏里有讲,而且讲的比较清楚。

把<time.h><stdlib.h>和电脑下棋函数放入game.h作为声明,下面显示目前放的所有声明

//game.h文件内容

#include <stdio.h>
#include <stdlib.h>
#include <time.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 PlayerMove(char board[ROW][COL], int row, int col);
//电脑下棋,空白位置随机下
void ComputerMove(char board[ROW][COL], int row, int col);

接着在game.c中加入电脑下棋函数

void ComputerMove(char board[ROW][COL], int row, int col)
{
	printf("电脑下棋:>\n");

	int x = 0;
	int y = 0;
	while (1)
	{
		//随机生成0~2的值
		x = rand() % row;
		y = rand() % col;
		//随机的坐标肯定是合法的,所以只需要判定是否被占用
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

7.判断输赢

最后写一个判断输赢的函数,写好后放入游戏主循环,在玩家下棋后和电脑下棋后都要进行判定。

先在game.h中声明

char IsWin(char board[ROW][COL], int row, int col);
//四种情况,用char作为返回类型,123三种都要退出游戏
//1.玩家赢----设定返回'*'
//2.电脑赢----设定返回'#'
//3.平局(棋盘慢且1未分胜负)----设定返回'Q'
//4.游戏未结束,继续 ----设定返回'C'

接着把内容写入game.c

char IsWin(char board[ROW][COL], int row, int col) {
	// 检查行是否有获胜
	for (int i = 0; i < row; i++) {
		if (board[i][0] != ' ' && board[i][0] == board[i][1] && board[i][1] == board[i][2]) {
			return board[i][0];
		}
	}
	// 检查列是否有获胜
	for (int j = 0; j < col; j++) {
		if (board[0][j] != ' ' && board[0][j] == board[1][j] && board[1][j] == board[2][j]) {
			return board[0][j];
		}
	}
	// 检查对角线是否有获胜
	if (board[0][0] != ' ' && board[0][0] == board[1][1] && board[1][1] == board[2][2]) {
		return board[0][0];
	}
	if (board[0][2] != ' ' && board[0][2] == board[1][1] && board[1][1] == board[2][0]) {
		return board[1][1];
	}
	// 如果没有玩家获胜,检查棋盘是否已满
	if (IsFull(board, row, col)) {
		return 'Q'; // 平局
	}
	return 'C'; // 游戏未结束,继续
}

这里调用了一个IsFull函数判断棋盘是否已满。因为平局也是一种游戏结果,平局时棋盘肯定是满的。而双方此时又没有分出胜负。

IsFull函数

还是先声明后使用,game.h声明,game.c写内容

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

IsFull函数就是一个简单的遍历二维数组的函数,跟初始化函数Initboard()很像,只是多了一个判定条件 。

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 < col; j++)
		{
			if (board[i][j] == ' ')
			{
				return 0;
			}
		}
	}

}

8.代码总结

         这是我做的代码分文件储存的第一个比较多的代码,总的来说,分文件是很方便的,我们把需要用到的函数声明和头文件写入game.h文件中,接着在game.c中去实现具体的函数板块,你甚至可以单独注释一行用来编写序号,用来更快速的查找,test.存放我们进入和退出游戏的主逻辑,三个文件相辅相成,减少了代码冗余,同时也更方便我们的测试和改错,而且有的函数的实际代码是非常相似的,大家要是实在不想再打一遍,也可以快速找到复制和粘贴。希望大家都能去尝试这种分文件的方法并且掌握,成为互联网大厂的大佬。Passion!!!

五.实例演示

 




六.完整代码

test.c

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu()
{
	printf("****** 三字棋游戏  ******\n");
	printf("*****1.play 0.exit *****\n");
	printf("************************\n");
}

void game()
{
	int ret = 0;
	char board[ROW][COL];
	Initboard(board, ROW, COL);
	Displayboard(board, ROW, COL);
	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");
	}
	else if (ret == '#')
	{
		printf("电脑嬴\n");
	}
	else
	{
		printf("平局\n");
	}
}

int main()
{
	srand((unsigned int)time(NULL));//设置随机数的生成起点
	int  input = 0;
	do
	{

		menu();
		printf("请选择:>");
		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

#include <stdio.h>
#include <stdlib.h>
#include <time.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 PlayerMove(char board[ROW][COL], int row, int col);
//电脑下棋,空白位置随机下
void ComputerMove(char board[ROW][COL], int row, int col);
//判断输赢
char IsWin(char board[ROW][COL], int row, int col);
//四种情况,用char作为返回类型,123三种都要退出游戏
//1.玩家赢----设定返回'*'
//2.电脑赢----设定返回'#'
//3.平局(棋盘慢且1未分胜负)----设定返回'Q'
//4.游戏未结束,继续 ----设定返回'C'
int IsFull(char board[ROW][COL], int row, int col);

 game.c

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


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

	while (1)
	{
		printf("请输入要下棋的坐标:>");
		scanf("%d %d", &x, &y);//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的位置,不能放错
		else
		{
			printf("坐标非法,请重新输入\n");
		}
	}
}

void ComputerMove(char board[ROW][COL], int row, int col)
{
	printf("电脑下棋:>\n");

	int x = 0;
	int y = 0;
	while (1)
	{
		//随机生成0~2的值
		x = rand() % row;
		y = rand() % col;
		//随机的坐标肯定是合法的,所以只需要判定是否被占用
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}
char IsWin(char board[ROW][COL], int row, int col) {
	// 检查行是否有获胜
	for (int i = 0; i < row; i++) {
		if (board[i][0] != ' ' && board[i][0] == board[i][1] && board[i][1] == board[i][2]) {
			return board[i][0];
		}
	}
	// 检查列是否有获胜
	for (int j = 0; j < col; j++) {
		if (board[0][j] != ' ' && board[0][j] == board[1][j] && board[1][j] == board[2][j]) {
			return board[0][j];
		}
	}
	// 检查对角线是否有获胜
	if (board[0][0] != ' ' && board[0][0] == board[1][1] && board[1][1] == board[2][2]) {
		return board[0][0];
	}
	if (board[0][2] != ' ' && board[0][2] == board[1][1] && board[1][1] == board[2][0]) {
		return board[1][1];
	}
	// 如果没有玩家获胜,检查棋盘是否已满
	if (IsFull(board, row, col)) {
		return 'Q'; // 平局
	}
	return '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 < col; j++)
		{
			if (board[i][j] == ' ')
				return 0;
		}
	}
	return 1;
}

        大家可以看到基本都是通过函数来进行模块化的设计,所以列出自己的逻辑非常重要,要制作什么样的功能,怎么去抽象化给计算机去实现,条件的基本判定,返回值的设定,区域的有效范围,可以说随便一个小坑都会成为我们调试的一大死敌,所以我建议大家在写代码时边写边调试,完成一个功能就就立马测试,不然堆得多了,会非常痛苦(不要问我我是怎么知道的,深受其害)

七.心得体会

         编程真的是一件很让人苦恼的事,对于不太会调试的我来说,找bug是一件很痛苦的事,小小的几行代码一改就是一个下午,一天,真的非常痛苦,但是随着学习的深入,确实是会有很大的收获,在学习的过程中,经常也会有很多不懂的知识点,但是这并不能成为我们放弃的理由,生活中励志的例子太多了,我们有相比而言确实是太微不足道了,因此我们才要不断的提高自己的能力,吗喽的力也是力,虽然我们会的并不多但是一定不能放弃。永远相信美好的事情正在发生!!!

        还有就是,编程一定要敲,敲敲敲,不然根本不知道自己都会了什么。理论和实际操作是完全不一样的,编程一定是在不断练习中强化自己的思维和操作流畅度的,所以一定要实践。最后希望大家早日成为互联网大厂的大牛,在我们成功之后,也希望大家不要忘记自己的来时路,记得为社会做出一点贡献。

        如果你一直坚持看到这的话,非常感谢你的关注,你一定能成功。时间也不早了,自习的教室也已经强制熄灯了,祝所有的读者梦想都能照进现实,谢谢大家。respect!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值