今天来写一个简单的三子棋游戏,与上次写的猜数字游戏一样是一个涉及代码较多的一个程序,接下来展开细致的剖析。
首先先规划一下,游戏的代码比较多,我们不可能把所有代码写在.c后缀的main函数里面,所以我们一共会拆开分为三大部分,test.c里面放的是游戏的测试逻辑,game.c里面放的是游戏的实现逻辑,最后再把游戏实现函数的声明放入game.h,这样调用的话就分工明确,代码可读性也变得很高。文件创建就如下图所示:
当创建好文件后,首先在test.c里面写一个调用test函数的流程,接着就是打印菜单提示玩家选择进行游戏,代码如下图所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void menu()
{
printf("**************************\n");
printf("******** 1.play ********\n");
printf("******** 0.exit ********\n");
printf("**************************\n");
}
void test()
{
int input = 0;
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch(input)
{
case 1:
printf("三子棋\n");
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误\n");
break;
}
}
while(input);
}
int main()
{
test();
return 0;
}
这里需要注意的就是,我们要设计好程序判断用户是否输入的是数字0或者1,然后用上swtich语句提示用户该怎么选择,该怎么输入,大概的游戏菜单就是这样设计。
如上图中case1中的打印三子棋只是演示给大家看,真正我们要实现这个三子棋游戏就要开始写函数了,接下来我们就要开始写一个game函数来实现,所以对用case1那个地方要把printf("三子棋\n")改成game()。现在先呈现给大家看一看这个游戏最终运行的效果是怎么样的,然后再从这个结果中来剖析告诉大家三子棋这个游戏是怎么一步步实现的,再结合起来写game函数,效果如下图所示:
这里可以看到,玩家选择玩游戏之后,屏幕上先呈现出的是一个空的棋盘,然后玩家填入坐标选择棋子所下的位置,电脑也会随之进行游戏,这就是游戏整体的效果。当我们程序员看到这个界面的时候要有一定的思考,看到图中玩家一共下了3步棋,其中都依次给记录下来了,不管是玩家还是电脑的数据都给存起来下一次打印。
然后最重要的一点就是,我们可以从图中看到,这就是一个3行3列的一个棋盘,共9个位置,9个位置的数据都要存起来,这不就跟我们的二维数组非常的相似吗 ! 而我们这里可以看到里面放的是字符的二维数组,所以现在整体思路就有了。
现在先开始规划棋盘,我们知道这是一个3乘3的棋盘,所以这里先假设一个3乘3的数组char board[3][3]棋盘,而且我们可以看到棋盘每一格里面的符号左右都有一个空格,还有一些竖杠和斜杠,这些都是为了修饰棋盘把棋盘设计更好看一些,更有空间感。
我们期望一开始的棋盘全部都是空格,这样游戏才能进行下去,程序设计如下:
void game()
{
//数据存储到一个字符的二维数组中,玩家下棋是'*',电脑下棋是'#'
char board[3][3] = {0};//数组的内容应该全部是空格
InitBoard(board, 3, 3);//初始化棋盘
}
但是这样设计有个弊端,大家想象一下,接下来要写的代码要对数组的行列进行变化操作,如果这里每次都写上对应的行列数,那么假设某一天这个3乘3的数组不够用了,要一个5乘5的棋盘,那么程序中的3行3列这些数字全部都要跟着修改,非常的麻烦,为了减少这种不必要的操作,我们在未来遇到这种需要频繁改变的变量,可以在之前拆分开的头文件里面先定义一个行和列:
那么这样的话,大家还要记得在前面包含以下头文件,程序员自己写的头文件使用双引号,我们这里对应的就用#include"game.h"。那么以后要改行列数的话,所有涉及到的地方,都会随之改变,非常方便,最后代码改善成如下图:
#include<stdio.h>
#include"game.h"
void menu()
{
printf("**************************\n");
printf("******** 1.play ********\n");
printf("******** 0.exit ********\n");
printf("**************************\n");
}
void game()
{
//数据存储到一个字符的二维数组中,玩家下棋是'*',电脑下棋是'#'
char board[ROW][COL] = {0};//数组的内容应该全部是空格
InitBoard(board, ROW, COL);//初始化棋盘
}
接下来为了实现InitBoard这个初始化函数,这是一个游戏模块的代码,所以我们还要在头文件里面声明一下:
把参数都传进去函数里头后,我们就要把函数放到game.c里面开始实现函数了,我们知道二维数组要用两层for循环来实现,所以代码编写如下:
#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] = ' ';
}
}
}
把第i行第j列的元素都初始化成空格,写到这一步的时候就可以打印棋盘了,这里给到一个DisplayBoard函数,也要在头文件声明一下,进行传参,再在game.c中实现。
然后进行这个DisplayBoard的实现,有的同学可能就上来直接把棋盘的结果理解成,一个空格加%c打印字符再加一个空格,就会把代码写成如下图:
void DisplayBoard(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++)
{
printf(" %c ",board[i][j]);
}
printf("\n");
}
}
这样我们先来测试一下这段代码有什么弊端,运行如下图所示:
我们可以看到棋盘确实打印出来了,但全部是空格,没有我们看到的修饰分界线,因此为了分割出行和列,让玩家清楚的知道下棋要下载哪个坐标,我们要这样设计代码,可以把数据和分割行看为一组,如果最后一行假设他也有分割行,那就是一共3组,首先打印数据,再打印分割行,设计如下:
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]);
//分割行
printf("---|---|---\n");
}
}
然后运行如下:
我们可以看到在打印第三组数据的时候,最后的分割行打印多了,因此我们在打印分割行前加上一个条件:if(i < row-1)即可,再次运行如下:
棋盘就很完美的打印出来了,但是这一段代码是可以优化的,因为,假设有一天要把棋盘打印成10乘10的棋盘,那么我们把头文件的ROW和COL各改为10,效果会像下面这样只有10行3列:
原因是我们把代码写死了,不便于代码扩展,因此我们应该把代码写成如下:
void DisplayBoard(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++)
{
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");
}
}
效果如下图,10乘10棋盘格:
接下来就是设计下棋的程序了,我们知道每次下完棋都会打印记录电脑或者玩家的落子后的棋盘,因此我们开设Player_move和Computer_move两个函数,在这里我们先写玩家下棋的函数,设置一个while循环,玩家下完棋到电脑下,电脑下完再到玩家下,此处同样要在头文件先声明函数:
void Player_move(char board[ROW][COL], int row, int col);//玩家下棋
接下来先定义这个函数,这里要注意的就是,要告诉玩家输入的坐标是要在3行3列以内的,所以这里我们要设定一定的判断条件,让玩家输入合法的坐标,第二点就是,要加入一个判断棋盘某一格位置是否给占用的条件,如果给占用了,此处就不是空格,玩家不可下在此处,这里有很多同学可能会写成 if(board[x][y] == ' '),但是这样是不对的。对于玩家来说,行和列从1开始的,但是我们程序员是知道对于数组来说,行和列是从0开始的,所以这里设计输入的坐标要优化成[x-1][y-1]才行,代码设计如下:
void Player_move(char board[ROW][COL], int row, int col)//玩家下棋
{
printf("玩家下棋:>\n");
int x = 0;
int y = 0;
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");
}
}
}
设置好这个后,程序运行,效果如下图所示:
完成玩家下棋的代码后接下来就是电脑下棋了,整体思路就是电脑下棋的随即下的,看到棋盘哪里是空格就可以往哪里下,现在就开始定义Computer_move函数,同样是先声明:
void Computer_move(char board[ROW][COL], int row, int col);//电脑下棋
现在再开始定义,电脑下棋也是同样的生成横纵坐标,所以我们也是开设一个x和y变量然后初始化,我们知道rand函数是可以生成随机数的,但是我们这里需要电脑下的坐标范围要在0~2之间,所以我们让rand生成的随机数都模上对应的行和列,注意这里在使用rand函数之前要在主函数内调用srand,用srand来设置随机数的生成器,time函数的返回值作为我们的时间戳即可(具体相关知识可以参考我之前写的文章《浅谈用C语言实现猜数字游戏》),并在头文件加上生成随机数相关的头文件#include<time.h>和#include<stdlib.h>。
这里对应的坐标合法性就已经不用再判断了,生成随机数的范围一定是0~2了,不会越界,唯一要判断的就是电脑下的坐标是否给占用了。程序设计如下:
void Computer_move(char board[ROW][COL], int row, int col)//电脑下棋
{
int x = 0;
int y = 0;
printf("电脑下棋:>\n");
while(1)
{
x = rand() % ROW;//0~2
y = rand() % COL;//0~2
if(board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
此时运行程序,如下图所示:
可以明确的看到,电脑在执行和玩家下棋的动作,现在还有最后最重要的一部分就是判断玩家或者电脑的输赢,不然游戏会无止境的进行下去,所以这里分析一共有四种情况:
1.玩家赢得比赛
2.电脑赢得比赛
3.平局
4.游戏未结束,继续进行
这里写一个is_win函数,最终返回4种结果,我们假设返回4种字符对应这4种情况:
1.玩家赢得比赛 --- ' * '
2.电脑赢得比赛 --- ' # '
3.平局 --- ' Q '
4.游戏未结束,继续进行 --- ' C '
所以is_win函数返回的类型是char,我们这里的思路就是,三行或者三列,或者对角线相连则为游戏赢的一方,所以这里的is_win函数最本质的还是访问board函数,对应的ROW和COL也要进行传参,主函数里面就像下图这样设计:
void game()
{
//数据存储到一个字符的二维数组中,玩家下棋是'*',电脑下棋是'#'
char board[ROW][COL] = {0};//数组的内容应该全部是空格
InitBoard(board, ROW, COL);//初始化棋盘
DisplayBoard(board, ROW, COL);
//下棋
char ret = 0;
while(1)
{
Player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if(ret != 'c')
{
break;
}
Computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if(ret != 'c')
{
break;
}
}
if(ret == '*')
{
printf("玩家赢\n");
}
else if(ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
}
共分为4种情况,然后现在欠缺的就是is_win函数了,同样也是在头文件里面写上:
char is_win(char board[ROW][COL], int row, int col);
这里开始定义is_win函数,无非就是三行、三列或者对角线相连,我们现在先来写三行:
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[2][i] == board[i][2] && 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];
}
那么现在差的就是平局和继续游戏两种情况了,平局的话我们这里再创建一个is_full函数,也就是判断棋盘内有没有空格,如果还有空格证明游戏还没结束。
//判断平局(没有空格)
if(is_full(board, row, col))
{
return 'Q';
}
这里就不用在game.h里面声明了,is_full函数只是给is_win用一下即可,所以函数在is_win之前定义就好,函数设计如下图:
int is_full(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;
}
最后继续游戏的这种情况就直接return’C'即可,到目前为止,整个游戏的代码接口已经全部完成,我们运行代码试试:
我们可以看到,电脑就算在我们玩家让着他的情况下,它都很难取得获胜,所以这是一个可以优化电脑操作的地方,电脑聪不聪明是取决于程序员,所以想要让这个游戏更有趣就要让电脑拥有分析棋盘的能力,但是总体的三子棋逻辑是已经实现了,实现了电脑与人的交互,因此怎么优化这个代码以后也是一个值得探讨的问题。
这次的三子棋整体代码再分三大部分附上。
1.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_move(char board[ROW][COL], int row, int col);//玩家下棋
void computer_move(char board[ROW][COL], int row, int col);//电脑下棋
//判断输赢的代码
//玩家赢 --- '*'
//电脑赢--- '#'
//平局--- 'Q'
//继续--- 'C'
char is_win(char board[ROW][COL], int row, int col);
2.test.c 测试游戏的逻辑
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu()
{
printf("************************\n");
printf("***** 1. play ******\n");
printf("***** 0. exit ******\n");
printf("************************\n");
}
void game()
{
//数据存储到一个字符的二维数组中,玩家下棋是'*',电脑下棋是'#',
char board[ROW][COL] = { 0 };//数组的内容应该是全部空格
InitBoard(board, ROW, COL);//初始化棋牌
//打印棋盘
DisplayBoard(board, ROW, COL);
//下棋
char ret = 0;
while (1)
{
player_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
computer_move(board, ROW, COL);
DisplayBoard(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("玩家赢\n");
}
else if (ret == '#')
{
printf("电脑赢\n");
}
else
{
printf("平局\n");
}
}
void test()
{
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);
}
int main()
{
test();
return 0;
}
3.game.c 游戏相关函数的实现
#define _CRT_SECURE_NO_WARNINGS
#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;
// int j = 0;
// for (i = 0; i < row; i++)
// {
// //数据
// printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
// //分割行
// printf("---|---|---\n");
// }
//}
void DisplayBoard(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++)
{
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 player_move(char board[ROW][COL], int row, int col)//玩家下棋
{
printf("玩家下棋:>\n");
int x = 0;
int y = 0;
while(1)
{
scanf_s("%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");
}
}
}
void computer_move(char board[ROW][COL], int row, int col)//电脑下棋
{
int x = 0;
int y = 0;
printf("电脑下棋:>\n");
while(1)
{
x = rand() % ROW;//0~2
y = rand() % COL;//0~2
if(board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
int is_full(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 is_win(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[2][i] == board[i][2] && 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];
}
//判断平局(没有空格)
if(1 == is_full(board, row, col))
{
return 'Q';
}
//继续游戏
return 'C';
}