C语言也学习一段时间了,为了巩固我们学习的知识,今天我们写一个三子棋的小游戏。这对初学者是个大工程,跟着我一起开始吧。
我们今天写的是三子棋小游戏,说到游戏肯定就有很多模块组成,所以为了提升游戏的可维护性和移植性
,我们应该采用模块化编写程序,将不同的版块分装在不同文件下。
上图为不同文件的任务,接下来我们就开始逐步讲解我们三字棋的实现。
在c语言程序中 最重要的就是main函数,所以我们现在main函数中引用test函数,实现代码分装 使得我们的编写更有层次。
接下来我们开始实现test函数。 在游戏的开始界面我们应该需要一个入口 可以选择玩游戏 或者退出游戏。do while循环可以很好的实现我们的需求。
void test()
{
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);
}
switch在游戏界面选择中非常实用,在上图的代码逻辑实现中我们又引用了菜单meua函数和游戏主体函数game。
meua菜单需要实现一个界面 提供玩游戏和 退出游戏的人两个选择 代码和效果图如下
void menu()
{
printf("*****************************\n");
printf("******** 1.play *********\n");
printf("******** 0.exit *********\n");
printf("*****************************\n");
}
在菜单选择开始游戏后我们进入到游戏后,我们应该可以看到三子棋的棋盘。在打印棋盘前,我们应该创建一个二维数组对应三子棋盘上的9个位置 接受棋子。
#define ROW 3
#define COL 3
首先我们.h文件中定义好数组的行和列,而不是直接对数组规定行和列,这样有利于后续我们对三子棋升级优化
char board[ROW][COL];//创建3*3数组存放字符
这是我们想要实现的棋盘效果,但是我们并没有对数组board初始化,所以数组中默认储存的是0,这就达不到上图的效果。
所以在打印棋盘之前,我们应当先对棋盘初始化,将数组的元素都定义为空格。
我们在test中引用函数 Int_board(board, ROW, COL);
在game.h声明函数后 在game.c文件中编写这个函数。
void Int_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] = ' ';
}
}
}
这里只需要用一个简单遍历 就可以实现。
接下来我们就要打印棋盘。棋盘的每两行可以看成一个部分,一共有3个部分
具体的解释,我们依据代码窥探``
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");
}
}
}
上面我们吧棋盘分为了三个部分,所以在整个打印代码段 都是被一个for套嵌的。之后只要编写一部分的打印就可以了。
在编写玩家下棋的代码部分时 我们需要注意一下几点:
1.数组接受玩家下的棋子
2.判断玩家坐标输入是否合法
3.玩家输入坐标后 将棋盘打印出来
void player_move(char board[ROW][COL], int row, int col)
{
printf("玩家下棋\n");
while (1)
{
printf("玩家输入坐标:>");
int a, b;
scanf("%d %d", &a, &b);
if (a >= 1 && a <= row && b >= 1 && b <= col)
{
if (board[a - 1][b - 1] == ' ')
{
board[a - 1][b - 1] = '*';
break;
}
else
{
printf("该坐标已被占用\n");
}
}
else
{
printf("坐标非法\n");
}
}
}
当玩家scanf输入坐标后,应该要判断坐标输入是否合法,如果玩家输入的坐标已经被占用或者超出了二维数组的范围,就应该循环再次进入玩家输入坐标的地方,知道坐标输入正确break跳出循环。
当玩家正确输入坐标后,我们要讲新的棋盘打印出来。所以我们需要再次在test.c文件中引用 Print_board(board, ROW, COL);
函数。
void comper_move(char board[ROW][COL], int row, int col)
{
srand((unsigned int)time(NULL));
while (1)
{
printf("电脑下棋\n");
int x = rand() % row;
int y = rand() % col;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
这里需要注意的是电脑随机坐标应该如何生成,还是运用我们熟知的rand函数,srand函数他们需要引用头文件**#include
int x = rand() % row;
int y = rand() % col;
这里我们有个结论可以 理解记忆一下
接着我们再将电脑随机下的棋 再打印出来。
游戏的结果判断 我们现在test.c文件中编写出逻辑 再去编写具体函数
在这里我们规定 输赢平局的返回值
while (1)
{
player_move(board, ROW, COL);
print_board(board, ROW, COL);
//判断输赢
ret = is_win(board, ROW, COL);
if (ret != 'C')
{
break;
}
computer_move(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");
用ret接受 is_win函数的返回值
接下来我们主要编写is_win函数 以及对他的优化升级
在三子棋中有这几种赢法
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 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][0] != ' ')
{
return board[i][0];
}
}
//判断三列
for (i = 0; i < col; i++)
{
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[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 (is_full(board, row, col) == 1)
{
return 'Q';
}
//继续
//没有玩家或者电脑赢,也没有平局,游戏继续
return 'C';
}
我们将所有情况列举出来 再写出平局的情况将 字符返回到ret
判断游戏是否需要继续。
在上面判断三子棋是否输赢时,我们是将所有的情况都一一列举出来。这样就限制的游戏的升级 所以我们能不能优化is_win函数呢?
char is_win(char board[ROW][COL], int row, int col)
{
int flag1 = 0;///玩家棋数
int flag2 = 0;//电脑棋数
int i, j;
for (i = 0; i < row; i++)///行 计算
{
flag1 = flag2 = 0;
for (j = 0; j < col; j++)
{
if (board[i][j] == '*')
{
flag1++;
}
if (board[i][j] == '#')
{
flag2++;
}
}
if (flag1 == col)///修改怎么应该实现N子棋
{
return '*';
}
if (flag2 == col)///修改怎么应该实现N子棋
{
return '#';
}
}
for (j = 0; j < col; j++)///列 计算
{
flag1 = flag2 = 0;
for (i = 0; i < row; i++)
{
if (board[i][j] == '*')
{
flag1++;
}
if (board[i][j] == '#')
{
flag2++;
}
}
if (flag1 == col)///修改怎么应该实现N子棋
{
return '*';
}
if (flag2 == col)///修改怎么应该实现N子棋
{
return '#';
}
}
flag1 = flag2 = 0;
for (i = 0, j = 0; i < row, j < col; i++, j++)//正对角线
{
if (board[i][j] == '*')
{
flag1++;
}
if (board[i][j] == '#')
{
flag2++;
}
}
if (flag1 == col)///修改怎么应该实现N子棋
{
return '*';
}
if (flag2 == col)///修改怎么应该实现N子棋
{
return '#';
}
flag1 = flag2 = 0;
for (i = 0, j = col-1; i<row, j >=0; i++, j--)//逆对角线
{
if (board[i][j] == '*')
{
flag1++;
}
if (board[i][j] == '#')
{
flag2++;
}
}
if (flag1 == col)///修改怎么应该实现N子棋
{
return '*';
}
if (flag2 == col)///修改怎么应该实现N子棋
{
return '#';
}
if (is_full(board, row, col) == 1)
{
return 'Q';
}
//继续
//没有玩家或者电脑赢,也没有平局,游戏继续
return 'C';
}
上图我用计数器写了 判断棋盘输赢的代码,虽看起来没有原来简单,但是如果升级到N子棋时,这种写法就比较方便
在我玩了七七九十九局后 感觉这个电脑实在太蠢了,所以我们还有什么方法能将电脑下棋更智能些呢? 我们在下篇讲解。
关于三子棋游戏的分享就到这里。
感谢相遇,共同进步。