三子棋游戏介绍
这一次我们要写一个三子棋游戏,其实就是井字棋。主要需要用到的知识有这些:1.多文件实现项目2.模块化编程。
源文件介绍
我们这次将游戏分成三个文件来实现
1.game.h头文件,用于定义常量(棋盘大小,游戏类型);引用头文件;函数的定义。
代码如下:
//头文件引用
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//常量声明
#define ROW 3//棋盘大小
#define COL 3//棋盘大小
#define TYPE 3//游戏类型(现在是三子棋)
//函数声明
//打印菜单
void menu();
//游戏函数
void game();
//初始化棋盘
void init(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)]);
//显示棋盘
void display_board(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)]);
//玩家下棋
char player_move(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)]);
//电脑下祺(入门)
char computer_move(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)]);
//判断游戏状态
static char state(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)], int r, int c);
void is_win(char ret);
2.test.c源文件,用于测试游戏的基本功能,充当接口。
代码如下:
#include "game.h"
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
//打印菜单,实现选择功能
do
{
menu();
printf("请选择:\n");
scanf("%d", &input);
switch (input)
{
case 1:/*printf("play!\n");*/
game();
break;
case 0:printf("游戏结束!\n");
break;
default:printf("输入错误,请重新输入\n");
break;
}
} while (input);
system("pause");
return 0;
}
3.game.c源文件,最主要的文件,游戏的实现基本上都在这个文件中。
代码如下:
#include "game.h"
void menu()
{
printf("*********************\n");
printf("***** 1. play *****\n");
printf("***** 0. exit *****\n");
printf("*********************\n");
}
void game()
{
//棋盘规模(2+3+2)*(2+3+2)
char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)] = { 0 };
char ret = 0;
char flag = 0;
init(board);
display_board(board);
while (1)
{
ret = player_move(board);
display_board(board);
if (ret != 'C')
break;
ret = computer_move(board);
display_board(board);
if (ret != 'C')
break;
}
is_win(ret);
}
void init(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)])
{
for (int i = TYPE - 1; i < ROW + TYPE - 1; i++)
{
for (int j = TYPE - 1; j < COL + TYPE - 1; j++)
{
board[i][j] = ' ';
}
}
}
void display_board(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)])
{
for (int i = TYPE - 1; i < ROW + TYPE - 1; i++)
{
for (int j = TYPE - 1; j < COL + TYPE - 1; j++)
{
printf(" %c ", board[i][j]);
if (j < COL + TYPE - 2)
{
printf("|");
}
}
printf("\n");
if (i < ROW + TYPE - 2)
{
for (int k = TYPE - 1; k < COL + TYPE - 1; k++)
{
printf("---");
if (k < COL + TYPE - 2)
{
printf("|");
}
}
printf("\n");
}
}
}
char player_move(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)])
{
int r = 0, c = 0;
printf("玩家走,请输入坐标:\n");
while (1)
{
scanf("%d %d", &r, &c);
r += TYPE - 2;
c += TYPE - 2;
if (r >= TYPE - 1 && r <= 2 * (TYPE - 1) && c >= TYPE - 1 && c <= 2 * (TYPE - 1))
{
if (board[r][c] == ' ')
{
board[r][c] = '*';
return state(board, r, c);
}
else
{
printf("该位置已经有棋子了,请重新下棋\n");
}
}
else
{
printf("坐标非法,请重新下棋\n");
}
}
}
char computer_move(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)])
{
printf("电脑走\n");
while (1)
{
int r = rand() % ROW + 1;
int c = rand() % COL + 1;
r += TYPE - 2;
c += TYPE - 2;
if (board[r][c] == ' ')
{
board[r][c] = '#';
return state(board, r, c);
}
}
}
static char state(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)], int r, int c)
{
//判断纵向
for (int i = r - (TYPE - 1); i <= r + (TYPE - 1); i++)
{
if (board[i][c] == board[r][c] && board[i + 1][c] == board[r][c] && board[i + 2][c] == board[r][c])
{
return board[r][c];
}
}
//判断横向
for (int j = c - (TYPE - 1); j <= c + (TYPE - 1); j++)
{
if (board[r][j] == board[r][c] && board[r][j + 1] == board[r][c] && board[r][j + 2] == board[r][c])
{
return board[r][c];
}
}
//判断右斜
for (int i = r - (TYPE - 1), j = c - (TYPE - 1); i <= r + (TYPE - 1); i++, j++)
{
if (board[i][j] == board[r][c] && board[i + 1][j + 1] == board[r][c] && board[i + 2][j + 2] == board[r][c])
{
return board[r][c];
}
}
//判断左斜
for (int i = r - (TYPE - 1), j = c + (TYPE - 1); i <= r + (TYPE - 1); i++, j--)
{
if (board[i][j] == board[r][c] && board[i + 1][j - 1] == board[r][c] && board[i + 2][j - 2] == board[r][c])
{
return board[r][c];
}
}
//判断平局
for (int i = TYPE - 1; i < ROW + TYPE - 1; i++)
{
for (int j = TYPE - 1; j < COL + TYPE - 1; j++)
{
if (board[i][j] == ' ')
{
//返回C代表游戏继续
return 'C';
}
}
}
//返回E代表平局
return 'E';
}
void is_win(char ret)
{
if (ret == '*')
{
printf("你赢了!\n");
system("pause");
system("cls");
}
else if (ret == '#')
{
printf("很遗憾,你输了!\n");
system("pause");
system("cls");
}
else if (ret == 'E')
{
printf("平局!\n");
system("pause");
system("cls");
}
}
游戏演示
详解
1.棋盘的定义
不知道大家看到没有,我这次写的代码还是挺有差异性的。这是因为我希望我的游戏可以很轻松地改成五子棋,所以出来棋盘的行ROW
和列COL
,我在常量定义中,多定义了一个TYPE
常量,用于改变游戏类型。
由于是三子棋(井字棋),所以我们的棋盘大小就定义成三行三列,游戏类型为3,即三子棋。
需要注意的是,我在game.c中将棋盘的大小定义成char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)] = { 0 };
可能有小伙伴心里充满了疑惑,先不要慌张,在之后的判断游戏状态的函数中我会详细的解释!
2.判断游戏状态(玩家赢,电脑赢,平局,游戏继续)
最重要的一个函数state
函数,它的实现比较复杂,接下来我们详细解释。
我们知道,游戏结束有五种情况,即:横向连了三个子,纵向连了三个子,左斜连了三个子,右斜连了三个子,还有一个平局(棋盘满了都没有胜负)。
这里我用了如下的算法:
从落子的坐标开始,计算它上方或者下方是否有两个连续的棋子(纵向),左方或者右方是否有两个连续的棋子(横向),左斜和右斜也是如此。但是这又会引出一个新的问题,数组越界的问题,因为边界的棋子周围并没有足够多的空间让他们判断。
所以我们在定义棋盘时,需要将棋盘扩大,扩大的空间经过计算过后为(TYPE-1)*2即游戏类型减一的两倍。
图解:
白色的方块即为扩大的部分,红色为玩家看到的棋盘。所以以三子棋为例,我们的棋盘大小为:2+3+2=7
这样能够避免数组越界的问题,并且在后期想改成五子棋时更加方便。
希望小伙伴能够理解我为什么将判断讲在这么前面。因为它的存在,影响了我们其他函数的设计。接下来我们再继续。
初始化棋盘,显示棋盘
初始化棋盘我们只需要将玩家看到的3*3的棋盘初识化为' '(空格)
。因为在我们定义棋盘时,已经将所有内容赋初值为0了。
打印棋盘也是如此,只需打印3 * 3的棋盘即可。需要注意的是,我们这里需要打印分割线,具体实现方式请看源代码。
打印效果:
玩家落子
玩家落子需要注意三点
1.玩家输入的坐标是否合法;
2.玩家输入的坐标是否已经有棋子了;
3.玩家输入的坐标是(1 1)而在我们数组中则是(2 2),我们的解决方法是将输入的坐标加上(TYPE-2)。
如果以上三个条件都满足,我们就在该位置放上一个'*'
。
电脑落子
电脑落子需要用到随机数,关于随机数的生成,我在之前的博客已经很详细地讲过了。这里就不再介绍了。
需要注意的是我们得到的随机数需要控制在棋盘内需要按照一下方式写:
int r = rand() % ROW + 1;
int c = rand() % COL + 1;
r += TYPE - 2;
c += TYPE - 2;
生成坐标利用循环实现,当坐标合适时,在棋盘放入一个'#'
游戏流程图
如何修改成五子棋
我们的程序比较好的一点就是可以比较方便地修改成五子棋。
只需修改两处代码即可。
1.修改棋盘和游戏类型:
#define ROW 10//棋盘大小
#define COL 10//棋盘大小
#define TYPE 5//游戏类型(现在是五子棋)
2.修改判断游戏状态的函数
static char state(char board[ROW + 2 * (TYPE - 1)][COL + 2 * (TYPE - 1)], int r, int c)
{
//判断纵向
for (int i = r - (TYPE - 1); i <= r + (TYPE - 1); i++)
{
if (board[i][c] == board[r][c] && board[i + 1][c] == board[r][c] && board[i + 2][c] == board[r][c] && board[i + 3][c] == board[r][c] && board[i + 4][c] == board[r][c])
{
return board[r][c];
}
}
//判断横向
for (int j = c - (TYPE - 1); j <= c + (TYPE - 1); j++)
{
if (board[r][j] == board[r][c] && board[r][j + 1] == board[r][c] && board[r][j + 2] == board[r][c] && board[r][j + 3] == board[r][c] && board[r][j + 4] == board[r][c])
{
return board[r][c];
}
}
//判断右斜
for (int i = r - (TYPE - 1), j = c - (TYPE - 1); i <= r + (TYPE - 1); i++, j++)
{
if (board[i][j] == board[r][c] && board[i + 1][j + 1] == board[r][c] && board[i + 2][j + 2] == board[r][c] && board[i + 3][j + 3] == board[r][c] && board[i + 4][j + 4] == board[r][c])
{
return board[r][c];
}
}
//判断左斜
for (int i = r - (TYPE - 1), j = c + (TYPE - 1); i <= r + (TYPE - 1); i++, j--)
{
if (board[i][j] == board[r][c] && board[i + 1][j - 1] == board[r][c] && board[i + 2][j - 2] == board[r][c] && board[i + 3][j - 3] == board[r][c] && board[i + 4][j - 4] == board[r][c])
{
return board[r][c];
}
}
//判断平局
for (int i = TYPE - 1; i < ROW + TYPE - 1; i++)
{
for (int j = TYPE - 1; j < COL + TYPE - 1; j++)
{
if (board[i][j] == ' ')
{
//返回C代表游戏继续
return 'C';
}
}
}
//返回E代表平局
return 'E';
}
建议
由于我们的电脑是生成的随机数,所以没有什么挑战性,我建议将游戏改成双人模式,这样就不会因为电脑太笨而失去游戏的乐趣了!
最后总结
这是我们第一次写一个小项目,还是有两百多行的代码量的。其中最有意思的地方就是判断游戏状态的那一部分。不过我们的这个小游戏还是有不足的地方的,比如说电脑太笨了,等以后能力足够时,可以试着改进算法,提高游戏难度。
如果有出错的地方大家可以提出来,一起讨论,一起进步!
顺便预告一下,下一期博客内容是扫雷游戏的实现,到时候记得来捧个场哈!