讲在前面:
个人认为:最耗时间也是最值得一看的是判断函数部分
本文为新手自己琢磨debug若干天后终于写出的一版,原是看@鹏哥C语言 课程时写的,但是里面对于获胜条件的判断仅仅适用于3×3的井字棋,且网上也没有很好的代码,于是在兴趣驱动下,自己写了一个一百多行的判断函数,适用于任意大棋盘中任意数目(即n子棋)的获胜判断
可能有很多可以优化的地方,如有错误还请指出纠正,十分感谢!
一、整体总览
我们先做如下规定
ROW——代表行数;
COL——代表列数;
WIN——代表赢需要连成线的子的数目;
' * '——代表玩家所下的棋子;
' # '——代表电脑所下的棋子;
我们不妨在头文件中直接定义
#define ROW 3 #define COL 3 #define WIN 3
以后想要拓展为n子棋,只需在此修改三个数值即可
对于一个N*N的棋盘来说,最好的方式就是用二维数组来创建
现就可以在游戏主体函数中创建一个二维数组
char board[ROW][COL];
再来看这个游戏应有的部分
一、菜单
二、游戏函数主体
- 打印棋盘(&&初始化)
- 执子操作
- 判断输赢
//初始化棋盘 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 Judge(char board[ROW][COL], int row, int col);
(↑我的game.h头文件最终效果)(↓我的game()函数总体效果)
void game() { //创建变量 char ret = ' '; char board[ROW][COL] = { 0 }; //战前准备 InitBoard(board, ROW, COL); DisplayBoard(board, ROW, COL); while (1) //循环下棋 { PlayerMove(board, ROW, COL);//玩家下棋 DisplayBoard(board, ROW, COL);//展示棋盘 ret = Judge(board, ROW, COL);//得到返回值 //if (ret == '*') 判断是否结束 //……………… ComputerMove(board, ROW, COL);//电脑下棋 DisplayBoard(board, ROW, COL);//展示棋盘 ret = Judge(board, ROW, COL);//得到返回值 //if (ret == '*') 判断是否结束 //……………… } }
接下来就是分步实现这些步骤
二、内容实现
1.菜单
这部分没什么好说的,创建一个menu()函数即可
void menu() { printf("****************三子棋****************\n"); printf("**************************************\n"); printf("***************1. 开始***************\n"); printf("**************************************\n"); printf("***************0. 退出***************\n"); printf("**************************************\n"); printf("请输入数字进行下一步操作:>"); }
这种1开始、0退出的做法主要是方便快速地进行or退出循环
我们可以在main() 函数里写一个循环,方便一轮结束后继续下一轮
int main() { int input = 0; do { menu(); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("\n\n已退出\n\n"); break; default: printf("\n\n输入错误,请重新输入\n\n"); break; } } while (input); return 0; }
(上来先干事再判断适合用do while循环)
根据写一点测试一点的原则,可以先在case 1中把game()换成printf(),看是否出现问题
一般而言,这一步没什么难度,不过后期可以接着嵌套switch()将游戏丰富为可以选择人机or双人、井字棋or五子棋
2.打印棋盘(&&初始化)
在打印之前应该先初始化棋盘,即让board[ROW][COL]数组内所有元素均为空格‘ ’,用两层for循环即可轻松实现
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] = ' '; } }
再来看一下3 * 3棋盘的预览图
为了图省事当然可以一行一行把九个格子都打印下来,但别忘了我们是为了能够推广到任意大小的n子棋,所以最好是利用两个for()循环语句逐个打印
‘ * ’处就是我们应该下的棋,可以看出来,一般而言,比较规矩的棋盘的一小格应该是
“空格 %c 空格|”,下一行是“---|”
但是仍有特殊的两处:
- 最后一行没有换行后的“---|”;
- 最右边一列没有“|”,取而代之的应该是换行
所以我们可以写出如下的代码 :
void DisplayBoard(char board[ROW][COL], int row, int col)
{
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j != row - 1)
printf("|");
}//括号中是一行的东西
printf("\n");//这一句表示下打印完一行、在打印下一行前换行
if (i != row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j != col - 1)
printf("|");
}
printf("\n");
}
}
printf("\n\n");//单纯想让打印出来的排版更好看
}
当然,在后续如果想改成更大的棋盘时,繁多的格子会让人眼花缭乱,最好是对这一部分进行优化
可以在最上面一行就进行一个for循环打印列数,然后再在每一行开头再进行打印行数
void DisplayBoard(char board[ROW][COL], int row, int col) { //补充:打印列数 int i = 0; printf(" ");//因每一行开头多打一个数字,多打出一个空格进行对齐 for (i = 0; i < row; i++) { if(i < 10) printf(" %d ", i + 1); if(i >= 10)//当i是两位数时,空格需要少打一个来保证不错位 printf(" %d ", i + 1); } printf("\n"); //开始打印棋盘 for (i = 0; i < row; i++) { int j = 0; for (j = 0; j < col; j++) { if (j == 0)//在每一行的第一列打印行数 printf("%2d %c ", i+1, board[i][j]); //选用%2d是为了防止行数大于10后出现错位,故1~9都用“ i”表示 if (j != 0) printf(" %c ", board[i][j]); if (j != row - 1) printf("|"); } printf("\n"); if (i != row - 1) { for (j = 0; j < col; j++) { if (j == 0)//同理,在每一行的第一个---前打印两个空格,防止错位 printf(" ---"); if (j != 0) printf("---"); if (j != col - 1) printf("|"); } printf("\n"); } } printf("\n\n"); }
里面的一些细节都在注释里注明了
这样优化后的DisplayBoard()就完成了,在15*15的棋盘里下五子棋比原来容易了许多
3.下棋操作
①玩家执子
首先要声明的一点是,玩家不是程序员,所以棋盘的开始不是(0,0),而是(1,1),所以要注意玩家输入的坐标值都要 -1
逻辑很简单,玩家输入坐标,将这个坐标对应的‘ ’改为‘ * ’
同时注意对应坐标不是空格的、超出范围的都要报错
void PlayerMove(char board[ROW][COL], int row, int col) { printf("轮到玩家下棋\n"); printf("请输入坐标:>\n"); while(1) { int x = 0; int y = 0; scanf("%d %d", &x, &y); if (x > row || y > col || x < 1 || y < 1) printf("坐标超出范围,请重新输入\n\n"); else if (board[x - 1][y - 1] != ' ') printf("该处已有棋子,请重新输入\n\n"); else { board[x - 1][y - 1] = '*'; break; } } }
十分简单
②电脑执子
目前我的技术力不够……只能完成随机下棋,等我学到算法再来考虑优化出更加智能的电脑
对于随机函数,需要先在main()函数里面整一个
#include <time.h> srand((unsigned int)time(NULL))
接下来就是在我们的ComputeMove()里调用rand()函数了
大体上基本与PlayerMove()一致,但是电脑输错了不需要报错,默默地执行到下一步即可
void ComputerMove(char board[ROW][COL], int row, int col) { printf("轮到电脑下棋\n\n"); Sleep(300);//慢一点点,让人也有反应的时间 while (1) { int x = rand() % row; int y = rand() % col;//这里对row、col取余,可以得到0~row - 1的数,完美符合我们的预期 if (board[x][y] == ' ') { board[x][y] = '#'; break; } } }
总而言之,这两个步骤操作起来算是最简单的了
4.判断函数
这一部分是耗费精力最多、但又是整个棋局中相当关键的一步,我将会使用函数
char Judge(char board[ROW][COL], int row, int col)
用char类型返回值,从返回结果上分为四种情况
- 返回' * ',代表玩家获胜;
- 返回' # ',代表电脑获胜;
- 返回'C',代表无事发生,进行下一步执子;(不过我后续也没有用到这个C)
- 返回'Q',代表平局;
除此之外,还应考虑函数内部分为三大模块:
- 判断行、列;
- 判断斜线;
- 判断平局or继续;
①判断行、列
首先讲清楚判断是否多子连成一条连续的直线的逻辑
开始先用两个for()循环嵌套,方便在判断完一个格子之后,接下来对所有的格子重复判断操作,直到找到多子连成线的情况
//判断行 int i1 = 0; int count_row = 0; for (i1 = 0; i1 < row; i1++) { int j1 = 0; for (j1 = 0; j1 < col; j1++) { if (board[i1][j1] != ' ' && j1 < col - 1)//注意j的范围,防范数据溢出!!! { if (board[i1][j1] != board[i1][j1 + 1]) count_row = 0;//如果中间有一次连不上,就要把计数器归零 else count_row++;//能连上就把计数器+1 if (count_row == WIN - 1) return board[i1][j1];//可以得到连成线的棋子类型 } } count_row = 0;//注意每一行循环结束之后要把计数器归零 }
一定一定要注意i、j在行、列、斜线的判断中都是有范围限定的!防范数据溢出!否则会出现跨行、跨列连成线的bug!
列与行是同理,只是改一改i、j的位置和变量名称罢了
//判断列 int j2 = 0; int count_col = 0; for (j2 = 0;j2 < col; j2++) { int i2 = 0; for (i2 = 0; i2 < row; i2++) { if (board[i2][j2] != ' ' && i2 < row - 1) { if (board[i2][j2] != board[i2 + 1][j2]) count_col = 0; else count_col++; if (count_col == WIN - 1) return board[i2][j2]; } } count_col = 0; }
②判断斜线☆☆☆
与别的只能死板地判断[i][i] == [i+1][i+1]的代码不同,我经过两三天思考修改的这版代码可以判断在任意区域连成的斜线,因此可以实现推广到五子棋乃至n子棋(自夸一下,因为新手自己想并写出来后真的很自豪)
首先还是强调注意i、j的取值范围,否则会出现跨行、跨列、首尾连线的bug
原理依旧是采取计数器,board[a][b] == board[c][d]且不为空格就计数器+1,当count == WIN - 1时就代表获胜
注意:每次处在一个循环结束,和下一个循环开始之间,就代表着没有一方获胜,所以要把计数器清零
余下的讲解放在代码的注释中自行体会
//判断斜 int i3 = 0; int count_dia1 = 0;//计数器1 int count_dia2 = 0;//计数器2 //左上右下 for (i3 = 0; i3 < row; i3++) { int j3 = 0; for (j3 = 0; j3 < col; j3++) if (board[i3][j3] != ' ') { count_dia1 = 0;//每次循环开始前要把计数归零 int x = i3; int y = j3; //这里选择使用x、y来代替i、j是怕后续+1的操作影响到了i、j循环的进行 while (x < row -1 && y < col - 1) //注意到后面的判断有[x+1][y+1],故x、y不能是代表最后一行、列的,防数据溢出 { if (board[x][y] != ' ') { if (board[x][y] != board[x + 1][y + 1]) count_dia1 = 0;//与判断行列同理,中间有断就计数器归零 else count_dia1++;//连续两格相等,则计数器+1 if (count_dia1 == WIN - 1) return board[x][y];//得到连成线的棋子类型 } if (board[x][y] == ' ') count_dia1 = 0; x++; y++; //在while循环中重复运行,从一开始的board[i][j]一路找下去 //直到找不到再退出,进入到i、j的循环,从下一个格子开始重新用while循环开始找 } } } //左下右上 int i5 = 0; for (i5 = 0; i5 < row; i5++) { int j5 = 0; for (j5 = 0; j5 < col; j5++) if (board[i5][j5] != ' ') { count_dia2 = 0;//每次循环开始前计数器归零 int x = i5; int y = j5; while (x < row - 1 && y > 0) //原理和上面一样,因为[x+1][y-1],所以不能是最后一行或者第一列的 { if(board[x][y] != 0) { if (board[x][y] != board[x + 1][y - 1]) count_dia2 = 0; else count_dia2++; if (count_dia2 == WIN - 1) return board[i5][j5]; } if (board[x][y] == ' ') count_dia2 = 0; x++; y--; } } }
毕竟是新手,写成这样自己已经很满意了,但嵌套了好多循环和条件语句,不知能否优化优化
③判断平局or继续
当判断函数从上到下一直进行到这里的时候,就代表着还没有人胜出,那么就需要判断是平局了or还能接着下
原理依旧简单,两个for循环逐个判断即可
//判断平局 int i4 = 0; for (i4 = 0; i4 < row; i4++) { int j4 = 0; for (j4 = 0; j4 < col; j4++) { if (board[i4][j4] == ' ')//如果检测到有任意空位,就还可以接着下 return 'C'; } } return 'Q';//到最后了发现实在没有空位,只得返回'Q',等待后续操作使其退出
小结:game()函数总览
自此,Jugde()函数内部已经全部完成,接下来只需要再回到main()函数中将这些东西合理摆放、写好循环和判断即可
void game_pvc()
{
char ret = ' ';//用来存储Judge()的返回值
char board[ROW][COL] = { 0 };
InitBoard(board, ROW, COL);
DisplayBoard(board, ROW, COL);
while (1)
{
PlayerMove(board, ROW, COL);//玩家执子
DisplayBoard(board, ROW, COL);//打印棋盘
ret = Judge(board, ROW, COL);
//判断输赢平
if (ret == '*')
{
printf("玩家获胜\n\n");
Sleep(1200);
break;
}
if (ret == '#')
{
printf("电脑获胜\n\n");
Sleep(1200);
break;
}
if (ret == 'Q')
{
printf("平局\n\n");
Sleep(1200);
break;
}
//下面是如法炮制
ComputerMove(board, ROW, COL);//电脑执子
DisplayBoard(board, ROW, COL);//打印棋盘
ret = Judge(board, ROW, COL);
//判断输赢平
if (ret == '*')
{
printf("玩家获胜\n\n");
Sleep(1200);
break;
}
if (ret == '#')
{
printf("电脑获胜\n\n");
Sleep(1200);
break;
}
if (ret == 'Q')
{
printf("平局\n\n");
Sleep(1200);
break;
}
}
}
这个游戏总体上就算完成啦!后续可以自行增加双人对战、人机大战、三五选择等功能
三、结语
能耐心看到最后或者看完我最煎熬的判断部分的,真的十分感谢,虽然想出一套逻辑、敲代码、断点调试很累,但是最后程序成功跑起来的时候还是成就感满满的
愿与诸位共勉,共同进步