当我们熟练掌握了选择结构,循环结构和函数调用等基础知识的使用之后,就可以将这些知识结合之后写出一个属于我们自己的小游戏——三子棋。
当我们初步认识到函数的知识时,可能会觉得函数一般只在当前的源文件调用而已,但是在一般的工程编写当中,一般是将工程分模块进行编写,然后会将单独将函数放在一个函数模块,然后把测试的源文件放在另一个测试模块。
这样做的好处时,可以将工作分为几个部分进行编写,可以使得代码看起来更加清晰明了,而不会出现所有代码都堆积在一个文件内过于繁琐复杂,再之当出现bug时也容易进行调试发现错误后改正。而且我们编写函数时,需创建两个文件,一个为头文件,里面放置相关函数的声明。另一个为源文件,里面放置相关函数的内容编写。由于三子棋游戏的代码行数较多,所以我们也是分模块进行编写。
我们就按照上图所示,创建以上三个文件,分为两个模块。
game.h中就放置游戏相关函数的声明,game.c中就放置函数内容的编写。test.c中就进行游戏的测试运行内容。
为了使得 test.c 文件中的 main 函数内部显得简洁明了,我们只写明调用了一个测试函数,然后其他内容在函数内部编写即可。
//test.c
int main()
{
test();
return 0;
}
然后就是编写 test 内部的内容了:
首先我们需要制作一个游戏入口,就相当于一个菜单,让玩家进行选择是否进行游戏。所以我们要先在 test 内调用一个 menu 函数,然后编写menu函数。
//test.c
void menu(void)
{
printf("***********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("***********************\n");
}
这样,我们就在调用测试函数的时候,会首先打印出一个菜单供玩家进行选择,选择1,即是进行游戏,选择0,即是退出系统。注意这个函数我们是不需要返回数据的,所以返回类型为 void ,而且我们也不需要进行传参,所以括号内形参也应为 void。(虽然不写void不会影响该函数的运行,但我们会发现这样编写之后,在调用时仍旧可以进行传参,即使这个函数调用时不会出错,但是我们没办法以后编写其他函数时不会出错,为确保安全我们应该在无需传参的函数的形参列表写上void,这样调用函数时如果硬要进行传参,编译器就会报错。我们应当养成这种良好的代码风格。)
为了使得玩家能够多次进行游戏,我们应当将菜单放置于循环结构中,供玩家多次选择。此处我们应选择 do...while 循环结构,因为我们需要在玩家进行游戏之前就打印一次菜单,此次打印不须进行循环条件的判断就要打印出来,所以 do...while 结构正好满足我们的需求。
当玩家选择之后,我们应该对玩家输入的内容进行判断之后,根据不同的输入内容选择进行不同的操作:输入1时,进入游戏(游戏内容用 game 函数进行包装,整体会显得更加简洁);输入0时退出游戏系统。输入其他数字,就会提示输入错误,需要重新输入。所以应当使用 switch 分支语句进行选择。这样,我们就完成了测试函数内容的编写。
//test.c
void test(void)
{
int i = 0;
do
{
menu();
printf("请选择->");
scanf("%d", &i);
switch (i)
{
case 1:
game();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (i);
}
我们使用输入的数字作为循环条件的判断,因为C语言中,0是错误,非0都是正确,所以只有当输入0时才会退出循环,其他情况下都会进行多次输入,所以使用输入的数字作为条件显得很巧妙很简洁。
然后,我们就要进行重头戏了,对游戏内容进行编写:
通过这张三子棋图片,我们可以发现,这个棋盘有着九个格子,三行三列,结构就正如我们所学的二维数组。所以我们就可以定义一个三行三列的二维数组来接收这些数据,有了数据的记录,我们之后才能够根据数据来进行输赢的判断。所以我们要先在 game 函数内定义好一个二维数组board。
为了后续编写完程序之后,方便改造成更多行更多列的棋盘以增加游戏难度,我们将行数列数用 #define 定义为常量,便可以一次性修改所有使用到行数列数之处。(因为代码中使用到行数列数的地方过多,若直接修改会使工作量大大增加)
在接收数据之前,我们应该先初始化好这个数组。三子棋表格一开始时是什么都没有,所以我们初始化时,将数组内的元素都初始化为空格即可。为此我们要调用一个初始化函数。这个函数由于在game函数的内部,所以我们应该将其声明放在game.h中,将定义放在game.c中,然后我们在test.c 和 game.c 中包含这个头文件即可。我们也可以将行数列数的定义以及输入输出函数的头文件放置于 game.h 中,这样在已经包含了 game.h 的两个文件中就不用再另外声明,以减少代码量。而在函数传参时,由于我们是要对二维数组进行初始化,所以形参应该为二维数组类型,同时,我们要将数组的行和列一同作为参数传给函数。
因为数组名作为参数传递给函数时,是作为指针传递,传递的是数组首元素的地址,所以函数接收数组时实质也是接收到一个指针而不是整个数组,因此我们无法用 sizeof 操作符进行对数组元素个数的计算(此时在函数内使用 size(数组名) 求出的是指针的内存大小而不是整个数组的内存大小),所以只能连同数组行列的个数一并作为参数传给函数。
//game.h
#include
#define ROW 3
#define COL 3
void init_board(char board[ROW][COL], int row, int col);//初始化棋盘
//game.c
#include "game.h"
void init_board(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] = ' ';
}
}
}
//test.c
#include "game.h"
void game(void)
{
char board[ROW][COL] = { 0 };
init_board(board, ROW, COL);
}
紧接着,我们就应该将棋盘打印出来了。但由于我们初始化时将数组元素都设置为空格,所以如果直接打印的话,可想而知,屏幕上会显示一处空白,好像打印了,但又好像什么也没打印。而且,当我们回过头再去看三子棋的图片时,我们会发现行与行之间,列与列之间,都有着实线隔开,所以即便我们的数组元素不是空格,打印出来的棋盘也不是很相似,因为没有设置实线隔开行和列,因此为了更加美观,让玩家有更好的体验,我们需要对打印出来的棋盘进行一番设计。同样的,我们定义好一个打印函数,将打印的内容包含在函数内部。
棋盘总共有三行三列,所以我们使用嵌套的 for 循环 进行设计。外层循环控制行的打印,内层循环控制一行之中的每一列的打印。
我们可以使用三个连续的 “-” 代替行之间的分隔线,然后使用 “|” 代替列之间的分隔线。
然后问题来了,我们应该如何设置循环内容?通过观察后可知,如果我们把每一行连同其分隔线看做一个整体,那么整体就只需要循环三次即可,也就是我们的外层循环只需循环三次。但是,分隔线只有前两行有,最后一行没有,该怎么处理?我们只需在循环内容再加上一个选择语句,使得分隔线内容循环两次,便可以控制只在前两行打印分隔线。所以,我们使用正好为 3 的行数作为外层循环的循环判断条件,使用行数 - 1 作为分隔线的选择判断条件。(不直接用 3 而是使用行数的变量,同样是为了方便后期改造成为更多行更多列的棋盘)
然后就是对于每一行的内容的编写了,也就是我们的内层循环。我们同理可以将分隔线连同每一列看成一个整体进行打印,同样使用选择语句解决只打印前两列的分隔线的问题。(注意这里,行之间判断是否打印行分割线时,其判断内部还需再判断是否打印列分隔线)。
为了打印出来的每一个小格子更加美观,我们打印数组的每个元素的左右都加上空格,使得元素内容处于每一小格的居中位置,左右都有空隙而显得更加美观。
//game.c
void print_board(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]);
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");
}
}
}
将棋盘打印好之后,游戏就完成了开始阶段,接下来就是设计下棋阶段。为了玩家获利,我们让玩家先下棋,那么就得先设计如何让玩家下棋。
我们采取让玩家输入想要操作的棋盘坐标,然后我们根据输入内容对棋盘相对应位置进行操作。我们将玩家落子后的位置对应的数组元素用 “ * ”代替。由于玩家不知道数组下标是从 0 开始的而是默认行和列的起始坐标是 1 ,所以我们不能将玩家输入的数据直接作为数组下标,应该先将数据 - 1 之后再进行操作。我们还应当先对数据进行简单的判断,因为只有当该位置没有落子才能被玩家选择下棋,所以应该判断该位置的元素是否是空格从而判断该位置是否已经被落子。除此之外,我们还应对非法操作或者误操作进行避免,如果玩家输入了一个超出棋盘范围内的坐标数据,那就得提醒其重新输入。为了保证玩家能够在输入错误后重新输入,所以整体应该放置于一个循环结构中,直至玩家成功下棋后才跳出循环。我们使用一个非 0 常数作为循环判断条件,保证循环在不跳出的情况下一直循环下去。综合上述,我们将这些步骤包含在一个函数内部。
//game.c
void human_play(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("玩家下棋->");
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");
}
}
}
接着就轮到了电脑下棋了,我们使用“ # ”表示电脑下棋的位置。电脑同样使用坐标进行下棋,我们使用随机数的方式产生坐标,来代表电脑下棋。所以我们要调用两次 rand 函数产生两个随机数,然后令结果分别 % 行数和列数就可以保证随机产生的横坐标处于 0到行数 - 1之间,纵坐标处于 0 到列数 - 1之间,由于电脑知道数组下标从 0 开始,所以我们将数据直接作为数组下标进行操作。同样要对数据进行简单的判断,但由于产生的数据肯定是位于棋盘范围内的,所以只需判断相应位置的元素是不是空格即可。
调用 rand 函数前必须先调用 srand 函数生成随机数的产生起点,否则每次程序生成的随机数都是固定且有序的。srand 函数只需调用一次,所以我们在同样只需调用一次的 test 函数内调用即可。而 srand 函数也需要初始化起点,为了达到每次初始化的起点都不同的效果,我们srand 函数内使用 time 函数生成一个时间戳。time 函数需要传一个指针参数,而在此我们无需使用到该指针,所以直接传一个 NULL 空指针即可。time 函数的返回值是 time_t 整型,而 srand 函数被调用时,所需要传递的参数类型是 unsigned int 类型,所以还应对 time 函数的返回值进行强制转换为 unsigned int 类型。
//game.c
void robot_play(char board[ROW][COL], int row, int col)
{
printf("电脑下棋->\n");
Sleep(1000);
while (1)
{
int x = rand() % row;
int y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
为了模拟实际游戏时的真实性,我们在电脑下棋时,先调用一次 Sleep 函数,传参为1000。(此处单位时毫秒,1000毫秒即为1秒,作用时模拟电脑思考一秒后进行下棋)。
玩家和电脑各自下棋时,我们都需将双方下棋后的棋盘打印出来,以供玩家观察,所以双方各自下棋后都应调用一次打印函数。
当一方下棋过后,我们都应该对局势做出判断,判断是否有一方获胜或者产生平局。所以我们定义一个 is_win 函数对棋盘内容进行判断。
只要有一行或一列棋子相同,或者任意一条对角线棋子相同,即为一方胜利。为了后期棋盘行数列数变化后仍然适用,在此的做法是:对每一种获胜方法单独判断,例如任意一行棋子相同的获胜方法中,设置两个计数变量,判断某一行中的每一个元素,如果是“ * ”则其中一个计数变量 +1,如果是“ # ”则另一个计数变量 +1,如果是空格则不作变化。到最后,如果有一行棋子相同,则对应的计数变量应等于列数,即最后对计数变量判断其是否等于列数即可判断是否有一方获胜。所以只需对每一行都进行计数,且在每一行判断开始前计数变量赋值为 0,防止上一行计数后的结果遗留到下一行的判断中。如果有一行的计数中,有一个计数变量已经等于了列数,则跳出循环不再判断。(因为此时已经有人获胜,无需再判断)同理,任意一列棋子相同的获胜方法中也只需判断是否有一个计数变量等于其行数,就可以判断是否有一方获胜。任意一条对角线棋子相同的获胜方法判断也同理,只不过两条对角线还需要分开判断。最后只要任意一种方法中,有任意一个玩家对应的计数变量或者任意一个电脑对应的计数变量符合条件即为一方获胜。当判断有一方获胜后,我们需要返回一个值供 game 函数判断打印获胜者是谁的语句。我们在此规定返回“ * ”即为玩家胜利,返回 “ # ”即为电脑获胜。
此处作者的获胜判断方法有些许呆板,因为暂时想不出其他更灵活的方法进行代替,导致代码量过长,如果有其他更好的方法进行代替则更优。
除了判断获胜,我们还需判断是否产生平局,即为判断棋盘上是否所有格子都已有棋子,如果棋盘上已经没有空格,就是已经没有可以落子的地了,便是平局。我们使用另一个函数包含判断是否为平局的内容。(这个函数我们只想要在 is_win 函数内调用,所以不用在 game.h 声明,这样其他源文件就无法调用此函数)如果还有空格子而且没有一方获胜,则双方继续下棋。我们令这两种情况都返回一个数值供我们返回到 is_win 中进行判断即可。
与前面获胜的判断一样,上述这两种情况返回到 is_win 函数之后,也需要返回值供 game 函数判断。我们规定返回“ Q ”则为平局,如果返回“ C ”则继续下棋。
//game.c
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;
int j = 0;
int count1 = 0;
int count2 = 0;
int count3 = 0;
int count4 = 0;
int count5 = 0;
int count6 = 0;
int count7 = 0;
int count8 = 0;
//一行相同为胜利
for (i = 0; i < row; i++)
{
count1 = 0;
count2 = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == '*')
count1++;
else if (board[i][j] == '#')
count2++;
}
if (count1 == row || count2 == row)
break;
}
//正对角线相同为胜利
for (i = 0; i < row; i++)
{
if (board[i][i] == '*')
{
count3++;
}
else if (board[i][i] == '#')
{
count4++;
}
}
//反对角线相同为胜利
for (i = 0; i < row; i++)
{
if (board[i][row-1-i] == '*')
{
count5++;
}
else if (board[i][row-1-i] == '#')
{
count6++;
}
}
//一列相同胜利
for (j = 0; j < col; j++)
{
count7 = 0;
count8 = 0;
for (i = 0; i < row; i++)
{
if (board[i][j] == '*')
count7++;
else if (board[i][j] == '#')
count8++;
}
if (count7 == col || count8 == col)
break;
}
if (count1 == col || count3 == row || count5 == row || count7 == row)
{
return '*';
}
else if (count2 == col || count4 == row || count6 == row || count8 == row)
{
return '#';
}
else if (is_full(board, row, col) == 1)
{
return 'Q';
}
else if (is_full(board, row, col) == 0)
{
return 'C';
}
}
在双方各自下棋后,我们都需判断一次棋盘的局势,所以在双方下棋后都需调用一次 is_win 函数。
最后在 game 函数中,如果 is_win 返回值是“ C ”,则说明此时还存在空格,还需继续下棋,所以整个下棋包括判断局势的过程应该放置于循环结构中,只有当 is_win 返回值不是“ C ”时,才跳出循环对结果进行判断是否有人获胜或者平局。
至此,我们对整个游戏的设置就已经完成了,我们可以来看一下成效。
最后,我们对整体思路进行一个概述:首先,我们要打印出一个入口菜单给玩家选择是否开始游戏,然后就是初始化我们想要的棋盘并打印出来,接着我们要对下棋的过程进行设计,分为玩家下棋和电脑下棋两部分,下棋之后我们要对棋盘上的局势进行判断是否进行或者结果已经产生。 这便是整体思路,然后对每一步进行细化分析,编写内容即可。
最后附上完整代码:
//game.h
#include
#include
#include
#include
#define ROW 3
#define COL 3
//初始化棋盘
void init_board(char board[ROW][COL], int row, int col);
//打印棋盘
void print_board(char board[ROW][COL], int row, int col);
//玩家下棋
void human_play(char board[ROW][COL], int row, int col);
//电脑下棋
void robot_play(char board[ROW][COL], int row, int col);
//判断胜利方法
char is_win(char board[ROW][COL], int row, int col);
//game.c
#include "game.h"
void init_board(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 print_board(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]);
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 human_play(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("玩家下棋->");
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 robot_play(char board[ROW][COL], int row, int col)
{
printf("电脑下棋->\n");
Sleep(1000);
while (1)
{
int x = rand() % row;
int y = rand() % col;
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;
int j = 0;
int count1 = 0;
int count2 = 0;
int count3 = 0;
int count4 = 0;
int count5 = 0;
int count6 = 0;
int count7 = 0;
int count8 = 0;
//一行相同为胜利
for (i = 0; i < row; i++)
{
count1 = 0;
count2 = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == '*')
count1++;
else if (board[i][j] == '#')
count2++;
}
if (count1 == row || count2 == row)
break;
}
//正对角线相同为胜利
for (i = 0; i < row; i++)
{
if (board[i][i] == '*')
{
count3++;
}
else if (board[i][i] == '#')
{
count4++;
}
}
//反对角线相同为胜利
for (i = 0; i < row; i++)
{
if (board[i][row-1-i] == '*')
{
count5++;
}
else if (board[i][row-1-i] == '#')
{
count6++;
}
}
//一列相同胜利
for (j = 0; j < col; j++)
{
count7 = 0;
count8 = 0;
for (i = 0; i < row; i++)
{
if (board[i][j] == '*')
count7++;
else if (board[i][j] == '#')
count8++;
}
if (count7 == col || count8 == col)
break;
}
if (count1 == col || count3 == row || count5 == row || count7 == row)
{
return '*';
}
else if (count2 == col || count4 == row || count6 == row || count8 == row)
{
return '#';
}
else if (is_full(board, row, col) == 1)
{
return 'Q';
}
else if (is_full(board, row, col) == 0)
{
return 'C';
}
}
//test.c
#include "game.h"
void game(void)
{
int i = 0;
char ret = 0;
char board[ROW][COL] = { 0 };
init_board(board, ROW, COL);
print_board(board, ROW, COL);
while (1)
{
human_play(board, ROW, COL);
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
robot_play(board, ROW, COL);
print_board(board, ROW, COL);
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if (ret == '*')
{
printf("玩家胜利!!!\n");
}
else if (ret == '#')
{
printf("电脑胜利~~~\n");
}
else if (ret == 'Q')
{
printf("双方打平\n");
}
}
void menu(void)
{
printf("***********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("***********************\n");
}
void test(void)
{
srand((unsigned int)time(NULL));
int i = 0;
do
{
menu();
printf("请选择->");
scanf("%d", &i);
switch (i)
{
case 1:
game();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (i);
}
int main()
{
test();
return 0;
}
以上就是我对设计三子棋游戏的代码内容的理解,如果有错误或者不足之处,劳烦各位指出,我一定及时做出修改。