目录
前言:
开门见山,第一次写这种博客,可能语言没有那么的华丽,但是,本篇对该程序的重点部分讲解详略得当,有许多部分我认为还是很值得一看的,相比大家基础这方面都应该会,所以基础部分比较简略一些。
有很多细节都写在注释里了,在正文有可能没有涉及到,希望大家能够认真看看代码注释!!!
首先,我是对该程序进行分文件和分函数写的,这样能够增强我们的条理性和阅读性也方便我们进行一些调试和修改。
相比大家应该对这有所了解,本文为了节约您的时间,就不在这里一一提及了。
好,废话不多说,直接上正文。
何为三子棋:
三子棋就是同一方向上所有元素保持一致即保持胜利!
三子棋思路:
1.制作一个菜单,让玩家进行选择
2.对棋盘进行初始化,再进行打印
3.玩家移动,电脑移动
4.判断输赢
5.胜负已分,返回步骤,重新游戏
这是我封装的一些各个功能的函数
//显示菜单
void Display_Menu(void);
//初始化棋盘
void Init_Board(void);
//显示棋盘
void Disp_Board(void);
//运行游戏
void Run_Game(void);
//玩家移动
void Player_Move(void);
//电脑移动
void Computer_Move(void);
//判断输赢
char Is_Win(void);
基本功能:
显示菜单:
//显示菜单
void Display_Menu()
{
printf("******************************\n");
printf("******** 1.player **********\n");
printf("******** 0.exit **********\n");
printf("******************************\n");
}
main函数:
int main()
{
int choose = 1;
Display_Menu();
printf("Your choose is:");
scanf("%d",&choose);
do
{
switch(choose)
{
case 1:
Run_Game(); scanf("%d",&choose); break;
default:
printf("Your choose is error , please afresh choose!!!\n");
printf("Your choose is:");
scanf("%d",&choose); break;
}
} while (choose);
printf("----------------Games Over----------------\n");
system("pause");
return 0;
}
首先,要对定义一个数组
#define ROW 3
#define COL 3
char board[ROW][COL] = {'\0'};
这里行坐标和列坐标尽量采用这种宏定义的方式,方便后期的修改,就不至于当想玩五子祺时而一个个去修改。
我们在定义完数组后首先应该对初始化吧,在游戏还没有玩之前棋盘上应该是空荡荡的,没有任何的下棋痕迹吧,所以我们应该对数组都初始化为空格
数组初始化:
//初始化棋盘
void Init_Board()
{
int i = 0;
int j = 0;
for(i = 0; i < ROW; i++)
for(j = 0; j < COL; j++)
board[i][j] = ' ';
}
注:这里有些人可能把 空格 和 '\0' 混淆 当数组存入'\0'输出时是完全没有任何信息的也不会占一个字符位
假设我们不给它初始化,那当我们输入坐标后再显示时那会不会突然冒出一个字符且把整个棋盘的布局打乱呢,所以这里必须初始化且必须为空格 ’ ‘(当然你要是硬刚那也有办法,但很麻烦,且代码可读性很差)
正如左图的显示效果。那如何达到这样效果呢?该如何显示横线(-)和下划线(|)呢?
我们可以直接利用这种输出格式实现:
printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
printf("---|---|---\n");
但是这种输出格式是不是一点也不优雅!永远固定住了只有三列,那我们要玩五子棋时候能也要一个个改嘛,我们尽然用宏定义去定义它的行坐标和列坐标,就是想在我们想切换成5子祺时能够简单有效的切换。
打印棋盘:
//显示棋盘
void Disp_Board()
{
system("cls"); //清屏-->方便每次输入时更好的查看棋盘,防止刷屏
int i = 0;
int 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"); //换行
for(j = 0; j < COL; j++) //控制循环的列数
{
printf("---");
if(j < COL - 1)
printf("|");
}
printf("\n"); //换行
}
}
这里我们行和列下标都从0开始知道我们宏定义的行和列是不是就能将想要的行列都输出出来呢还能随意切换任何想要的板子大小
那又可能有朋友疑问了,那上述代码中的这两句有什么用呢?
if(j < COL - 1)
printf("|");
那如果我们要是没有这句它会不会在最末端再打印一个竖线(|)呢,这对于强迫症玩家来说是极其难受的,所以我们要在最后少打印一个竖线(|)
好,基本的问题解决完了,我们来看游戏的主体部分
主体大致思路是不是就是 玩家 下一次棋判断一下输赢,电脑 下一次棋判断一下输赢,知道胜负或平局分出来时就结束
游戏主体:
//运行游戏
void Run_Game()
{
char tag = ' '; //临时变量-->用来储存 判断输赢 返回的信息
Init_Board(); //初始化棋盘
while(1)
{
Disp_Board(); //显示棋盘
printf("Player move:\n"); //提示信息
Player_Move(); //玩家移动
tag = Is_Win(); //判断输赢
if(tag != 'c') //如果返回的不是 c 就说明已经得到了谁输谁赢的结果 就直接结束
break;
Computer_Move(); //电脑移动
tag = Is_Win(); //判断输赢
if(tag != 'c') //如果返回的不是 c 就说明已经得到了谁输谁赢的结果 就直接结束
break;
}
Disp_Board(); //显示棋盘
switch(tag) //当循环结束时,就已经分出胜负,打印提示信息
{
case '#': //玩家胜利
printf("Congratulations,you have won!\n"); break;
case '*': //电脑胜利
printf("Unfortunately,you lost !\n"); break;
case 'q': //平局
printf("Draw !\n");
}
printf("Do you want to continue ?\n");
printf("*********** 1.continue 0.break ***********\n");
printf("Your choose is:");
}
这里把他们写在循环里,直到分出胜负时该程序才结束,否则一直进行
这里还有就是当无论是玩家下完棋后还是电脑下完棋后都应该随即分别判断一下输赢,谁也不知道谁能赢,如果不随即判断的话,极可能胜利的信号会滞后,当然,显示棋盘也应该放在循环里,每次输入完后打印新的棋盘,方便查看
最后结束时,也可以打印上提示信息,方便让玩家再进行游戏。
大致流程就是这样,我们重点要的是看 玩家移动部分 和 电脑移动部分 和 最关键的判断输赢部分
重点部分:
玩家移动:
一共有三种情况:
1:坐标超出棋盘范围,坐标不合法
2:输入的坐标已下棋,坐标被占用
3:以上两种情况都不满足,坐标合法
具体可以看下面代码-->看如何判断的合法性
//玩家移动
void Player_Move()
{
int X = 0;
int Y = 0;
printf("Please input your coordinate -->");
scanf("%d%d",&X,&Y);
while(1) //循环方便输入错误时重新输入
{
//当 该位置不为 ' ' 时只有两种情况 --> 1.坐标被占用 2.坐标超出范围
//此时这两种情况都是不符合题意的可以放在一块写
if( board[X - 1][Y - 1] != ' ')
{
if(X < 0 || X > ROW || Y < 0 || Y > COL) //坐标超出范围时
printf("Your coordinate error!\n");
else
printf("coordinate occupied!!!\n"); //剩下的这个只能是坐标被占用
printf("Please afresh input -->"); //提示重新输入
scanf("%d%d",&X,&Y);
}
else //不满足以上情况就说明该坐标输入正确
{
board[X - 1][Y - 1] = '#'; //给该坐标赋值,循环结束
break;
}
}
}
注意:非程序员玩家大都没有0坐标这个概念,我们这里的(0,0)玩家在输入的时候会输入(1,1)所以在用数组board接收位置信息时,需要将x,y进行减一处理即board[X - 1][Y - 1]进行判断!!!
电脑移动:
电脑移动是不是随机性的啊,所以这里用到了 产生随机数 的 rand()函数 以防它每次产生的时间起始点一致,我们还用到了 srand()函数 以及它们的头文件 <time.h> 和 <stdlib.h>
//电脑移动
void Computer_Move()
{
srand((unsigned int)time(NULL)); //确保每次产生的时间起始点不一样
while(1) //已防随机产生的坐标错误
{
int X = rand() % ROW; //0 -- ROW - 1
int Y = rand() % COL; //0 -- COL - 1
if(board[X][Y] == ' ')
{
board[X][Y] = '*';
break;
}
}
}
rand()函数是随机产生0--32767的数,假设当我们模除 3 时此时出来的数最大为 2 最小为 0 范围也就是0--2,刚好是我们下标的范围,此时我们可以通过rand()函数来得到我们想要范围的下标直接拿过来用
这里注意的是:我们使用rand()函数产生的是随机的坐标,即使我们控制了坐标的范围,但是也有可能该坐标被占用!
所以,我们这里应该对其判断一下,只有当该位置没有任何信息时才对它进行正确的赋值,这里也应该用循环,一旦正确赋值,循环结束!
判断输赢:
终于来到最关键的部分了,判断输赢!
首先,先看有几种赢法:
1. 行(直线)全部相等
2. 列(竖线)全部相等
3. 对角线 全部相等
3.1 自西北到东南
3.2 自东北到西南
以及几种结局:
1. 玩家胜利
2. 电脑胜利
3. 平局
4.以上都不满足,继续游戏
胜负或平局分出返回结果值:
1. 玩家赢返回’#‘。
2. 电脑赢返回’*‘。
3. 平局返回’q’。
4. 棋盘没满,电脑和玩家没赢,返回’c’,继续游戏
//判断输赢
char Is_Win()
{
int i = 0;
int j = 0;
int flag = 0; //判断 (行 or 列) 是否保持一致的标志
char tag = ' '; //临时变量-->用作储存(行 or 列)起始位置的信息
//判断 行 是否全部相等
for(i = 0; i < ROW; i++)
{
if(board[i][0] != ' ') //先判断该起始位置是否不为 ' ' 然后才进行判断 大大减少了资源浪费
{
flag = 1;
tag = board[i][0];
for(j = 1; j < COL; j++) //如果从 0 开始相当于起始位置和起始位置判断(多判断了一次)所以从1开始
if(tag != board[i][j]) //当有一个不一样时就直接结束本轮循环
{flag = 0;break;}
}
if(flag == 1) //如果循环结束flang为1则该行始终保持一致
return tag;
}
//判断 列 是否全部相等
for(j = 0; j < COL; j++)
{
if(board[0][j] != ' ') //先判断该起始位置是否不为 ' ' 然后才进行判断 大大减少了资源浪费
{
flag = 1;
tag = board[0][j];
for(i = 1; i < ROW; i++) //如果从 0 开始相当于起始位置和起始位置判断(多判断了一次)所以从1开始
if(tag != board[i][j]) //当有一个不一样时就直接结束本轮循环
{flag = 0;break;}
}
if(flag == 1) //如果循环结束flang为1则该行始终保持一致
return tag;
}
//判断 对角线 是否相等
if(ROW == COL) //首先判断它是否有对角线 如果行和列不相等就没有对角线这种情况了
{
//对角线 1 :
if(board[0][0] != ' ')
{
flag = 1;
tag = board[0][0];
for(i = 1; i < ROW; i++) //因为此时 行和列 是一样的所以这里用 ROW 或 COL 都行
if(tag != board[i][i])
{flag = 0;break;}
if(flag == 1)
return tag;
}
//对角线 2 :
if(board[0][COL -1] != ' ')
{
flag = 1;
tag = board[0][COL - 1];
for(j = 1; j < COL; j++) //因为此时 行和列 是一样的所以这里用 ROW 或 COL 都行
if(tag != board[j][COL - j - 1])
{flag = 0;break;}
if(flag == 1)
return tag;
}
}
//判断是否平局(棋盘占满)
for(i = 0; i < ROW; i++)
{
for(j = 0; j < COL; j++)
{
if(board[i][j] == ' ') //当有一个为 空格 时就直接返回 --> 继续
return 'c';
}
}
return 'q'; //这个判断是否平局是放在最后的 所以只有上述所有情况都不满足后才会走到这一步
//此时只有 平局 或 不是平局继续 两种情况
}
这里说明:我们应该分别对各行各列都进行判断,用一个临时变量去存储起始位置的信息,用剩余两个元素判断是否保持一致
只要有一条情况不满足时立即结束,开始下一轮循环,并且在这里还应该保证它不为空格 ’ ‘
找到满足条件的一条信息后,将此时的元素信号返回过去进行判断
小提示:可以将较难的可能放到后面,以便减少资源浪费
这里也尽量用循环的方式涵盖所有情况,方便修改而不是单一的固定模板!
最后本次游戏结束后也可以再次将打印信息打印出来,再次让玩家继续游戏,详情可以看看我上面的程序代码!
总结(建议看):
https://github.com/HackerActivists/Tic-Tac-Toe.git
这个链接是我的源码,大家可以进去看看,看看源程序也能再次捋顺自己的思路!
非常感谢大家能够看到这里,以上,是我对该程序的见解,有很多细节的东西都写在了注释里,大家尽量详细看看注释!!!
这个程序写完基本功能后200来行,但只要思路清晰,写起来还不是很难的事,大家尽量分程序块设计这样条理能很清晰,也方便找错。
建议大家能用大一统的写法写代码,就别用单一性的写法,不便于修改,就跟我这个程序一样只需要修改一个宏定义就还能在保持功能不变的情况得到我的目的!
还有就是,我这个定义数组变量是为了懒,才这样写的,定义了个全局变量,正常情况尽量传入参数地址!!!
这个只是基本功能,代码都是人写出来的,你也可能再在此基础上附加你想要的功能,使其更加达到你心中的标准,可以加上一些算法,使计算机更加聪明一些,这些都有待你开发!!!
如果有什么更好的建议也欢迎大家积极讨论,我们相互学习,共同进步!!!
最后,祝愿看到这篇文章的人编程能力大涨!!!