前言:感谢各位朋友的捧场,相信大家刚开始学习C语言时都有想过能自己写一些简单的小游戏,比如贪吃蛇、扫雷之类的(先给自己立个flag,日后把这些能用C简单实现的小游戏都补上)。这里给大家分享的是三子棋游戏的简单实现(即以实现游戏整体流程为主)
三子棋是一种民间传统游戏,又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战,双方依次在9宫格棋盘上摆放棋子,率先将自己的三个棋子走成一条线就视为胜利,而对方就算输了,但是三子棋在很多时候会出现和棋的局面。 ——百度百科
一、准备工作
虽说是简单小游戏,但怎么也算是一整个工程了,那么我们的代码就应该符合完整、有序建立一个工程的需求,不能随意取变量或函数名(平时写代码最好也不要有这个不好的习惯),再来就是要分文件进行编写,感受模块化开发
1. 文件分类:
总共可分为三个文件:
(1)game.h文件
- 此文件用来放置游戏实现的函数的声明:
如下图:
(2)game.c文件
- 此文件用来编写游戏实现的函数的定义
如下图例:
(3)test.c文件
- 用于对游戏功能进行测试
如下图例:
接下来的内容会向大家介绍如何逐步完成每个文件的编写
2. #define 定义标识符常量的设置
如下图:
这个我们也放到头文件中,供以“全局”使用
- 问:自定义常量有什么好处呢?
实现了代码风格的统一性,及游戏功能的快速更改。虽现在介绍的是三子棋,但若是想玩四字棋、五子棋,只需将自定义常量的数值进行更改即可,省去了逐行进行检查再进行相应修改的过程,方便快捷。
3. 头文件包含
我们把函数声明放在头文件中,那么在 test.c 文件中进行测试时就需要包含这个头文件,即:#include"game.h"
。我们在测试过程中肯定也会用到一些库函数,那我们不妨将使用库函数需要的头文包含一起写在我们自己的头文件 game.h 中,如下图:
这样在测试的时候无论是使用自定义函数还是库函数只需进行#include"game.h"
一个头文件的包含即可
二、编写过程
这一部分主要分享和介绍整个游戏的编写思路与具体编写过程。对于个人的编写,思路还是很重要的,毕竟我们一开始肯定不能把所有功能都想到并直接在game.c文件中全都实现,比较好的做法就是有一个大体的思路,然后写一部分测试一部分,直到完成整个游戏。
1. 简单主界面的建立
游戏一般一开始都会展现一个可供选择的主界面,我们这里也简单实现一下:
在 test.c 文件中定义一个test()函数放在主函数中用于测试:
int main()
{
test();
return 0;
}
接下我们就对 test() 函数进行编写。
- 首先就是实现通过输入来选择是否进行游戏,输入1进行游戏,执行game()函数;输入0,退出程序输入其他数字时提示输入错误,然后重新输入。
如上需求我们可通过do while和switch语句来实现,代码如下:
void test()
{
int input = 0;
do
{
menu();//打印菜单
printf("请输入进行选择:\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();//进行游戏
case 0:
break;
default:
printf("输入错误,请重新输入:\n");
}
} while (input);
}
解释:do while语句可先执行一次循环体的内容,若输入1则执行case 1中的game()函数;若输入0则执行case 0跳出switch语句,此时将输入作为do while的循环判断条件,循环也随之结束。由此就实现了选择是否进行游戏的功能
- 至于主界面的样式,用 “ * ”号等符号的排列整齐即可,代码如下:
void menu()
{
printf("***********************************\n");
printf("************ 1.Play *************\n");
printf("************ 0.Exit *************\n");
printf("***********************************\n");
}
- 对于打印菜单的menu()函数与进行游戏的game()函数直接服务于测试环节,故置于 test.c 文件中,所谓 game.c 中放置游戏实现主要是指游戏中的各个板块,如打印棋盘、判断输赢等,而game() 函数的作用就在于把它们“组装、连接”起来。
那么下面我们继续按照思路来介绍 game() 函数及其内部相关游戏实现的函数的编写
2. 棋盘的初始化
- 既然是三子棋,那么我们自然就需要一个3×3的二维数组,结合上述的定义的标识符常量,创建语句就可写成:
char board[ROW][COL];
和我们平常使用一个变量一样,我们想使用这个棋盘,就得先对其进行初始化,那么在game()函数中就应是这样两行语句:
char board[ROW][COL];
init_board(board,ROW,COL);
那么接下来我们就进行棋盘初始化函数的编写:
(PS:下面其他函数的编写过程(即头文件先包含,再在game.c文件进行编写)同理,就不再赘述啦)
- 先在头文件中声明一下:
函数传参解释:要对二维数组进行操作,故需传二维数组(首元素的地址);因为通过二维数组的行、列信息可简化一些操作,故也作为参数进行传递
(PS:下面其他函数同理) - 然后再在game.c文件中进行编写实现:
void init_board(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] = ' ';
}
}
}
- 解释:其实就是通过一个双重循环把二维数组中的每个元素置为空格,这样在打印棋盘时就显示出了空棋盘的效果。
接着我们就可以打印棋盘来看看效果了。
3. 棋盘的打印
这里先附上一张“成品图”,大家可以先思考一下怎么实现
- 代码:
//打印棋盘
void print_board(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 < row; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
}
printf("\n");
}
}
- 解释:从第一行来看,我们应该先打印一个二维数组中的元素,然后打印一个 “ | ”,为了一定的美观性,我们在打印二维数组中的元素时在其前后都加上空格,即
printf(" %c ", board[i][j]);
接着就是需要注意“ | ”每一行只需要打印两次,故需一个条件
if (j < col - 1)
来限制一下;
再来就是 “- - -” 的分割线,只需把其当作一个上述前后都加上空格的数组元素即 “ %c ”,其他同理
打印完一行后记得进行换行。(打印数组元素看作一行,打印“- - -”也看作一行,所以如上代码一次由 i 控制的循环会打印两个换行)
函数完成后,就可把其加到game()函数中进行测试了:
char board[ROW][COL];
init_board(board,ROW,COL);
print_board(board, ROW, COL);
棋盘打印完,就可以开始下棋啦!
4. 玩家落子
- 思路:玩家移动用 “ * ”来表示,在输入相应坐标值后,将棋盘上对应的位置放上 “ * ”(这里需要注意的是我们输入坐标的思维一般都是从1开始,而数组下标是从0开始);
还需对两种输入不合法的情况进行处理:
(1)输入的坐标值超出了棋盘大小,如:(4,3)
(2)输入的坐标在棋盘的对应位置上已有棋子
如上两种情况的出现都需要重新输入,故我们把输入过程写成循环,直至正确输入才break跳出。 - 代码如下:
void player_move(char board[ROW][COL], int row, int col)
{
int x;
int y;
printf("玩家回合:请输入落子坐标:\n");
while (1)
{
scanf("%d%d", &x, &y);
if (x < 1 || x > row || y < 1 || y > col)
{
printf("输入错误,请重新输入:\n");
continue;
}
if (' ' == board[x - 1][y - 1])
{
board[x - 1][y - 1] = '*';
break;
}
else
printf("该位置已有棋子,请重新输入:\n");
}
}
5. 电脑落子
- 思路:电脑移动用 “#” 表示,因为可以让电脑只在指定范围内随机落子,故不会出现上述(1)的情况,也就不用考虑输入的视角问题。需要考虑的就是随机数坐标生成后要分别模上一个行和列来保证在指定范围内落子。对于随机数生成的具体方法可见同专栏猜数游戏的文章,链接如下:
【C语言】-猜数游戏-简单版
随机的坐标也会出现上述(2)的情况,故也需要一个循环,直到随机坐标符合条件break跳出。 - 代码如下:
void computer_move(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑回合:\n");
while (1)
{
x = rand() % ROW;
y = rand() % COL;
if (' ' == board[x][y])
{
board[x][y] = '#';
break;
}
}
}
同打印棋盘,写到这也可以在game()函数中进行一下简单的测试:
char board[ROW][COL];
init_board(board,ROW,COL);
print_board(board, ROW, COL);
player_move(board, ROW, COL);
computer_move(board, ROW, COL);
6. 判断输赢
- 思路:设玩家赢返回’*‘;电脑赢返回’#‘;平局返回’Q’;游戏继续返回’C’;
判赢条件:某一行或某一列或某一对角线上的元素相同 且不为空格,再根据上面的返回规则,直接返回该行或该列或该对角线上的任一元素即可
对于平局:专设一个is_full函数来判断棋盘是否已满
is_full()函数:遍历棋盘,若仍有空格返回1表示未满,否则返回0。由于is_full函数是专门为判断输赢这个函数服务的,故可直接放在 game.c 文件中,还可再用关键字static对其进行修饰,让其彻底为该文件服务 - 代码如下:
static 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 1;
}
return 0;
}
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][0] == board[i][2] && board[i][0] != ' ')
return board[i][0];
}
//列判断
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[0][i] == board[2][i] && board[0][i] != ' ')
return board[0][i];
}
//对角线判断
if (board[0][0] == board[1][1] && board[0][0] == board[2][2] && board[0][0] != ' ')
return board[0][0];
else if(board[0][2] == board[1][1] && board[0][2] == board[2][0] && board[0][2] != ' ')
return board[0][0];
//平局判断
int temp = is_full(board, ROW, COL);
if (temp == 0)
return 'Q';
else
return 'C';
}
有了上述内容我们就可以在game()函数进行“组装”了!
7. 函数的组装整合
- 思路:首先,在上述测试的基础上,我们可以确定下棋是一个循环的过程,要么在一方获胜后break跳出循环;要么满足平局条件break跳出循环;所以我们创建一个变量 ret 来接受判赢函数is_win()的返回值,然后每走一步判断就判断一次,只要返回值不是 “C” 就break跳出循环(因为当返回值不是C时要么是一方赢要么是平局),并在每次落子后都打印一次棋盘。
跳出循环后,对ret的值进行判断然后打印结果即可:若是 “ * ” 则输出玩家赢;若是 “#” 则输出电脑赢; 若是 “Q” 则输出平局 - 代码如下:
void game()
{
char ret;
srand((unsigned int)time(NULL));
char board[ROW][COL];
init_board(board,ROW,COL);
print_board(board, ROW, COL);
while (1)
{
player_move(board, ROW, COL);
ret = is_win(board, ROW, COL);
print_board(board, ROW, COL);
if (ret != 'C')
break;
computer_move(board, ROW, COL);
ret = is_win(board, ROW, COL);
print_board(board, ROW, COL);
if (ret != 'C')
break;
}
if (ret == '*')
printf("玩家获胜!\n");
else if (ret == '#')
printf("电脑获胜!\n");
else if (ret == 'Q')
printf("平局\n");
}
8. 完整代码:
(1)game.h文件
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#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 player_move(char board[ROW][COL], int row, int col);
//电脑回合
void computer_move(char board[ROW][COL], int row, int col);
//判断输赢
char is_win(char board[ROW][COL], int row, int col);
(2)game.c文件
#include"game.h"
//初始化棋盘
void init_board(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 print_board(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 < row; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
}
printf("\n");
}
}
//玩家回合
//PS: 1.视角问题 2坐标合法性. 3.位置是否已被占用
void player_move(char board[ROW][COL], int row, int col)
{
int x;
int y;
printf("玩家回合:请输入落子坐标:\n");
while (1)
{
scanf("%d%d", &x, &y);
if (x < 1 || x > row || y < 1 || y > col)
{
printf("输入错误,请重新输入:\n");
continue;
}
if (' ' == board[x - 1][y - 1])
{
board[x - 1][y - 1] = '*';
break;
}
else
printf("该位置已有棋子,请重新输入:\n");
}
}
//电脑回合
//PS:1.不需判断合法 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;
y = rand() % COL;
if (' ' == board[x][y])
{
board[x][y] = '#';
break;
}
}
}
//判断输赢
//PS:
//1. 玩家赢返回'*';电脑赢返回'#';平局返回'Q';游戏继续返回'C'
//2. 专设is_full函数判断棋盘是否已满
//3. 判赢条件:行或列或对角线相同且不为空格
static 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 1;
}
return 0;
}
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][0] == board[i][2] && board[i][0] != ' ')
return board[i][0];
}
//列判断
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[0][i] == board[2][i] && board[0][i] != ' ')
return board[0][i];
}
//对角线判断
if (board[0][0] == board[1][1] && board[0][0] == board[2][2] && board[0][0] != ' ')
return board[0][0];
else if(board[0][2] == board[1][1] && board[0][2] == board[2][0] && board[0][2] != ' ')
return board[0][0];
//平局判断
int temp = is_full(board, ROW, COL);
if (temp == 0)
return 'Q';
else
return 'C';
}
(3)test.c文件
#include"game.h"
void menu()
{
printf("***********************************\n");
printf("************ 1.Play *************\n");
printf("************ 0.Exit *************\n");
printf("***********************************\n");
}
void game()
{
char ret;
srand((unsigned int)time(NULL));
char board[ROW][COL];
init_board(board,ROW,COL);
print_board(board, ROW, COL);
while (1)
{
player_move(board, ROW, COL);
ret = is_win(board, ROW, COL);
print_board(board, ROW, COL);
if (ret != 'C')
break;
computer_move(board, ROW, COL);
ret = is_win(board, ROW, COL);
print_board(board, ROW, COL);
if (ret != 'C')
break;
}
if (ret == '*')
printf("玩家获胜!\n");
else if (ret == '#')
printf("电脑获胜!\n");
else if (ret == 'Q')
printf("平局\n");
}
void test()
{
int input = 0;
do
{
menu();
printf("请输入进行选择:\n");
scanf("%d", &input);
switch (input)
{
case 1:
game();
case 0:
break;
default:
printf("输入错误,请重新输入:\n");
}
} while (input);
}
int main()
{
test();
return 0;
}
9. 不足与优化思路
PS:这一部分作为即时更新内容,由于本人知识技术尚薄弱,优化的具体实现需一段时间来完成,后续若有新的优化思路或解决了某个优化问题,将在这一部分进行更新分享
(1)判断输赢部分其实被写“死”了,只能用于判断三子棋的胜负
(2)可多自定义一个常量来控制输赢条件,因为判断输赢和棋盘规模都用的是ROW和COL作为控制,所以若是五子棋,5×5的棋盘可能会相对较小了。此时就可以多定义一个 WIN 5 作为获胜条件来服务五子棋的判赢,而ROW和COL就可改为10从而扩大棋盘的规模。但这点优化应该是建立在(1)的基础上的
(3)电脑下棋是完全随机的,可以通过一些算法让其更 “智能” 一些
(4)可通过Sleep()和system(“cls”)等函数进行游戏视觉效果上的优化等
如果大伙还有什么优化思路,非常希望各位在评论区留下宝贵的意见。感激不尽!
以上就是我对C语言实现简单版三子棋的内容分享啦。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹