目录
前言
上一篇博客,我们讲述了猜数字游戏的实现过程,很多小伙伴可能觉得猜数字游戏太过于简单,今天我们就来小小的升级一下难度,共同探讨一下三子琪这款经典游戏的实现。
一.游戏概述
三子棋是一种民间传统游戏,又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战,双方依次在9宫格棋盘上摆放棋子,率先将自己的三个棋子走成一条线就视为胜利,而对方就算输了,但是三子棋在很多时候会出现和棋的局面。
三.游戏过程
写一个三子棋游戏:
- 打印棋盘
- a.创建一个二维数组存放下棋数据
- b. 初始化棋盘
- c.打印下棋前棋盘开始下棋
- 玩家下棋
- a. 玩家输入一个有效坐标(下棋)
- b.打印出下棋位置
- 电脑下棋
- a.电脑自动生成一个有效坐标(下棋)
- b.打印出电脑下棋位置
- 判断输赢
- 玩家和电脑各自下完棋后进行判断
首先,我们先来看一下三子棋下棋时最开始的样子,这样我们心里也有个底,才知道该如何下手,实现这个游戏。
那么下棋肯定要有棋盘,而这个棋盘该怎么产生呢?我们只需要用简单的打印函数printf即可实现,但是打印棋盘总得将下棋的数据打印出来,否则我们就无法看见对弈的轨迹。而棋盘有行列之分,所以我们学习了二维数组以后立马就能想到此处存储下棋数据应该使用二维数组,因此我们在打印棋盘之前,首先应该创建一个二维数组,并将其数组内容初始化为空格。这样一开始呈现在玩家面前的就是一个全新的棋盘。本期采用分模块编程的方法,简称模块化编程。将会把各个功能模块封装成函数,然后由主函数中的游戏模块调用。
三.游戏实现
(1)主函数
既然是玩游戏,那么一开始我们就需要先进入游戏主页面,并且希望能够一直玩,而不是玩一次就退出游戏,所以应该选择使用do....while()循环,这样一开始就能够进入游戏然后选择是否玩游戏。而进入游戏主页面需要一个菜单进行选择。接下来我们就用printf()函数实现一个简易菜单。代码如下:
void menu()
{
printf("***********************\n");
printf("****** 1.play *******\n");
printf("****** 0.exit *******\n");
printf("***********************\n");
}
主函数代码如下:
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
//printf("三子棋\n");
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
(2).游戏主体(game.c)
主函数中调用game()函数实现三子棋游戏的全过程,上面我们已经大概分析了一下游戏过程,大致分为打印棋盘,玩家下棋,电脑下棋,判断输赢4步。那么game函数应该怎么实现呢?我们先来看一下game()函数的主体。各分模块后续讲解。
void game()
{
char arr[ROW][COL] = { 0 };//存储数据
InitBoard(arr, ROW, COL);//初始化棋盘
DisplayBoard(arr, ROW, COL);//打印棋盘
char ret = 0;
while (1)
{
//玩家下棋
PlayerMove(arr, ROW, COL);
ret = IsWin(arr, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
ComputerMove(arr, ROW, COL);
ret = IsWin(arr, ROW, COL);
if (ret != 'C')
{
break;
}
//玩家赢了返回*
//电脑赢了返回#
//平局返回Q
//游戏继续返回C
}
if (ret == '*')
{
printf("恭喜玩家,赢得比赛!\n");
}
else if (ret == '#')
{
printf("电脑赢得比赛!\n");
}
else
{
printf("平局!\n");
}
}
这里我们在循环外部定义一个字符型变量ret,如果ret返回‘*’,代表玩家赢,如果ret返回‘#’,代表电脑赢。平局返回Q,游戏继续返回C。这里为什么这么设计,后续判断输赢模块讲解。
1)二维数组初始化(棋盘初始化)(InitBoard)
代码如下:
//初始化棋盘
void InitBoard(char arr[ROW][COL],int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
arr[i][j] = ' ';//将棋盘内容初始化为空格
}
}
}
上面二维数组中我们用ROW表示行,COL表示列,row与col是函数传参穿过来的行与列,ROW与COL都是在头文件(game.h)中#define定义的标识符常量,这样定义的好处是可以提高程序代码的应用性,比如如果你现在要写一个五子棋游戏,那么只需要将头文件中的行与列的值换成5即可。
2)打印棋盘(DispalyBoard)
代码如下:
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
printf(" %c | %c | %c \n",board[i][0],board[i][1],board[i][2]);
if (i < row - 1)
{
printf("---|---|---\n");
}
}
}
上面这个代码对于打印三子棋棋盘已是足够,但是上述在定义棋盘行列时我们将其行列定义为了标识符常量,为了扩大代码的应用范围,那么打印棋盘时自然也应该对应能够打印出其他的棋盘。优化版棋盘打印函数如下:
//打印棋盘
void DisplayBoard(char arr[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 ", arr[i][j]);
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
//打印分割线
for (j = 0; j < col; j++)
{
if (i < row - 1)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
}
printf("\n");
}
}
此优化版本的棋盘打印函数就可以打印其他诸如五子棋,七子棋,十子琪之类的棋盘,例如打印五子棋棋盘:
因此,此函数不仅可以打印三子棋棋盘,其余棋盘也能打印,极大地提高了该函数的应用范围。
3)玩家下棋(PlayerMove)
代码如下:
//玩家下棋
void PlayerMove(char arr[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家下棋:\n");
while (1)
{
printf("请输入坐标:");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)//判断坐标合法性,在棋盘内下棋
{
if (arr[x - 1][y - 1] == ' ')
{
arr[x - 1][y - 1] = '*';
DisplayBoard(arr, ROW, COL);//打印棋盘
break;
}
else
{
printf("该坐标已被占用,请重新输入坐标!\n");
}
}
else
{
printf("坐标输入错误,请重新输入!\n");
}
}
}
对于玩家下棋,需要注意的是玩家不是程序员,不知道数组坐标是从0开始的,下棋时输入坐标都是从1开始,所以输入时横纵坐标都必须大于0,而想要找到对应坐标数组元素只需将横纵坐标各自减一即可。这里我们设定玩家下的琪为‘*’,同时在玩家下棋时必须判断坐标是否被占用,若坐标已被占用即电脑下棋占用了坐标,那么玩家就需要重新输入一个坐标,否则玩家就会缺失一次下棋机会,所以下棋动作需要在循环内完成,直至正确下好琪退出循环。
4)电脑下棋(ComputerMove)
代码如下:
/电脑下棋
void ComputerMove(char arr[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋:\n");
while (1)
{
x = rand() % row;
y = rand() % col;
if (arr[x][y] == ' ')
{
arr[x][y] = '#';
DisplayBoard(arr, ROW, COL);//打印棋盘
break;
}
}
}
电脑下棋的关键在于产生一对在三行三列区间范围内坐标,这个坐标的产生可由rand()函数随机生成。别忘了!使用rand()之前需要调用srand()生成时间戳,使系统时间初始化!需要注意的是,srand()不能写在随机数生成的循环中,因此可以将srand()放在主函数中,生成一次随机数种子即可。而电脑下棋不需要考虑像玩家下棋时的坐标问题,电脑只需要产生一对行列都在0~2之间的坐标下棋即可。我们设定电脑下的棋为‘#’。而想要产生在这区间的数只需要将rand()函数生成的随机数对行列分别取余即可,因为任意一个数对3取余,其结果一定小于3。对3取余其值范围为0~2,正好对应二维数组下标。
5)判断输赢函数(IsWin)
代码如下:
char IsWin(char arr[ROW][COL], int row, int col)
{
//判断行
int i = 0;
for (i = 0; i < row; i++)
{
if (arr[i][0] == arr[i][1] && arr[i][1] == arr[i][2] && arr[i][2] != ' ')
{
return arr[i][1];
}
}
//判断列
for (i = 0; i < col; i++)
{
if (arr[0][i] == arr[1][i] && arr[1][i] == arr[3][i] && arr[1][i] != ' ')
{
return arr[1][i];
}
}
//判断对角线
if (arr[0][0] == arr[1][1] && arr[1][1] == arr[2][2] && arr[2][2] != ' ')
{
return arr[1][1];
}
if (arr[0][2] == arr[1][1] && arr[1][1] == arr[2][0] && arr[1][1] != ' ')
{
return arr[1][1];
}
//判断平局
int ret = IsFull(arr, row, col);
if (1 == ret)//返回1表示满了,返回0表示没满
{
return 'Q';
}
else
{
return 'C';
}
}
三子棋判断输赢的条件无非就是三种情况,相同的三个子连成一行,相同的三个子连成一列,又或是相同的三个子连成一条对角线。值得一提的是,在改代码中,若满足胜利条件,返回的是元素内容,这样,无论是玩家赢了还是电脑赢了都能够返回对应的输赢判断字符,这个判断函数无论是玩家还是电脑都可以使用。这也是为什么在game函数中使用‘*’和‘#’作为判断条件的原因。需要注意的是,判断条件中需要三子相同并且不等于空格。因为空格是还没有下过棋的格子。判断输赢的过程中还需考虑一种情况,那就是平局,那什么时候会平局呢?若玩家下棋和电脑下棋在把棋盘下满后任然没有分出胜负,那么此时就是和局。若没有胜负,并且棋盘也还没有下满,那么此时游戏继续,输赢判断函数返回‘C’。棋盘是否下满代码如下:
int IsFull(char arr[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 (arr[i][j] == ' ')
{
return 0;//没下满,返回0
}
}
}
return 1;//下满返回1
}
四.完整代码!!!
由于代码很多,为了让代码更加易读、逻辑性更强,将代码分为test.c,game.c,game.h三个文件编写。这其实也就是本文开头时所说的模块化编程。
1.test.c源文件
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void menu()
{
printf("***********************\n");
printf("****** 1.play *******\n");
printf("****** 0.exit *******\n");
printf("***********************\n");
}
void game()
{
char arr[ROW][COL] = { 0 };//存储数据
InitBoard(arr, ROW, COL);//初始化棋盘
DisplayBoard(arr, ROW, COL);//打印棋盘
char ret = 0;
while (1)
{
//玩家下棋
PlayerMove(arr, ROW, COL);
ret = IsWin(arr, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
ComputerMove(arr, ROW, COL);
ret = IsWin(arr, ROW, COL);
if (ret != 'C')
{
break;
}
//玩家赢了返回*
//电脑赢了返回#
//平局返回Q
//游戏继续返回C
}
if (ret == '*')
{
printf("恭喜玩家,赢得比赛!\n");
}
else if (ret == '#')
{
printf("电脑赢得比赛!\n");
}
else
{
printf("平局!\n");
}
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
//printf("三子棋\n");
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
2.game.h头文件
由于在test.c中包含了game.h的头文件,因此在test.c中用到的其他头文件只需要包含在game.h头文件中即可。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 3
#define COL 3
//初始化棋盘
void InitBoard(char arr[ROW][COL], int row, int col);
//打印棋盘
void DisplayBoard(char arr[ROW][COL], int row, int col);
//玩家下棋
void PlayerMove(char arr[ROW][COL], int row, int col);
//电脑下棋
void ComputerMove(char arr[ROW][COL], int row, int col);
//判断输赢
char IsWin(char arr[ROW][COL], int row, int col);
3.game.c源文件
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
//初始化棋盘
void InitBoard(char arr[ROW][COL],int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
arr[i][j] = ' ';//将棋盘内容初始化为空格
}
}
}
//打印棋盘
void DisplayBoard(char arr[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 ", arr[i][j]);
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
//打印分割线
for (j = 0; j < col; j++)
{
if (i < row - 1)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
}
printf("\n");
}
}
//玩家下棋
void PlayerMove(char arr[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("玩家下棋:\n");
while (1)
{
printf("请输入坐标:");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)//判断坐标合法性
{
if (arr[x - 1][y - 1] == ' ')
{
arr[x - 1][y - 1] = '*';
DisplayBoard(arr, ROW, COL);//打印棋盘
break;
}
else
{
printf("该坐标已被占用,请重新输入坐标!\n");
}
}
else
{
printf("坐标输入错误,请重新输入!\n");
}
}
}
//电脑下棋
void ComputerMove(char arr[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋:\n");
while (1)
{
x = rand() % row;
y = rand() % col;
if (arr[x][y] == ' ')
{
arr[x][y] = '#';
DisplayBoard(arr, ROW, COL);//打印棋盘
break;
}
}
}
int IsFull(char arr[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 (arr[i][j] == ' ')
{
return 0;
}
}
}
return 1;
}
char IsWin(char arr[ROW][COL], int row, int col)
{
//判断行
int i = 0;
for (i = 0; i < row; i++)
{
if (arr[i][0] == arr[i][1] && arr[i][1] == arr[i][2] && arr[i][2] != ' ')
{
return arr[i][1];
}
}
//判断列
for (i = 0; i < col; i++)
{
if (arr[0][i] == arr[1][i] && arr[1][i] == arr[3][i] && arr[1][i] != ' ')
{
return arr[1][i];
}
}
//判断对角线
if (arr[0][0] == arr[1][1] && arr[1][1] == arr[2][2] && arr[2][2] != ' ')
{
return arr[1][1];
}
if (arr[0][2] == arr[1][1] && arr[1][1] == arr[2][0] && arr[1][1] != ' ')
{
return arr[1][1];
}
//判断平局
int ret = IsFull(arr, row, col);
if (1 == ret)//返回1表示满了,返回0表示没满
{
return 'Q';
}
else
{
return 'C';
}
}
刹国(结束)!!!