本篇文章将手把手带你通过C语言中简单的分支循环语句,使用二维数组编写一个井字棋小游戏。
我们在这里只需要实现一个功能,在键盘上输入两个数字,一个代表进入游戏,另一个代表退出游戏,进入游戏就跳转到游戏模块,退出游戏则直接结束程序。此外,这一步需要注意的有两个点:一个是用户可输入的不只是规定的两个数字,所以为避免bug出现,我们需要编写出现其他字符时的情况;还有一个就是为了让我们的游戏可以进行多次,我们需要写成循环结构。
void menu()
{
printf("*********************************\n");
printf("******* 1.play 0.exit ******\n");
printf("*********************************\n");
}
int main()
{
int input = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("三子棋游戏\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
return 0;
}
这里我使用了switch和dowhile语句,当然你们也可以使用用ifesle还有其他循环语句,只需要实现上述逻辑就行。
void game()//游戏模块实现
{
char board[ROW][COL];//创建一个棋盘
char recei = 0;//用于判断棋盘状态的变量
srand((unsigned int)time(NULL));//生成随机数起点
//初始化棋盘
initi_board(board, ROW, COL);
//生成棋盘
Create_board(board, ROW, COL);
printf("请输入您要下的棋的坐标,示例:第一行第二列1 2\n");
while (1)
{
//玩家下棋
Playermove(board, ROW, COL);
Create_board(board, ROW, COL);
//判断棋盘状态
recei = judge_condition(board, ROW, COL);
if (recei == '*' || recei == 'D')
break;
//电脑下棋
Computermove(board, ROW, COL);
Create_board(board, ROW, COL);
//判断棋盘状态
recei = judge_condition(board, ROW, COL);
if (recei == '#' || recei == 'D')
break;
}
if (recei == '*')
{
printf("游戏结束,玩家胜利!\n");
printf("请选择是否开始下一局游戏\n");
}
else if (recei == '#')
{
printf("游戏结束,电脑胜利!\n");
printf("请选择是否开始下一局游戏\n");
}
else
{
printf("游戏结束,平局!\n");
printf("请选择是否开始下一局游戏\n");
}
}
void initi_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] = ' ';
}
}
}
我们需要创建一个二维数组当作棋盘用于存放棋子。并且我们的棋子是用符号*和#来表示的,所以我们只需要创建一个char类型的二维数组并将其初始化成空白字符即可。
这一步是需要我们将完整的棋盘展示出来,所以注意,这不仅仅是把上面创建打印出来,而是要将棋盘上的格子以及二维数组上的字符一起展示出来
void Create_board(char board[ROW][COL], int row, int col)//生成棋盘
{
int i = 0, 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 < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
}
printf("\n");
}
}
上图是我们这个Create_board函数实现的效果。这个九宫格,四分割线的棋盘实际上是由五行字符组成的,第一行是二维数组元素与单竖杠交替打印而成,且单竖杠字符比数组元素少一个,所以我们需要对第二层for循环打印单竖杠字符加上一个循环变量j<col-1也就是列减一的条件语句,而第二行则是由三道短横线字符和一道单竖杠字符交替打印,同样,单竖杠比短横线少一个,我们使用同样的条件语句选择是否打印单竖杠。除此之外,由于第一二行是在同一次循环打印的,但是总共只需要五行字符,也就是说第二行的打印在第三次循环是多余,因此我们同样需要打印第二行的语句加上条件语句选择是否打印。
while (1)
{
//玩家下棋
Playermove(board, ROW, COL);
Create_board(board, ROW, COL);
//判断棋盘状态
recei = judge_condition(board, ROW, COL);
if (recei == '*' || recei == 'D')
break;
//电脑下棋
Computermove(board, ROW, COL);
Create_board(board, ROW, COL);
//判断棋盘状态
recei = judge_condition(board, ROW, COL);
if (recei == '#' || recei == 'D')
break;
}
这部分我们需要分成三个模块也就是三个函数来实现,玩家下棋模块Playermove,电脑下棋模块Computermove以及判断棋盘状态模块judge_condition。
玩家下棋Playermove的功能是我们在键盘上输入九宫格对应的坐标则下棋成功,如果输入九宫格以外或者以及落过子的坐标就提醒坐标非法或者坐标也已被占用需要重新输入。所以为了实现重新输入的功能需要写成循环结构,而落子需要经过两条判断语句,第一条判断坐标是否落于棋盘内。第二条判断落点是否已被占用,经过两条条件语句的判断方可落子。
void Playermove(char board[ROW][COL], int row, int col)//玩家下棋
{
int x = 0;
int y = 0;
while (1)
{
printf("请您输入您下的坐标:\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("抱歉!该处已被占用,请重新选择:\n");
}
}
else
printf("输入值不合法,请重新输入:\n");
}
}
电脑下棋Computermove的功能是随机在一个未被落子的位置下棋,而这个随机的功能如何实现呢?首先,我们需要这个随机的具体功能是随机数字,我们需要电脑生成两个1到3之间的两个数字来当作下棋的坐标,而在C语言库函数中恰好有我们需要的随机生成数字的函数rand。rand函数的功能是相对于起点,或者说定点随机生成一个数字当作返回值,无需传参,而这个起点rand函数是不会自己生成,这起点的设置需要另一个函数srand,通过srand函数传参就能设置起点。
但是这就出现了一个很尴尬的问题,我们需要一个起点来生成随机数,但是由于每一次启动程序调用rand相对于同一个起点生成的随机数是重复的,这就导致虽然我们在同一次游戏的落子是随机的,但是当我们退出去重进一次就会发现这次游戏电脑的落子的顺序和上一次打开游戏游玩时是一样的。所以我们需要一个随机数来当作rand函数的定点,或者说,在每次启动游戏时,这个定点是不同的。而这个需求恰好可以用时间戳来搞定,由于每一台电脑都自带时间戳并且时间戳会随着时间而改变,所以我们只需要一个函数来获取电脑上的时间戳并放入srand函数中当作函数参数就能保证每次打开游戏的起点是不相同的。而C语言的库函数中有time函数可以获取电脑上的时间戳并返回,而time函数的返回值的类型是time_t,srand函数的参数是unsigned int,而由于只需要一个随机数据,不考虑数据损失,我们直接对返回值进行强制类型转换。此外time函数的参数是一个地址,用于存储返回值,但由于我们没有这个需求,所以我们只需要传给它同一个空指针NULL,它就不再需要存储返回值。
srand((unsigned int)time(NULL));//生成随机数起点
随机数的生成搞定了,接下来搞定随机数的范围在1到3之间,这个我们只需要将生成的随机数摸上一个3再加上1就行,最后再加上一个条件语句来判断落子位置是否被占用即可。
void Computermove(char board[ROW][COL], int row, int col)//电脑下棋
{
printf("电脑下:\n");
while (1)
{
int x = rand() % ROW;
int y = rand() % COL;
//判断占用
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
判断游戏状态judge_condition的功能就是判断玩家胜利、失败或平局,但是我们仅仅在函数里判断完是无法对game函数产生影响的,所以我们要写成一个有返回值的函数,在game函数创建一个变量recei接收这个返回值,通过这个变量来判断棋局是否结束。而在返回值类型中,我们希望玩家胜利时返回玩家的棋子*,电脑胜利时返回电脑的棋子#,平局(棋盘下满且未分胜负时)返回字符‘D’,游戏未终结返回字符‘C’。
char recei = 0;//用于判断棋盘状态的变量
首先,我们希望判断是否有选手胜出,就要判断是否有三个相同的棋子同一行,同一列或同一对角线,行与列的判断我这里使用循环语句,对角线的判断我使用两个if语句,同时为了之间返回胜者的棋子符号,我直接将这几条语句的返回值写成判断的第一个元素,这样就不用写多余的代码判断是谁的棋子。然后当以上语句都不满足时,就来到判断是否平局的语句,我直接用了一个函数来判断棋盘是否下满,并且用此函数的返回值来判断条件语句,满时返回1执行满足条件时的语句也就是返回字符‘D’。所以判断棋盘是否下满的函数full_board只需要用一个循环判断语句判断数组内的所有元素是否为棋子,满足则返回1,不满足则跳出循环,返回0。最后,如果以上所有语句都不满足,执行最后一条表达式返回字符‘C’(C与D本身无意义,只是一个标志罢了)。
int full_board(char board[ROW][COL], int row, int col)
{
int i = 0, j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 0;
}
}
return 1;
}
char judge_condition(char board[ROW][COL], int row, int col)//判断棋盘状态
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)//判断行相同
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1] != ' ')
return board[i][0];
}
for (j = 0; j < col; j++)//判断列相同
{
if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[1][j] != ' ')
return board[0][j];
}
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 (full_board(board, ROW, COL))
return 'D';
return 'C';
}
然后在game函数中玩家电脑对弈的循环中,每次选手下完棋时都对棋盘进行判断,再用if语句对recei判断,不为字符‘C’则继续进行,时则直接跳出循环结束对局,并对recei接收的字符判断是谁胜谁负或平局。
if (recei == '*')
{
printf("游戏结束,玩家胜利!\n");
printf("请选择是否开始下一局游戏\n");
}
else if (recei == '#')
{
printf("游戏结束,电脑胜利!\n");
printf("请选择是否开始下一局游戏\n");
}
else
{
printf("游戏结束,平局!\n");
printf("请选择是否开始下一局游戏\n");
}
到此,这个小游戏就算编写完成了,当然除了上述步骤之外,我们还有很多细节要再提一下,比如,使用库函数是记得引头文件,编写这样一个代码较多的程序时最好想这样将代码分模块使用函数实现,并且分文件编写代码,再每个代码模块进行注释便于修bug或优化,三行三列的数据这种全文通用的数据应该直接用define语句写成两个单词使用,便于修改优化。