目录
一、三子棋简单介绍
二、设计思路和主函数部分
(1)设计思路
(2)主函数部分
三、game()函数的内容
(1)初始化棋盘
(2)打印棋盘
(3)玩家下棋
(4)电脑下棋
(5)判断棋盘是否填满
(6)判断输赢
四、总结
一、三子棋简单介绍
三子棋是一种民间传统游戏,又叫井字棋,游戏规则很简单,在一个九宫格棋盘上,以X代表一方,O代表一方,在九宫格中依次落子,当有一方的棋子在行或列或对角线三个棋子连成一条线时获胜。
二、设计思路和主函数部分
(1)设计思路
在编译软件中我们用'*'和'#'两个字符来代替“X”和“O”两种棋子,通过打印棋盘,创建字符数组,对字符数组初始化,以及对字符数组的赋值和判断等操作来实现游戏的运行;(这里我使用的编译器是VS2019)。完整的代码我会放在整篇文章末尾的总结部分。
(2)主函数部分
首先我们要先写出一个菜单页面,让玩家来选择是否进行游戏;这里我们创建一个menu()函数来实现这个功能:
void menu()
{
printf("******************************\n");
printf("********** 1. play ***********\n");
printf("********** 0. exit ***********\n");
printf("******************************\n");
}
将menu函数放入主函数中,同时需要创建一个变量input来接收玩家做出的选择,这里选择为1或0,所以我们不妨设置一个int类型的变量input,并接收输入值。
int main()
{
int input = 0;
menu();
scanf("%d",&input);
return 0;
}
在接收输入值后我们应该对输入值进行判断,并通过判断结果决定后续运行情况,这里我们用switch条件语句进行判断。当输入1时,应当进行游戏;当输入0时应当退出游戏,当输入既不是1也不是0时,我们应该对玩家进行提醒输入错误,重新输入!!
int main()
{
int input = 0;
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
return 0;
}
此时我们希望玩家在运行过程中能够在选择1进行游戏并且完成游戏之后继续游戏,因此菜单menu()函数和判断应该是一个循环,并且在玩家输入input为0时循环结束,这里我们可以用到do while();循环语句;将input设置为循环的判断值,当玩家选择0退出游戏时,循环跳出。
int main()
{
int input = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
到这里主函数就基本完成了,当玩家选择 1 进行游戏时,通过game()函数进行游戏;具体的游戏运行规则,我们放在game()函数中去设计。
三、game()函数的内容
(1)初始化棋盘
使用InitBoard()函数进行棋盘初始化,基本思路是首先我们需要一个二维数组来接收棋盘的内容,声明一个全局变量char board[][];在game()函数中写入InitBoard()函数,这里我们可以把game()函数的内容放到另一个源文件game.c中,头文件部分我们可以创建一个game.h头文件,把需要用到的头文件和宏都放入game.h中,需要用到时在.c文件中引用#include "game.h"即可。
由于我们在game.c,game.h,study.c三个文件中都要调用库函数的头文件#include <stdio.h>,因此我们直接将 #include <stdio.h> 放在game.h文件中,同时我们使用宏定义,定义棋盘的行和列ROW 3,COL 3;在game.c和study.c文件中引用#include "game.h"(这里的#include <stdlib.h>和#include <time.h>头文件在生成随机数时要用到再做解释)
这里我们把InitBoard()函数的定义在game.c文件中完成,在study.c文件的game()函数中只需要声明即可。
(study.c文件中声明,传参为board,ROW,COL;ret变量后续判断输赢时用到,这里可以先不管)
声明完成,我们就需要在game.c文件中完成这个初始化棋盘的InitBoard()函数,思路是这样的,我们使用两个局部变量i和j,通过for循环对数组实现遍历,在每次遍历的时候把对应位置的符号位改为' '(空格)。这里的传参,就是传递想要打印棋盘的棋盘数组以及对应的行和列,我们分别定义一个数组board[][]和两个宏定义ROW和COL来进行传参,形参可以使用小写的row和col(当然也可以是自己给定的任意变量,看个人习惯咯)
void InitBoard(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] = ' ';
//初始化空棋盘(用' '空格替换数组内的元素)
}
}
}
这时我们初始化好了棋盘,可以把它打印出来看一看,但是打印棋盘在整个游戏中不会只进行一次,因此我们依旧写一个函数来实现棋盘打印,再次打印时只需要调用函数即可;这里我们写一个打印棋盘的函数,命名为DisplayBoard()。
(2)打印棋盘
同样的,我们通过两个局部变量i和j,利用for循环来实现遍历并将棋盘各位置依次打印出来。
DisplayBoard(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]);
}
printf("\n");
//在每行循环结束时换行
}
}
将打印棋盘的函数放到study.c文件的game()函数中,运行代码,我们会发现,由于初始化棋盘给定的初始化字符为' '(空格),这时打印出来的是一片空白,为了解决这个问题,我们应该给棋盘加上边界。
先给大家看一下加上边界后应该达到的效果:
可以看到我们应该使用"---"和'|'分别在棋盘对应棋位的下方和右方来将棋盘划分为九宫格;那么怎么实现呢,整体思路是这样的:我们需要在每次打印数组字符后打印一个'|'并在每行最后一次数组字符打印时,不打印'|',,对于数组字符下一行,我们则需要先打印一行"---"和'|',再进行数组字符的输出,同样的,在最后一个"---"打印后'|'不进行打印;同时,在最后一组数组字符打印完成时,不再打印边界线。因此我们可以写出这样的一串代码来优化打印棋盘这个功能:
void Display(char board[ROW][COL],int row,int col)
{
int i = 0;
for (i = 0; i < row; i++)//通过i控制行数
{
int j = 0;
for (j = 0; j < col; j++)//通过j控制列数
{
printf(" %c ", board[i][j]);
if (j < col - 1)//控制条件使最后一次数组字符打印后不打印'|'
printf("|");
}
printf("\n");//每行遍历完换行
if (i < row - 1)//控制条件使最后一行字符数组打印完后不再打印下边界线
{
int j = 0;//局部变量互不影响,可以再次使用j,上下两个j不同
for (j = 0; j < col; j++)//再次循环控制列数,使下边界线和右边界线打印
{
printf("---");
if (j < col - 1)//同样的保证最右边的右边界线不打印
printf("|");
}
printf("\n");//边界线打印完成再次换行
}
}
}
}
这样我们的打印棋盘功能也实现了。
(3)玩家下棋
玩家下棋部分的实现函数我们起名为PlayerMove(),传参仍为board,ROW,COL;在game()函数中完成声明,这里我们需要玩家和电脑以回合制的形式下棋,所以不妨设置一个while(1)死循环,等到游戏结束时跳出即可。在每次玩家下棋后打印棋盘让玩家看到落子位置,因此game()函数内声明PlayerMove(board,ROW,COL);game.c文件中定义PlayerMove()函数内容:
PlayerMove()函数中代码的思路:
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;//传入两个变量来接收玩家落子的坐标
printf("请输入落子坐标,中间用空格隔开:>");
while (1)//设置一个死循环,保证输入错误时可以回到循环内重新输入
{
scanf("%d %d", &x, &y);
if ((x >= 1 && x <= row) && (y >= 1 && y <= col))//判断玩家输入坐标是否在九宫格内
{
if (board[x - 1][y - 1] == ' ')//玩家输入的x,y为行数和列数,为了转变为数组下标
{ //需要用x-1和y-1
//判断落子位置是否是初始化状态,若不是则是重复落子
board[x - 1][y - 1] = '*'; //落子不重复时,将该位置改为'*'表示玩家落子
break; //跳出循环,进入下一步,让电脑落子
}
else
{
printf("坐标被占用,请重新输入坐标:>");
//若该位置不是初始化状态,则做出提示,并回到循环内重新输入
}
}
else
{
printf("输入坐标非法,请重新输入坐标:>");
//若该位置不在九宫格内,提醒玩家,并回到循环内重新输入
}
}
}
到这里玩家下棋就完成了,接下来就需要电脑跟玩家对下,并判断输赢了,我们先进行电脑下棋的程序编写。
(4)电脑下棋
要实现电脑下棋,最简单的方式就是通过随机数的生成来控制电脑落子的位置;因此这里我们在主函数main()中写入一个初始化随机数的函数srand((unsigned int)time(NULL)),至于这串函数的用法和原理,在这里我不做赘述了,大家感兴趣的话可以自己查一下百科或Cplusplus等网站,它起到一个使rand()函数的返回值为随机数的作用,在使用时我们需要在game.h头文件中引入#include <stdlib.h>和#include <time.h>,这里就解释了之前代码中的#include <stdlib.h>和#include <time.h>。
这样我们有了随机数rand()。接着我们对电脑下棋的函数命名为ComputerMove();传参仍为board,ROW,COL;
在study.c文件中game()函数内我们声明一下电脑下棋的函数,并在每次电脑下棋后进行棋盘打印,此时game()函数中的内容就是下面这样:
之后我们完成game.c文件中ComputerMove()函数的定义:
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;//同样的,使用x,y来对玩家指定的行和列进行接收
int y = 0;
printf("电脑下棋:>\n");//说明电脑下棋
while (1)
//设置一个死循环,当不满足循环结束条件时(即未遇到break),持续进行电脑下棋的动作直到完成下棋
{
x = rand() % row;//这里是一个简单的数学知识,即通过让随机值%行值,保证x的值为0~row-1
y = rand() % col;//同理,保证y的值为0~col-1
if (board[x][y] == ' ')//确保电脑落子的位置为初始化位置,避免电脑重复落子
{
board[x][y] = '#';//电脑落子用'#'来表示
break;
}
}
}
(5)判断棋盘是否填满
现在玩家下棋和电脑下棋都已经完成,接下来就该进行判断玩家和电脑的输赢了;在判断输赢之前我们要考虑到一种情况,就是电脑和玩家可能都没有赢,即当棋盘被填满的时候,出现了平局的情况,这种情况下我们就需要一个函数来判断棋盘是否已经被填满,我们给这个函数取名为IsFull()。由于IsFull是在判断是否输赢的函数中起作用的,因此我们只需要在game.c文件中定义即可,在study.c文件中是不会直接使用到的。在game.c文件中我们的IsFull函数可以这么写:
int IsFull(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++)//依旧是使用i和j对board数组进行遍历
{
if (board[i][j] == ' ')
//当没有初始化的位置,则说明棋盘被填满了,反之,则IsFull函数返回0
{
return 0;
}
}
}
(6)判断输赢
在判断完棋盘是否填满后,就到了我们的最后一步——判断玩家和电脑的胜负了,这里我们先提供一下判断胜负的思路:首先我们确定一下判断胜负函数的类型,这里我们不妨让函数返回值的类型为char类型,即每个胜负情况下,我们返回一个字符来表示这种胜负情况;接下来我们分析一下在判断胜负时一共会有几种可能的情况:第一种情况:玩家获胜;第二种情况:电脑获胜;第三种情况:平局,但是不要忘了,我们为了保证能够及时判断胜负,这里的判断胜负函数就要放在玩家下棋和电脑下棋之后;因此我们还要有一种在玩家和电脑还未决出胜负,并且也没有平局的时候游戏继续的情况;因此第四种情况就是:继续游戏;这样我们得到了判断胜负总共的四种情况;当玩家获胜时,我们不妨让玩家获胜用玩家的棋子'*'作为返回值进行判断;同样的,当电脑获胜时,我们不妨让带闹闹获胜用电脑的棋子'#'作为返回值进行判断;在平局时,我们取字符'Q'作为返回值进行判断;在未决出胜负时,我们取字符'C'作为返回值进行判断。这里我们仍然用board,ROW,COL作为传参进行函数书写,将判断胜负函数命名为IsWin(),IsWin()函数可以这样写:
char IsWin(char board[ROW][COL], int row, int col)
{
//1.赢(包括玩家赢和电脑赢):
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 < row; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
{ //把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 (IsFull(board, row, col) != 0)
//还记得IsFull()函数中我们设置的返回值return 0;吗?
//当遍历整个board数组发现有初始化位置' '(空格)时,我们返回0。
//也就是说在IsFull()函数返回值不是0的时候,表示整个board数组中没有初始化位置' '(空格)。
//也就是说这时整个board数组被玩家和电脑的棋子布满;
//并且双方没有决出胜负(如果决出胜负,会返回值,函数结束,进行不到当下这一步);
//这时我们返回一个字符'Q'来表示平局
{
return 'Q';
}
//继续
return 'C';
//如果说上面的代码都没有满足条件,也就是说既没有出现获胜方,棋盘也没有被棋子布满,这时我们返回字符'C'表明游戏应该继续进行
}
(整个函数主体仍放到game.c文件中完成,在study.c文件中调用)
这样我们就实现了对游戏胜负的判断,接下来最后一步就是在study.c文件中对game()函数进行完善了,最终我们在study.c文件中的game()函数部分的代码应该是下面这样:
void game()
{
char ret = 0;//定义一个char类型的变量接收IsWin()函数的返回值方便之后的判断
//初始化棋盘
InitBoard(board, ROW, COL);
//打印棋盘
DisplayBoard(board, ROW, COL);
while (1)
//这里设置一个死循环,确保在IsWin()函数返回值为'C'(即表明游戏应该继续进行时)
//使玩家和电脑继续依次下棋
{
//玩家下棋
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
//判断输赢
//这里需要注意的是,在玩家和电脑每次下棋后,都要进行一次胜负判断
//避免在另一方下棋后才判断出胜负
ret = IsWin(board, ROW, COL);
if (ret != 'C')
break;
//电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
//判断输赢
ret = IsWin(board, ROW, COL);
//在每次下棋后ret都需要再次赋值,否则下完棋无法立即判断输赢
if (ret != 'C')
break;
}
//当ret != 'C'时,对胜负情况中的前三种进行正常判断,并打印出结果即可。
if (ret == '*')
printf("玩家赢\n");
else if (ret == '#')
printf("电脑赢\n");
else
printf("平局\n");
}
到这里大家应该明白之前出现的char类型ret变量的作用,即接收IsWin()函数的字符返回值,并通过条件语句打印出胜负结果。
四、总结
这样我们的三子棋代码就算是全部完成了,大家可以试着运行自己写出的代码,如果有bug出现可以对照总结中的完整代码部分参考各步骤的说明自行检查一下,本期代码到这里就结束了。
下面附上这个三子棋游戏的完整代码:
1.game.h头文件部分:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 3
#define COL 3
//初始化棋盘
void InitBoard(char board[ROW][COL], int row, int col);
//打印棋盘
void DisplayBoard(char board[ROW][COL], int row, int col);
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col);
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col);
//判断胜负
char IsWin(char board[ROW][COL], int row, int col);
2.game.c文件部分:
#include "game.h"
//初始化空棋盘(用' '空格替换数组内的元素)
void InitBoard(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] = ' ';
}
}
}
void DisplayBoard(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)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
}
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("请输入落子坐标,中间用空格隔开:>");
while (1)
{
scanf("%d %d", &x, &y);
if ((x >= 1 && x <= row) && (y >= 1 && y <= col))
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("坐标被占用,请重新输入坐标:>");
}
}
else
{
printf("输入坐标非法,请重新输入坐标:>");
}
}
}
void ComputerMove(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;
}
}
}
int IsFull(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++)
{
if (board[i][j] == ' ')
{
return 0;
}
}
}
}
//1.玩家赢 - '*'
//2.电脑赢 - '#'
//3.平局 - 'Q'
//4.继续 - 'C'
char IsWin(char board[ROW][COL], int row, int col)
{
//1.赢(包括玩家赢和电脑赢):
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 < row; 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 (IsFull(board, row, col) != 0)
{
return 'Q';
}
//继续
return 'C';
}
3.study.c文件部分:
#include "game.h"
char board[ROW][COL];
void menu()
{
printf("******************************\n");
printf("********** 1. play ***********\n");
printf("********** 0. exit ***********\n");
printf("******************************\n");
}
void game()
{
char ret = 0;
//初始化棋盘
InitBoard(board, ROW, COL);
//打印棋盘
DisplayBoard(board, ROW, COL);
while (1)
{
//玩家下棋
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
break;
//电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);
//判断输赢
ret = IsWin(board, ROW, COL);//ret 需要再次赋值,否则电脑下完棋无法立即判断输赢
if (ret != 'C')
break;
}
if (ret == '*')
printf("玩家赢\n");
else if (ret == '#')
printf("电脑赢\n");
else
printf("平局\n");
}
int main()
{
int input = 0;
srand((unsigned int) time(NULL));
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}