前面我们讲解过一个猜数字游戏的实现,想来应该让大家感受到了属于编程的趣味性,并且在实现过程中应该也收获了知识。但猜数字这种简单的游戏肯定满足不了大家对于游戏的高标注、严要求,估计玩不了多久就会没有兴趣了,所以,今天在这里和大家分享一个更好玩,也更有实现难度的小游戏——井字棋!相信大家都不会对这个游戏陌生的(可能有朋友不久前还在学校和小伙伴一起玩呢),那么今天就让我们一起通过编程把这个小游戏实现吧。
1.井字棋游戏的大致流程
首先,由计算机产生一个3*3大小的棋盘,并显示玩家和计算机各自使用的棋子,然后由玩家、计算机轮流在上面落子,哪一方先3颗棋子连成一条线,则显示该方获胜,如果棋局填满,还没有一方三子连线,则判定为和棋。
2.游戏实现的思路
还记得我在猜数字游戏的实现中说过的一句话吗?没错,我们在写编程题或者小游戏代码时,最重要的就是理清实现思路——主体是什么?为了实现目的要创建哪些函数?函数的功能都是什么?只有当我们心中有了一个大体的框架,知道该做些什么时,我们才能更高效地编写代码,完成程序设计。
井字棋游戏的算法如下:
1.menu(提供游戏菜单,由玩家选择是否进行游戏:按“1”开始游戏,按“0”退出游戏,按其他则显示“选择错误,请重新选择”。)
2.game( )(进行游戏)
2.1 InitBoard(初始化棋盘)
2.2 DisplayBoard(打印未放置任何棋子的棋盘,并显示玩家和计算机分别分配的棋子类型,并提示玩家先下棋)。
2.3 PlayerMove(玩家下棋),在棋盘上显示位置,并ret=IsWin(判断是否胜利)。
2.4 ComputerMove(电脑下棋)在棋盘上显示位置,并ret=IsWin(判断是否胜利)。
2.5 根据ret(定义判断结果的变量)判断最终结果,若玩家胜利,则显示“亲爱的玩家,恭喜你获得游戏胜利!”;若电脑胜利,则显示“亲爱的玩家,请不要因失败而气馁,期待你的下一次开始!”;若平局,则显示“亲爱的玩家,该局游戏平局!”
3.本轮游戏结束,打印游戏菜单并再次询问玩家选择。
如上面就是井字棋游戏大体框架的算法呈现。
我们在思考大体框架时不用过多在意具体函数的实现方法,可以先起个能表达其功能的函数名并把它放在需要的位置。(说白了就是先搞一个空壳函数占位置,等到大体框架调试完毕不再有问题后,再去实现这些空壳函数)大体框架代码如下:
#include<stdio.h>
int main()
{
int input;
do
{ //menu函数在调试大体框架时,也可以是空壳函数
menu();//打印游戏菜单。我们建立函数可以让主函数不至于太过冗长,而且增加了代码的可读性,使程序模块化。
printf("请选择:>");
scanf("%d", &input);
switch(input)
{
case 1:
game();//guess是用来猜测并判断是否正确的函数,这里也是空壳函数
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
}while(input);
return 0;
}
可能有些朋友会产生这样疑惑:为什么在上面的算法中出现的DisplayBoard、PlayerMove、ComputerMove和IsWin等函数没有出现呢?因为上面这些函数都是在进行游戏中的分支函数,所以我们在大体框架中就不需要写那么详细。
menu函数的代码实现如下:
void menu()
{
printf("*****************************************\n");
printf("**********1.play 0.exit*************\n");
printf("*****************************************\n");
}
game函数的实现涉及到一个新的知识点,我们在后面一点再详细介绍。
3.多文件程序
实际上在C语言中,我们根据程序文件的数量,可以将C语言程序分为单文件程序和多文件程序,单文件程序就是所有程序代码都在一个源程序文件中,多文件程序中通常包含一个或多个自定义头文件和一个或多个源程序文件。严格地讲,结构化程序应该使用多文件结构,尤其对于大型程序。
3.1多文件程序的构成
多文件程序中通常包含一个或多个自定义头文件和一个或多个源程序文件,每个文件称为程序文件模块。通常编程环境都使用工程来管理程序文件模块,若将若干个程序文件模块添加到一个工程中,再点击“连接”按钮,就能将这些程序文件模块连接成一个可执行文件。
在多文件程序中,头文件通常包含程序文件模块的共享信息,如符号常量定义、数据类型定义、全局变量定义和函数原型。因为当我们把这些符号常量和全局变量放到头文件中,然后用#include预处理指令将这个头文件包含到源程序文件中,这样就不必重复定义这些符号常量和全局变量,从而避免重复工作,减少差错和编译运行。在多文件程序中,源程序通常包含主函数和其他函数定义,相应的函数原型一般放在头文件中。由于整个程序的运行只能从主函数main开始,所以,有且只有一个源程序文件包含主函数。一般将主函数放到一个源程序文件中,将其他函数定义组成若干个源程序文件。具体情况如下图:
3.2将源程序文件分解为多个程序文件模块
如果源程序文件的规模较大,应该将源程序文件分解为几个程序文件模块;如果一个项目需要多人开发,应该将任务分解,每个人编写的程序代码放在增加的程序文件模块中。如何将源程序文件分解为多个程序文件模块呢?在多文件程序中,一般将函数定义在源程序文件中,相应的函数原型放在头文件中,为了方便编译器检查头文件中的函数原型与源程序文件中的函数定义是否一致,在定义函数的源程序文件中也包含该头文件。
综合上面两大段话和一张图片,相信大家对于多文件程序应该有了自己的初步认识,但是为了加深大家的理解程度,方便大家记忆,下面我分享一下个人的理解归纳(当然,有朋友有更透彻的见解,欢迎评论区谈论):
在我看来,多文件程序本质上是供多人协同开发大型项目的,而我们个人在写代码量较大的程序也可以使用创建多文件程序,因为这样不仅会使代码模块化,而且会使主函数很清晰简洁,提高我们的工作效率。而多文件程序的组成可以看作“目录+目录内容的具体介绍+主函数”——我们可以把包含程序文件模块共享信息的头文件看作“目录”,里面有我们要使用的符号常量定义、全局变量定义、函数原型等;而大多数源文件就是“目录内容的具体介绍”,也就是说大部分源文件是用来实现“目录”中函数的定义的,它们是一个个小小的模块,在源程序中定义完毕,等待主函数的调用;每个项目有且仅有一个主函数,而这个主函数就是借用“目录”中的所有内容来完成程序设计最终目的的。简言之,我们可以把程序需要解决的问题看作学习中的难题,含主函数main的源程序文件就是我们最后写的“答案”,包含程序文件模块共享信息的头文件就是我们解决难题所需的课本的“目录”,除含main函数的源程序文件就是目录中涉及到的知识点的“详细讲解”。
4.井字棋游戏的代码实现
上面围绕多文件程序介绍那么久,相信聪明的大家我们接下来要干什么了吧😏
game函数的代码如下:
//游戏的整个算法实现
void game()
{
char ret = 0;
//数组—存放走出的棋盘信息 通过数组建立一个棋盘,为什么是数组相信看过数组讲解的可以会明白。
char board[ROW][COL] = { 0 };//建立行列,理应让棋盘上都是空格,0是不可打印字符
//初始化棋盘
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\n");
}
else if (ret == '#')//if ,else if,else后面都没有分号
{
printf("亲爱的玩家,请不要因失败而气馁,期待你的下一次开始!\n\n");
}
else
{
printf("亲爱的玩家,该局游戏平局!\n\n");
}
}
game函数的算法已经展示出来了,那么我们就可以给出井字棋游戏代码实现这个“难题”的“答案”了——含main主函数的源程序程序,如下:
/* main.c */
#include<stdio.h>
#include"game.h"
void menu()
{
printf("*****************************************\n");
printf("**********1.play 0.exit*************\n");
printf("*****************************************\n");
}
//游戏的整个算法实现
void game()
{
char ret = 0;
//数组—存放走出的棋盘信息
char board[ROW][COL] = { 0 };//建立行列,理应让棋盘上都是空格,0是不可打印字符
//初始化棋盘
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 == '#')//if ,else if,else后面都没有分号
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
}
int main()
{
int input;
srand((unsigned int)time(NULL));//我们要想让电脑自己下棋,肯定要设置符合棋盘的随机数,不懂随机数的可以看看猜数字中的介绍
do
{ //menu函数在调试大体框架时,也可以是空壳函数
menu();//打印游戏菜单。我们建立函数可以让主函数不至于太过冗长,而且增加了代码的可读性,使程序模块化。
printf("请选择:>");
scanf("%d", &input);
switch(input)
{
case 1:
game();//guess是用来猜测并判断是否正确的函数,这里也是空壳函数
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
}while(input);
return 0;
}
虽然我们完成了难题,但是就像有些论文要求我们标注参考文献一样,我们还要写出自己参考了“哪本书”——头文件,避免我们用的知识点“老师”——系统无法识别。
game函数中所涉及内容的头文件的代码如下:
/* 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 PlayerMove(char board[ROW][COL], int row, int col);
void ComputerMove(char board[ROW][COL], int row, int col);
//告诉我们四种游戏的状态
//玩家赢-'*'
//电脑赢-'#'
//平局-'Q'
//继续-'C'
char IsWin(char board[ROW][COL], int row, int col);
有目论,肯定要实现,不然用不了。
game函数中所涉及内容的源程序文件如下:
/* 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;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
//1.打印一行数据
printf(" %c ", board[i][j]);//printf( %c | %c | %c ) 可以直接打印棋盘 ,但效果不好,太死板了
if (j < col - 1)
printf("|");
}
printf("\n");
//2.打印分割线
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)
{
scanf("%d%d", &x, &y);
//判断左标的合理性
if (x >= 1 && x <= row && y >= 1 && y <= col && board[x - 1][y - 1] == ' ')
{
//玩家理解的坐标和数组不同
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("坐标非法,请重新输入\n");
}
}
}
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑走:>");
while (1)
{
x = rand() % row;
y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
printf("电脑下的坐标为:%d %d\n", x + 1, y + 1);
}
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][0] == 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[0][i] == board[2][i] && board[1][i] != ' ')
{
return board[1][i];
}
}
//两个对角线
if (board[0][0] == board[1][1] && board[0][0] == board[2][2] && board[1][1] == board[2][2] && board[1][1] != ' ') //这有传递性比两组即可但不能是空格
return board[1][i];
if (board[2][0] == board[1][1] && board[1][1] == board[0][2] && board[1][1] != ' ')
return board[1][1];
//判断是否平局
if (1 == IsFull(board, ROW, COL))
{
return 'Q';
}
return 'C';
}
各位朋友们,代码到这里的话已经是完结了。大家只需要按照顺序分别建立两个源程序文件和一个头文件(当然注意代码细节哦),应该就能实现这个很经典的小游戏喽🤭
不过,值得一提的是大家记住一定要把多个程序文件模块添加到一个工程中,再点击“连接”按钮,将这些程序文件模块连接成一个可执行文件。如果是用VS就是在新建的项目中再创建一个源程序文件和一个头文件;用Dev的话就要点击建立项目,而不是源代码哦(其他编译器同理)。
当然,这个代码还有很多值得开发的地方,比如让笨笨的电脑变聪明一点,让井字棋变成五子棋等等,希望大家不要被限制了想象,多多开动脑筋,期待在下一次发布关于井字棋升级的文章时我们有同样的升级方案!😀