c语言期中实战项目—三子棋,思路分析+代码详细注释

c语言期中实战项目—三子棋,思路分析+代码详细注释>

项目简述

  • 游戏简介
    此游戏很简单,就是打印一个三行三列的空格,外加一些字符,构成如下的棋盘,游戏玩家和电脑在棋盘里面加元素,当谁填充元素使行或者列,或者对角线元素三个相同时,谁就获胜,但当棋盘满时也结束游戏。(其实就是井字棋,高中无聊上课玩的),棋盘如下,注意这里的 | 是两根短的 | 拼在一起的。
    在这里插入图片描述

此项目综合应用了c语言中的变量的定义,输入,输出函数,循环,选择的嵌套使用,指针,二维数组的初始化和使用等知识。希望通过阅读这篇博文与练习代码,提高你的代码书写能力及对知识的理解应用。

思路分析

  • 多文件编程拆分代码
    **随着编程学习的深入,程序越编越庞大,可读性降低,这使得我们编写程序复杂,检查调试程序成为一项很困难的事情,分模块可以使初学者更好地理解程序,理解程序的思维。多文件(模块化)开发C程序的方法,就是把不同功能的函数封装到不同的文件中,多个.c文件和一个.h文件,c语言中主函数调用其他文件中的函数实现相应的功能。**此次项目也将程序分为三个模块,各模块具体如下图:
    在这里插入图片描述

  • 代码整体思路流程图

c语言中默认以main函数作为程序的入口,test.c文件中有main函数,所以整个项目从test.c开始,首先打印菜单(玩不玩都有的),之后调用game.c里面的函数,(下面图片的7个函数列出了)之后返回test.c打印结果

在这里插入图片描述
在这里代码用到了许多的返回值,其实这里是程序需要,下面是我写程序,理解代码时查阅资料对函数调用return 带返回值的一些讲解

这里是引用return 表示把程序流程从被调函数转向主调函数并把表达式的值带回主调函数,实现函数值的返回,返回时可附带一个返回值,由return后面的参数指定。 return通常是必要的,因为函数调用的时候计算结果通常是通过返回值带出的。 如果函数执行不需要返回计算结果,也经常需要返回一个状态码来表示函数执行的顺利与否(-1和0就是最常用的状态码),主调函数可以通过返回值判断被调函数的执行情况。

在此次代码中我认为game是主调函数,它调用的函数是被调函数。

代码及注释

代码是简单的三子棋,电脑不会智能的下棋,是随机的,定义了#define定义的宏常量行和列可以打印n*n棋盘,但功能没完善,待改善。代码后面就有详细的注释,所以过程就不详细的讲了,难的我会单独提出来说一下,建议复制到编译器细细研究。
test.c文件代码

#define _CRT_SECURE_NO_WARNINGS

#include "game.h"//包含game.h里面的头文件

void menu()//打印菜单
{
	printf("******************************\n");
	printf("******    1. play        *****\n");
	printf("******    0. exit        *****\n");
	printf("******************************\n");
}

void game()
{
	//存储数据 - 二维数组
	char board[ROW][COL];
	//初始化棋盘 - 初始化空格
	InitBoard(board, ROW, COL);

	//打印一下棋盘 - 本质是打印数组的内容
	DisplayBoard(board, ROW, COL);
	char ret = 0;//接受游戏状态,判断结果
	while (1)
	{
		//玩家下棋
		PlayerMove(board, ROW, COL);
		//下完打印棋盘,打印下过棋的样子
		DisplayBoard(board, ROW, COL);
		ret = IsWin(board, ROW, COL);//判断玩家是否赢得游戏,下一次棋,判断一次
		if (ret != 'C')//不等于‘C'就是为#或者*或者Q,因为被调函数里return会返回其中一个字符,就是说通过该字符可以判断赢输,平局,之后就要跳出循环,在下面判断游戏结果
			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");//返回值非*非#,为Q
	}
	DisplayBoard(board, ROW, COL);
}

int main()
{
	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);

	return 0;
}

game.c里面的代码

在这里插入代码片#define _CRT_SECURE_NO_WARNINGS
#include "game.h"//包含game.h里面的头文件
void InitBoard(char board[ROW][COL], int row, int col)//自定义函数,前面的字母含义是:void返回值类型,InitBoard函数名,括号里面的是形式参数
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)//ROW,COL是#define定义的标识符常量,值为 3,3,循环打印的9个空格元素,二维数组的下标也是从0开始
		{
			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]);//打印棋盘空的位置,%c那用数组初始化的空格代替,两边的空格是为了美观,打印棋盘时参照图片思考
			if (j < col - 1)//为什么要减一,是因为数组下标从0开始,最后一列不打印
				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 PlayerMove(char board[][COL], int row, int col)
{
	int x = 0;
	int y = 0;

	printf("玩家走:>\n");

	while (1)
	{
		printf("请输入下棋的坐标:>");
		scanf("%d %d", &x, &y);
		//判断坐标合法性,棋盘上人输入的坐标
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//下棋,只有坐标合法才下,不合法就提示,重新输入,第一个if与最后一个else配对
			//坐标是否被占用
			if (board[x - 1][y - 1] == ' ')//二维数组的两个下标都是从0开始,所以要减一
			{
				board[x - 1][y - 1] = '*';
				break;//while那里为真1一直循环,当你在坐标上放上一个符号就跳出循环,用户只能下一次,不能多次,一定要有break
			}
			else
			{
				printf("坐标被占用, 请重新输入\n");
			}
		}
		else
		{
			printf("坐标非法, 请重新输入\n");
		}
	}
}

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

	while (1)
	{
		int x = rand() % row;//限制电脑下棋位置,取余产生0—2的数字,,调用rand 函数,该函数有讲解外面
		int y = rand() % col;

		//判断占用
		if (board[x][y] == ' ')
		{
			board[x][y] = '#';
			break;
		}
	}
}

//循环的嵌套,遍历数组
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;//棋盘满了
}
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][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];
	}

	//之前先判断是否赢,没赢再在这里判断平局
	int ret = IsFull(board, row, col);
	if (ret == 1)
	{
		return 'Q';//通过返回值,返回给主调主调函数里的ret,打印游戏结果,也就是test.c所在的game函数
		         //一个函数可以有多个retuen 但只可以有一个返回值,返回了上面的*或者#就不会返回这里的Q或者C,这里很重要
	}

	//继续
	return 'C';
}

v
game.h里面的代码

#pragma once
//头文件的包含,test.c game.c里面用到的函数声明
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


//符号的定义,#define定义的标识符常量,这样可以轻松的整体改变数组大小,可以自定义几行几列的棋盘,免去了之后多次改变数组行和列
#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);

//
//1. 玩家赢了 - *
//2. 电脑赢了 - #
//3. 平局 - Q
//4. 游戏继续 - C

//判断游戏是否有输赢
char IsWin(char board[ROW][COL], int row, int col);

知识补充;
在这里我们用到一个rand 函数,他的作用是产生一个0-32767的随机数,但在用它之前要条用另一个函数srand,
函数里面用time 函数作为种子,如果种子不变产生的数字也就不随机了,函数返回值为unsignedint,变量为空指针,因此该函数写为scrand((unsignedint)time(NULL));

总结及注意事项

其实上面的分析说的已经够了,其余的问题自己都可以决解的。没有太大的注意事项,只是写代码时大家可以尝试大化小,小化大,先把一个大的问题拆分成几个小问题,分模块,分函数,再组合再实现整体的功能,这一思想我们以后工作中可能接触的会更多。
代码有待改善不是很完美,文章哪里写的有问题欢迎评论指正,有不懂得代码评论解析,关注博主不迷路,后期更新扫雷实战项目。记得点赞鼓励哦!!!!

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值