一. 写在前面的话
众所周知,"#"字形游戏在几乎所有人的校园时光中都带来过一定的欢声笑语,那么,我们是否有可能用有限的技术在电脑上实现简单的一个“#“字形游戏呢?
可喜的是,在我们学完二维数组以后,就已经掌握了"#"字形游戏的核心技术,完全可以在终端上敲出这样的小程序了。
二. 项目构建流程
//注意:这里笔者用的软件是mac版本的VScode,具体流程在win上类似
1. 第一步 创建项目
我们首先需要先创立一个文件夹jingziqi,并在其中创立三个子项目文件:game.c game.h test.c
我们将在在game.h中定义一系列需要使用到的函数,引用需要的库文件,并define一些基本参数,方便后期修改,在game.c文件中我们将实现我们定义的函数,并在test.c中调用它们。
这样的做法可以使我们的代码的可阅读性大大提高,是相对合理的书写规范。
2. 第二步 文件互相引用
我们打开game.h 在这里我们需要引入我们需要的库文件和定义我们需要的变量的值
示例如下:
而以后我们便不再需要重复调用<stdio.h> <stdlib.h>等库文件,而只需要调用game.h便可以了
示例如下:
实际上,由于我们的game.c文件需要geme.h中的库和数值,test.c需要game.c和game.h中的函数和库,所以可以game.c调用game.h,而test.c调用game.c实现最简化调用。
这里还是有细节可以注意的,比如在引用同目录下文件时,我们往往用检索能力更强的双引号" ",而对于系统自带的库,我们用< >引用即可。
3. 搭建框架
3.1 test.c文件
首先,我们当然需要一个main()函数作为程序的主入口。
我们需要在test.c中实现这个入口,这也意味着以后的程序启动也是由test.c文件负责。
在main函数中,我们需要完成一个游戏的基本功能,即:输入一定的数值,然后根据输入判定游戏开始/退出/输入非法重新输入。
具体的实现逻辑,可以用一个menu菜单函数返回一个input类型的值,最后用switch判断input的值并执行相应的代码。
//test.c文件部分
int main(){
int input;
do{
input = menu();
switch (input)
{
case 1:
printf("开始游戏!\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入非法,请重新输入!\n");
}
}while(input);
return 0;
}
3.2 game.h文件
显而易见的是,我们需要在game.h文件中定义menu()函数,game()函数,并在game.c中实现它们的功能。
这是我们要在game.h中写的代码,注意末尾的;,具体实现我们需要放在game.c中
需要特别注意的时,不管是我们在test中直接调调用的函数menu() game()还是后续在game.c的game()函数内部调用的函数如InitBoard() Playerturn()等等,都需要在game.h中声明,这么做的目的是方便管理自写函数。
本程序所需手写的所有函数如下:
//game.h中定义函数部分
int menu();
void game();
void InitBoard(char board[ROW][COL], int row, int col);
void PriBorad(char board[ROW][COL],int row,int col);
void PlayerTurn(char board[ROW][COL],int row,int col);
void ComputerTurn(char board[ROW][COL], int row, int col);
char CheckBoard(char board[ROW][COL],int row,int col);
3.3 game.c文件
在game.c文件中,我们要填充在game.h文件里声明的所有函数,当然,在这个文件中也可以调用内部的函数,这正是我们在game()函数中做的。
menu()函数较为简单,我们可以顺手解决,然后集中精力完成game函数
以下是menu函数的具体内容,我们将在game.c中实现
int menu(){
int input;
printf("********************************\n");
printf("***** 欢迎来到井字棋游戏 *****\n");
printf("***** 1. 开始游戏 *****\n");
printf("***** 0. 退出游戏 *****\n");
printf("********************************\n");
scanf("%d",&input);
return input;
}
由此,游戏的框架已经搭建完毕,运行test.c文件已经初有模型,接下来,我们需要完成最复杂的game函数的实现。
4. game函数的实现
1. 创建和初始化棋盘InitBoard()
我们需要一个用于下棋的棋盘,这是简单的
char board[ROW][COL];
ROW 和 COL我们之前都已经define过,从这里开始可以直接调用
由于刚刚创建的board数组中没有储存确定的数据,因此直接打印到棋盘上会出现行列歪斜(效果图可看下文“试运行”),因此我们需要对棋盘进行全元素' '的初始化设定(空格)。利用for循环这是容易实现的,我们可以吧for循环装进InitBoard()函数:
void InitBoard(char board[ROW][COL], int row, int col)
{
int i,j;
for(i=0;i<row;i++){
for(j=0;j<col;j++){
board[i][j]=' ';
}
}
}
InitBoard()函数可以直接写在game()函数的上方
接下来便是在game()函数中调用这个函数了,注意传参
//初始化棋盘 令其中元素均为' '
char board[ROW][COL];
InitBoard(board,ROW,COL);
由此,棋盘棋子初始化便完成了。
2. 打印棋盘函数PriBoard()
打印棋盘在游戏中几乎是必不可少的操作,接受玩家每一步指令之后我们几乎都要打印一次棋盘到屏幕上,实现相关功能以后,我们将频繁调用这个函数
我们的目标棋盘样式为:
这同样也是容易的,我们可以把 | | 和---|---|---作为一组循环打印三次,只需要注意第三次打印的时候不把---|---|---打印上去即可,这样的功能我们通过if条件语句实现
void PriBorad(char board[ROW][COL],int row,int col)
{
for(int 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");
}
}
这样一来,打印棋盘的功能就被实现了。我们在game()中调用它,作为开始游戏的第一个提醒,
与此同时,我们也需要给出其他提醒语句,示例如下:
//打印棋盘
PriBorad(board,ROW,COL);
//提示语句
printf("请玩家选择棋盘的横纵坐标落子\n");
printf("(例如,如果想落在左上角处,请输入:1 1)\n");
3. 落子机制和判定机制
这几乎是整个游戏中最复杂的一段代码,但整体难度其实并不高。
游戏逻辑在于,在接收到系统的提醒语句之后,玩家会开始给出坐标信息,系统就此在先前初始化的board数组中填充"#",同时棋盘会被打印一次,接下来电脑通过算法会填充数组中的一个非空元素为"*",同时再次打印一次棋盘。循环往复。
分析这段逻辑之后,我们发现,每次电脑/玩家操作完之后,不仅需要立刻打印一次棋盘,同时也需要给出判定,判定玩家是否获胜/电脑是否获胜/数组是否被填满但未分出胜负(平棋)。
由此我们知道,实现这样的功能需要一个while循环嵌套三个新的函数PlayerTurn() CompuerTurn()和CheckBoard()
3.1 玩家落子函数PlayerTurn()
根据分析,玩家无法落子的情况只有两种,一种是输入的坐标函数数组越界,另外一种是输入的数组坐标已经被玩家/电脑 占用。我们可以用两个if语句实现相应情况的判断。
而显然的是,如果输入错误,玩家有机会返回重新输入,所以这些代码仍然是嵌套在一个while循环之中的,只有输入正确可以break跳出循环。
以下是代码的具体实现:
void PlayerTurn(char board[ROW][COL],int row,int col)
{
printf("请玩家落子\n");
while(1){
int x,y;
scanf("%d%d",&x,&y);
if (x >= 4 || x < 1 || y >= 4 || y < 1)
{
printf("输入数字违法,请重新输入!");
continue;
}
else if (board[x-1][y-1] == ' ')
{
board[x-1][y-1] = '#';
PriBorad(board,row,col);
break;
}
else {
printf("输入处已被占用,请重新输入!\n");
continue;
}
}
}
3.2 电脑落子函数ComputerTurn()
电脑落子函数,其是可以算作是一个算法而细细研究。"#"字形游戏先手不输的结论已经不再是秘密,这里我们游戏中的电脑是后落子,我们可以设计一款算法,让电脑在先手情况下处于不输的地步,但是为了简单起见,我们暂时为电脑设计随机落子,后续知识充沛了再回来完善算法,让电脑更加聪明。
需要注意的是,由于我们取随机数时已经限制了范围,所以相比较于玩家落子的判定,少了一个if条件语句,其他逻辑几乎是一样的。
具体实现如下:
void ComputerTurn(char board[ROW][COL], int row, int col)
{
printf("电脑落子\n");
while(1)
{
int x = rand()%row;
int y = rand()%col;
if (board[x][y] == ' ')
{
board[x][y] = '*';
PriBorad(board,ROW,COL);
break;
}
else
continue;
}
}
3.3 判定胜负函数CheckBoard()
判定胜负的逻辑也是简单的
首先可以横着三行检查,若数组元素相等且不为' ',则相应的玩家或电脑获胜;
竖着三列同理;
接下来,是斜着×型检查;
最后,else检查board数组是否被填满,如果代码运行到这并判定填满,则显然平局
我们用大量的for循环实现上述伪代码:
char CheckBoard(char board[ROW][COL],int row,int col)
{
//判定胜负情况
for(int i = 0; i <3; i++){
if(board[i][0]==board[i][1] && board[i][2]==board[i][1] && board[i][1] != ' ')
return board[i][0];
}
for(int i = 0; i <3; i++){
if(board[0][i]==board[1][i] && board[2][i]==board[1][i] && board[1][i] != ' ')
return board[0][i];
}
if (board[1][1]==board[0][0] && board[0][0]==board[2][2] && board[1][1] != ' ')
return board[1][1];
if (board[2][0]==board[1][1] && board[1][1]==board[0][2] && board[1][1] != ' ')
return board[1][1];
//判定平局情况
int s = 0;
for(int i = 0; i < 3; i++){
for(int j = 0; j<3; j++){
if (board[i][j] == ' ')
s++;
}
}
if (s == 0)
return 'Q';
//否则则未分出胜负,返回'C'
else
return 'C';
}
值得一提的是,检查胜负函数并不是一个void类型的函数,我们需要创建相应的变量接受它不同情况的返回值,这决定了game()中何时跳出while循环。
4. 组装函数
我们至此终于完成了所有需要函数的实现,接下来就是在game()函数中一一调用它们。
void game(){
//初始化棋盘 令其中元素均为' '
char board[ROW][COL];
InitBoard(board,ROW,COL);
//打印棋盘
PriBorad(board,ROW,COL);
//提示语句
printf("请玩家选择棋盘的横纵坐标落子\n");
printf("(例如,如果想落在左上角处,请输入:1 1)\n");
char index;
while(1){
//玩家落子
PlayerTurn(board,ROW,COL);
index = CheckBoard(board,ROW,COL);
if (index == '#'){
printf("玩家胜利!\n");
break;
}else if (index == '*'){
printf("电脑胜利!\n");
break;
}else if (index == 'Q'){
printf("平局\n");
break;
}
//电脑落子
ComputerTurn(board,ROW,COL);
index = CheckBoard(board,ROW,COL);
if (index == '#'){
printf("玩家胜利!\n");
break;
}else if (index == '*'){
printf("电脑胜利!\n");
break;
}else if (index == 'Q'){
printf("平局\n");
break;
}
}
}
由此,一个简易的"#"字形游戏就搭建完成了。
三. 试运行
写完代码之后当然要试运行一下,感受一下自己的劳动成果了。
这是我们最开始写的menu()函数
我们输入1开始游戏
这里可以看到,我们之前初始化的函数InitBoard和打印函数PriBoard()生效了,如果注释掉InitBoard()函数,我们也可以看看相应的效果:
由此便可见初始化的重要性了。
继续往下看,在玩家输入坐标1 1的时候棋盘被打印一次,电脑瞬间落子之后棋盘又被打印一次,显然Check()函数没有打断这里的while循环,接下来while循环到玩家继续落子。
如果输入非法,相应的效果也是不错的
输入占用情况和输入越界情况:
最后检查判定模块:
1
所有功能实现,小程序成功!