前面两篇博客咱们分别就C语言中的随机数函数及#define给大家进行了介绍,今天我们将用这些知识及C语言数组的相关知识给大家介绍如何实现一个童年小游戏游戏“三子棋”。游戏规则非常的简单:3*3棋盘,双人对弈,率先三子连珠的玩家即可获胜。
一、游戏界面的设计
如何去设计一个游戏界面呢?这里给大家介绍一个简单又实用的游戏界面吧,仅仅使用了printf函数来实现的游戏界面。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void menu()
{
printf("**************************\n");
printf("******** 1.PLAY **********\n");
printf("******** 0.EXIT *********"\n");
printf("**************************\n");
}
int main()
{
int input;
printf("三子棋\n");
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新选择:>\n");
break;
}
} while (input);
}
这里和大家简要说明一下这个游戏界面程序的设计思路吧!
1.首先是打印菜单的函数void menu(),菜单信息告诉玩家选择1开始游戏,选择0结束游戏。
2.在主函数的设计上,我们通过switch的分支语句以及do while的循环语句来实现我们游戏玩完之后还能继续玩的逻辑。
3.其中的int类型的变量input,是我们等待玩家来输入的变量。通过input的值来控制switch的分支。
4.这里我们可以直接将变量input的值作为循环的条件,这是没有问题的,大家可以去分析看一下。
5.这里面的game()函数是我们将在后面实现的“三子棋”功能的函数。
二、棋盘与初始化棋盘
棋盘怎么设计呢?棋盘它是一个平面图形,有行有列,我们可以通过一个二维数组来设计我们的数组。同时考虑到以下两个方面:
- 我们的不同的棋子需要用不同的图案加以区分,所以我们需要一个char类型的二维数组。
- 同时考虑到三子棋的棋盘规格是3*3的大小,同时你又希望你以后有不同需求了,能够比较方便地去修改棋盘的大小,所以在这里我们使用对象宏对棋盘的大小进行规格的设定。即通过#define来创建ROW和COL两个对象宏,并将它们的大小设计为3。
棋盘设计好的同时我们还要对它进行初始化,即我们需要将棋盘的初始图案设计为空格,也就是将该二维数组所有元素赋值为空格。所以初始化棋盘部分的代码可以写作:
#define ROW 3
#define COL 3
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] = ' ';
}
}
}
三、如何去展示棋盘
先来看一下效果吧!
好的,那么如何去设计这么一个棋盘呢?我们去观察一下这个棋盘,其实不难发现它的三个特点:
其一,它的第一行是:空格+数据+空格,然后打印一个“|”,其中“|”比“空格+数据+空格”少一个;
其二,它的第二行是:---,然后再打印一个“|”的形式,其中的“|”比“---”少打印一个;
其三,我们不妨将“其一”的这种格式称之为“模式一”,“其二”的这种模式称之为“模式二”。不难发现模式二的打印比模式一的少组,且模式一具体打印多少和我们的棋盘有多少行有关系。
基于以上几点讨论,我们可以基本可以通过for循环语句与if判断语句实现来展示棋盘的这么一个功能,值得注意的是,需要在打印完一个模式之后要记得换行哦:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i, j;
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");
}
}
}
四、玩家下棋功能的实现
由于我们已经将二维数组board中所有元素初始化为空格了,所以你才能看到空空如也的棋盘,给大家的感觉就是还没有开始下棋。那现在我们来设计一个玩家下棋的功能吧!首先我们在这里不妨规定一下,玩家的棋子为“*”的字符图案,那么要设计出玩家下棋的逻辑就太简单了,只需要让我们玩家去输入坐标,然后再给相应位置字符数组元素赋值为“*”即可。
但是还有些方面需要我们去注意:
- 我们玩游戏的人不一定都是程序员,他们看到棋盘的第一行第一列的位置,自然而然会去输入坐标(1,1),而对于有一定C语言编程基础的都清楚,二维数组的第一个元素是board[0][0]。因此我们需要让坐标输入贴合大众的一般认识,这是我们需要考虑的。
- 同时有时候啊,有一些玩家会故意输入一个坐标,这个坐标怎么了呢,这个坐标可能是一个已经被占据的坐标,也可能这个坐标本身就不合法,它越界了。这个时候吖,我们就需要提示玩家,并且让玩家去重新输入一个坐标,直到输入坐标合法为止,就是这样!
基于以上的考虑,我们设计出了玩家下棋功能函数的实现:
void PlayerMove(char board[ROW][COL], int row, int col)
{
printf("玩家下棋\n");
printf("请输入下棋的坐标:>\n");
int x, y;
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");
}
}
五、电脑随机下棋功能的实现
这里我们使用随机数函数来生成随机坐标,从而实现电脑的随机下棋功能。这里我们不妨规定电脑下棋的棋子为“#”。
同时考虑以下两个方面
- 由于是3*3的棋盘,所以对于二维数组它的行和列最小为值为0,最大值为2,我们只需要对生成的随机数对3取余即可。
- 我们的电脑不会越界,但是考虑到生成坐标的位置已经有棋子了,所以我们需要使用if语句,让电脑在没有棋子的坐标(即该位置是空格)去下棋。
综上所言,我们可以如此来设计我们电脑下棋的程序:
void ComeputerMove(char board[ROW][COL], int row, int col)
{
int x, y;
printf("电脑下棋\n");
while (1)
{
x = rand() % ROW;
y = rand() % COL;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
六、判断输赢的程序设计
现在我们的代码已经可以实现人机正常,可是却缺少游戏最核心的部分,判断输赢。如何去设计一个程序让电脑给我们判断输赢呢?在思考这个问题我们先来思考下棋之后会有哪些状态存在呢?
- 状态一:玩家胜利;
- 状态二:电脑胜利;
- 状态三:游戏平局;
- 状态四:游戏继续。
同时在判断上面:
1.玩家胜利和电脑胜利的条件都是需要三子连珠,我们需要从横,竖,对角线三个维度去进行检查;
2.游戏平局就是当玩家和电脑没有一个实现三子连珠,同时棋盘已满(即二维数组元素均不为空格);
3.游戏继续就是既没有玩家或者电脑的胜利,同时游戏也没有平局,这个时候游戏继续进行。
4.游戏继续的另一种说法就是当玩家和电脑没有一个实现三子连珠,同时棋盘未满(即二维数组元素还有空格元素);
这个无伤大雅,这是两种不同的思考问题的角度,不同的角度去思考问题,会带来不同解决问题的方法。我们在这里以前面那个问题角度去设计我们的程序。由此可以设计出以下判断输赢的程序代码(我们这里单独设计了一个函数IsFull,作用是判断棋盘是否已满):
#include<stdbool.h>
bool IsFull(char board[ROW][COL], int row, int col)
{
int i, j;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return false;
}
}
return true;
}
char JudgeGame(char board[ROW][COL], int row, int col)
{
int i, j;
//判断行列是否相等
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
return board[i][0];
else if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
return board[0][i];
}
//判断对角线是否相等
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ')
return board[1][1];
else if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2] != ' ')
return board[1][1];
//判断平局
else if (IsFull(board, ROW, COL))
return 'P';
//以上条件都不满足,返回'C',游戏继续
else return 'C';
}
这里的IsFull函数使用了,所以需要用到stdbool.h的头文件。同时我们的判断输赢的函数JudgeGame设计为char类型,让它返回不同的字符,由此来反映出游戏的各种不同状态。如谁赢我们就直接返回了他所使用的棋子的图案,平局我们返回了字符P(取自汉字“平”的拼音首字母),游戏继续返回C(取自英文单词continue的首字母)。这些单词会在我们设计game函数时用到。
七、整合功能,设计game函数
给大家介绍了游戏中相关功能的实现,现在我们需要做的就是将这些功能整合到game函数中,让我们的游戏可以真正的跑起来!
简单介绍一下整合思路:
- 前期工作:定义一个二维数组,并对它进行初始化,之后展示出来让我们的用户可以看到;
- 下棋流程介绍:你下棋----展示棋盘----判读一次输赢----电脑下棋----展示棋盘----判断一次输赢。这个过程很明显不止被执行一遍,所以我们需要一个循环,当游戏不再继续时,我们跳出此循环。
- 宣布游戏结果:根据JudgeGame函数从循环中带出的讯息来给出游戏最终结果,这需要我们的if,else的分支语句。
由此game函数也就设计好了哦!
void game()
{
char board[ROW][COL] = { 0 };
printf("电脑目前有点笨\n");
//初始化棋盘&展示棋盘
InitBoard(board, ROW, COL);
DisplayBoard(board, ROW, COL);
while (1)
{
//玩家下棋&展示棋盘&判断输赢
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
if (JudgeGame(board, ROW, COL) != 'C')
{
break;
}
//电脑下棋&展示棋盘&判断输赢
ComeputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
if (JudgeGame(board, ROW, COL) != 'C')
{
break;
}
}
if (JudgeGame(board, ROW, COL) == '*')
{
printf("玩家胜利\n");
}
else if (JudgeGame(board, ROW, COL) == '#')
{
printf("电脑胜利\n");
}
else
{
printf("游戏平局\n");
}
}
八、结语(送给大家的一些话以及关于这个代码的一些修改意见):
我们一步一步做下来,大家发现了,其实像这么一些游戏其实它用到的知识都非常的简单和基础。所以大家在学习C语言的过程中,打好比较扎实的基础是非常有必要的。程序设计的思路和思想是比程序代码本身更加重要和有意思的事情,各种各样的不同功能的实现离不开大家的讨论和思考。只要思想不偏差,但只要我们的目的是一致的,不同的代码也是可以实现相同的功能。
同时在学习过程当中也要对自己有更多的自信心,可以勇敢地去尝试一些看似目前无法完成的项目。C语言的基础知识很有限,但是你学扎实之后,一些项目自己也是可以去做的!同时我们也需要认识到目前的一些不足:
- 首先我们的界面是不是可以设计更好看一些,这可能需要新的工具的帮助;
- 我们的判断输赢的部分其实设计得比较死板,大家没有,它只适合用来判断三子棋输赢,你以后想玩五子棋,你可能还需要对这一部分进行修改,有没有更加好的算法可以使这一部分功能更加灵活呢?
- 最后一个问题,诚如我在代码中所言,这是一台比较笨的电脑,它下棋非常随机。以人类智慧认真和它打,它是没有胜算的,这一定程度上降低了我们游戏的趣味性。有没有什么设计方法让我们的电脑更聪明一些呢?
笔者的篇幅及能力有限,这些问题留给读者朋友自行思考吧!有新灵感的朋友们,也欢迎来和作者讨论!
然后最后想说的就是实际我们在写这么一个比较大型的项目的时候,往往会将它们分装为很多文件,一般会将一些头文件以及函数的声明放在一个自己定义的头文件当中。函数功能的实现一般单独写一个.cpp文件进行分装。主函数放在另外一个新的.cpp文件当中。这样做的好处就是有利于后期我们对代码的维护,这是给大家的建议。
作者将完整可执行代码放在文末,需要的小伙伴可以自取(温馨提示:日常的学习和工作中,还是要养成分装代码的好习惯,不要学习作者哦!)。
九、完整可执行的代码
#define _CRT_SECURE_NO_WARNINGS 1
#define ROW 3
#define COL 3
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include<stdbool.h>
void menu()
{
printf("**************************\n");
printf("******** 1.PLAY **********\n");
printf("******** 0.EXIT **********\n");
printf("**************************\n");
}
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] = ' ';
}
}
}
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i, j;
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 PlayerMove(char board[ROW][COL], int row, int col)
{
printf("玩家下棋\n");
printf("请输入下棋的坐标:>\n");
int x, y;
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");
}
}
void ComeputerMove(char board[ROW][COL], int row, int col)
{
int x, y;
printf("电脑下棋\n");
while (1)
{
x = rand() % ROW;
y = rand() % COL;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
bool IsFull(char board[ROW][COL], int row, int col)
{
int i, j;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return false;
}
}
return true;
}
char JudgeGame(char board[ROW][COL], int row, int col)
{
int i, j;
//判断行列是否相等
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
return board[i][0];
else if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
return board[0][i];
}
//判断对角线是否相等
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ')
return board[1][1];
else if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2] != ' ')
return board[1][1];
//判断平局
else if (IsFull(board, ROW, COL))
return 'P';
//以上条件都不满足,返回'C',游戏继续
else return 'C';
}
void game()
{
char board[ROW][COL] = { 0 };
printf("电脑目前有点笨\n");
//初始化棋盘&展示棋盘
InitBoard(board, ROW, COL);
DisplayBoard(board, ROW, COL);
while (1)
{
//玩家下棋&展示棋盘&判断输赢
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
if (JudgeGame(board, ROW, COL) != 'C')
{
break;
}
//电脑下棋&展示棋盘&判断输赢
ComeputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
if (JudgeGame(board, ROW, COL) != 'C')
{
break;
}
}
if (JudgeGame(board, ROW, COL) == '*')
{
printf("玩家胜利\n");
}
else if (JudgeGame(board, ROW, COL) == '#')
{
printf("电脑胜利\n");
}
else
{
printf("游戏平局\n");
}
}
int main()
{
int input;
printf("三子棋\n");
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入:>\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新选择:>\n");
break;
}
} while (input);
}