C语言实现三子棋(详解版)

三子棋

1. 介绍

今天就带大家用C语言实现一下简洁版本的三子棋,三子棋也称为井字棋。

我们先看看三子棋的预期效果:

这里*代表玩家的棋子,#代表电脑的棋子。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mkHZOdHP-1683595692502)(C:\Users\30539\AppData\Roaming\Typora\typora-user-images\image-20230508161811074.png)]

我们通过观察发现,打印出的棋盘是一片空白,表面上看着没有数据,实际上打印的是字符数组,字符中存放的是空格。

注意:当我们想要完成一个比较大的项目时,可采取模块化处理。即:将函数的声明放在头文件中将函数的定义和实现放在源文件里不论该函数有多么小

2. 准备工作

作者在写三子棋时将整个工程分为了三个部分:

  • test.c
  • game.c
  • game.h
  1. test.c源文件用于测试三子棋代码,程序从test.c文件开始执行。
  2. game.c文件中存放与游戏实现相关函数的定义和实现
  3. game.h文件中存放与游戏实现相关的函数的声明、

3. 棋盘相关操作

通过前面我们了解到,所谓的下棋操作本质上是对字符数组中的元素进行操作,通过改变字符数组中的元素的值再将其打印出来,达到下棋效果。但是棋盘是含有横纵坐标的。如果我们定义一维字符数组,则无法方便的表示棋盘中的横纵坐标。综合考虑,二维字符数组比较符合要求。

因为棋盘是三行三列的布局,所以先创建一个二维字符数组:

char board[3][3] = {0};

但是经过思索,这里创建数组时,直接指定数组的行数和列数是不太稳妥的,因为万一我想实现一个6*6或其他参数类型的棋盘,再从这里修改就不太方便。所以,我们可以使用#define定义常量来控制数组的行数和列数,这样我们就可以很方便的修改棋盘的参数了。如下:

#define ROW 3//行
#define COL 3//列
char board[ROW][COL] = {0};//创建一个3*3的数组

通常而言,#define 定义常量这种语句一般放置于头文件中。

棋盘创建好了之后,首先将其每个元素初始化成空格。初始化成功之后,最好将棋盘打印出来,看是否初始化成功。

//初始化棋盘
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] = ' ';
		}
	}
}

棋盘打印我们可以直接遍历该二维数组进行打印:

//打印棋盘 版本一
void DisplayBoard(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++)
		{
			printf("%c", board[i][j]);
		}
		printf("\n");
	}
}

但是这种方式的效果不直观,一片空白。我们可以为棋盘设计一个轮廓,使其可视化。先来看看棋盘的规律:

在这里插入图片描述

先看每一行的打印,整个棋盘有三行,每一行的打印是一组,整个棋盘按行就被分为了三组,每一组可用绿色和橙色部分表示,只不过最后一组的橙色部分没有打印而已。所以通过分析可以通过数组的行数控制分组数。

先对绿色部分进行分析:绿色部分先打印三个空格,再打印一个竖线,继续打印三个空格,再打印一个竖线,继续打印三个空格,最后一个空格不打印。

橙色部分:先打印三个减号,在打印一个竖线,继续打印三个减号,再打印一个竖线,继续打印三个减号,最后一个竖线不打印。

注意:每一组打印完之后要记得换行。

代码实现:

//打印棋盘 版本二
void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)//控制行数
	{
		printf(" %c | %c | %c ", board[i][0], board[i][1], board[i][2]);
		printf("\n");
		if (i < row - 1)
		{
			printf("---|---|---");
			printf("\n");
		}
	}
}

但是这样就算成功了吗?

我们将ROW和COL改成5试试呢:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r8aAKaAE-1683595692503)(C:\Users\30539\AppData\Roaming\Typora\typora-user-images\image-20230508172925477.png)]

我们发现,棋盘虽然变成5行,但是列数并没有发生改变。

同理,在之前的基础上,我们也可以对列数进行分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rpRhVWGA-1683595692503)(C:\Users\30539\AppData\Roaming\Typora\typora-user-images\image-20230508173610689.png)]

整个棋盘按每一行分,也能将每一行的打印分为三部分,每部分由绿色和橙色组成。因为这个棋盘有三列,所以每一行的打印可以分为三组,假设是五列,就可以分为五组,那么每一行的打印就可通过列数来控制了。

通过观察可以发现:每一行要打印的绿色部分(三个空格和三个减号)的次数就是该棋盘的列数,每一行要打印的橙色部分(竖线部分)的次数就是该棋盘的列数-1次。所以我们可以将代码再改进:

//打印棋盘 版本三
void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)//控制行数
	{

		//每一行的打印  %c | %c | %c 
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);
			if (j < col - 1)
				printf("|");
		}

		//每一行的打印 ---|---|---
		printf("\n");
		if (i < row - 1)
		{
			int j = 0;
			for (j = 0; j < col; j++)
			{
				printf("---");
				if (j < col - 1)
					printf("|");
			}
			printf("\n");
		}
	}
}

4.下棋流程

棋盘打印好了就可以开始下棋了。

【玩家下棋】

首先让玩家下棋,玩家输入落子的坐标,再判断该坐标是否符合要求,符合要求就落子,若不符合要求就重新输入坐标。

注意:在玩家下棋时,由于不确定玩家是不是程序员,所以就不知道横纵坐标实际上是从0开始的。我们就设计成横纵坐标从1开始,在判断合法性时,稍作处理就好。在本游戏中x,y分别代表行号和列号,并不是严格的数学上的横纵坐标,例如3,6代表的是3行6列。

//玩家下棋
void Player(char board[ROW][COL], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("请玩家输入你的落子坐标>:(使用空格隔开)\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");
			continue;
		}
	}
}

【电脑下棋】

电脑下棋同理,让电脑生成相应数组下标范围内的随机数,再判断该下标的元素是否为空格(即未被占用),为空格则在该坐标落子,若不为空格,重新生成随机数,直到生成的随机数满足要求为止。

void Computer(char board[ROW][COL], int row, int col)//电脑下棋
{
	int x = 0;
	int y = 0;
	while (1)
	{
		x = rand() % 3;
		y = rand() % 3;
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

5. 判断胜负

在每一次下完棋之后,系统应该判断是否分出了胜负,所以在每次的下棋函数之后再写一个判断胜负的函数。

【判断胜负】

整个三子棋的游戏结局包含4中情况,分别是;

  1. 电脑获胜(返回#)
  2. 玩家获证(返回*)
  3. 平局(返回’Q’)
  4. 游戏继续(返回’C’)

【玩家/电脑获胜】

1.先判断行和列。

行:

	//判断行
	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][0];
	}

列:

//判断列
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];
}

2.再判断是否有斜着连着三个字的情况。(当然也可以先判断斜着再判断行和列)

//判断斜着
	if (board[0][0] == board[1][1] && board[1][1] == board[2][2]&&board[1][1]!=' ')
		return board[1][1];
	if (board[1][1] == board[0][2] && board[1][1] == board[2][0]&&board[1][1]!=' ')
		return board[1][1];

【判断是否为平局】

若电脑和玩家都没有获胜,此时就要根据棋盘上是否还有空格(即未落子区域),若还有空格,则游戏继续,若没有空格了,则达成平局。

int IsFull(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++)
		{
			if (board[i][j] == ' ')//有空格说明棋盘没满 返回0
				return 0;
		}
	}
	return 1;
}
//判断是否为平局
	if (IsFull(board, row, col) == 1)//等于1则说明平局
	{
		return 'Q';
	}
	else
		return 'C';

6.test.c

再来看test.c函数的设计

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void game()
{
	char board[ROW][COL] = {0};//创建一个3*3的数组
	InitBoard(board, ROW, COL);//初始化棋盘初始化成空格
	DisplayBoard(board, ROW, COL);//打印棋盘
	char ret = 0;
	while (1)
	{
		Player(board, ROW, COL);
		ret = IsWin(board, ROW, COL);//判断是否分出了胜负
		if (ret != 'C')
			break;
		DisplayBoard(board, ROW, COL);//打印棋盘
		printf("\n");
		Computer(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");
	}
}
void menu()
{
	printf("*******************\n");
	printf("******1. Play******\n");
	printf("******0. Exit******\n");
	printf("*******************\n");
}
int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));//调用随机数种子
	do
	{
		menu();
		printf("请输入你的选择>:\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏!\n");
			break;
		default:
			printf("你的选择有误,请重新选择!\n");
			break;
		}
	} while (input);
	return 0;
}

当代码来到22行时,有两种情况,一种是某一方获胜,一种是达成平局,所以我们只需对ret的值进行判断就可以分辨出游戏最终结果。

7. game.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
int IsFull(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++)
		{
			if (board[i][j] == ' ')//有空格说明棋盘没满 返回0
				return 0;
		}
	}
	return 1;
}
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] = ' ';
		}
	}
}



//打印棋盘 版本一
//void DisplayBoard(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++)
//		{
//			printf("%c", board[i][j]);
//		}
//		printf("\n");
//	}
//}



//打印棋盘 版本二  让棋盘可视化
//void DisplayBoard(char board[ROW][COL], int row, int col)
//{
//	int i = 0;
//	for (i = 0; i < row; i++)//控制行数
//	{
//		printf(" %c | %c | %c ", board[i][0], board[i][1], board[i][2]);
//		printf("\n");
//		if (i < row - 1)
//		{
//			printf("---|---|---");
//			printf("\n");
//		}
//	}
//}


//打印棋盘 版本三  让棋盘可视化,实现通用版
void DisplayBoard(char board[ROW][COL], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)//控制行数
	{

		//每一行的打印  %c | %c | %c 
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf(" %c ", board[i][j]);
			if (j < col - 1)
				printf("|");
		}

		//每一行的打印 ---|---|---
		printf("\n");
		if (i < row - 1)
		{
			int j = 0;
			for (j = 0; j < col; j++)
			{
				printf("---");
				if (j < col - 1)
					printf("|");
			}
			printf("\n");
		}
	}
}


void Player(char board[ROW][COL], int row, int col)//玩家下棋
{
	int x = 0;
	int y = 0;
	printf("请玩家输入你的落子坐标>:(使用空格隔开)\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");
			continue;
		}
	}
}

void Computer(char board[ROW][COL], int row, int col)//电脑下棋
{
	int x = 0;
	int y = 0;
	while (1)
	{
		x = rand() % 3;
		y = rand() % 3;
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

char IsWin(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][0];
	}
	//判断列
	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[1][1] == board[0][2] && board[1][1] == board[2][0]&&board[1][1]!=' ')
		return board[1][1];

	//判断是否为平局
	if (IsFull(board, row, col) == 1)//等于1则说明平局
	{
		return 'Q';
	}
	else
		return 'C';
	
}

8. game.h

#pragma once
#define ROW 3//行
#define COL 3//列
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
void InitBoard(char board[ROW][COL],int row,int col);//初始化棋盘
void DisplayBoard(char board[ROW][COL], int row, int col);//打印棋盘
void Player(char board[ROW][COL], int row, int col);//玩家下棋
void Computer(char board[ROW][COL], int row, int col);//电脑下棋
//玩家赢了返回 '*' 
//电脑赢了返回 '#' 
//平局返回 ‘Q'
//继续游戏返回 'C'
char IsWin(char board[ROW][COL], int row, int col);//判断是否分出了胜负

9. 完结

本章的内容就到这里啦,若有不足,欢迎评论区指正,最后,希望大佬们多多三连吧,下期见!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

这里是彪彪

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值