相信大部分让小时候都玩过三子棋(井字棋),这是我们小时候的回忆,那我们如何用C语言去实现它呢?跟着我往下看。
功能实现
1.打印开始菜单
voidinter() { printf("***********************\n"); printf("***** 开始:1 *****\n"); printf("***** 退出:0 *****\n"); printf("***********************\n"); } voidinter1() { printf("***********************\n"); printf("***** 单人:1 *****\n"); printf("***** 双人:2 *****\n"); printf("***********************\n"); }
2.实现选择
我们打印好菜单,怎么去实现选择1和0去决定是退出还是开始呢?那我们是不是要先定义一个变量,然后输入一个数,在使用if语句或者switch语句去选择,然后我们玩游戏不可能只玩一次,并且开始菜单要最少打印一次,那这里就用do while循环语句去实现,将do while语句和if语句或者switch语句去嵌套,就能实现选择的功能,那选择能实现了,可是菜单里面就只有1和0啊,要是我们输入别的数,没有这个选项怎么办?这里用我们的else(if)或者default(switch),当我们输入错误的时候,提醒我们输入错误。
voidtest() { inti=0; do { inter(); printf("请选择:"); scanf_s("%d", &i); if (i==1) { printf("开始游戏\n"); } elseif (i==0) { printf("退出游戏\n"); } else { printf("选择错误重新选择\n"); } } while (i); }
那现在就有人疑问,为什么进循环的条件是i呢?我们看上面菜单,1开始,0退出,在计算机里面,非0为真,所以当我们输入0的时候为假,就不会在进去do while循环语句里面。
3.游戏实现
(1)初始化棋盘
我们要下棋,下棋要落子,怎么才能让我们落子和没有落子的格子有区别呢?那这里我们就先定义一个字符类型的二维数组,那现在就有人问,为什么要定义二维数组,还必须是字符类型?我们的棋盘是一个横三竖三的棋盘,那横三竖三,要是用一维数组就需要三个三个元素的一维数组,那不如直接定义一个二维数组,那为什么要用char?我们要让棋盘每个格子都看起来是空的,就需要让里面原来存的是空格,空格是字符,所以用char,那数组定义好了,和我们的初始化棋盘有什么关系?初始化棋盘,是为了让我们每一步落子能被看见,那所谓的落子是什么?其实无非就是将”子“落到数组里面,所以我们要初始化棋盘,也就是初始化数组,那怎么初始化数组?这里分装成一个自定义函数去实现,我们是个二维数组,所以要有两个参数(行和列)去初始化,然后用for循环或者while循环去将数组没一个元素初始化成空格就行。
voidinit_chess(charchess[ROW ][LIN], introw, intlin) { inti=0; for (i=0; i<ROW; i++) { intj=0; for (j=0; j<LIN; j++) { chess[i][j] =' '; } } }
这里就有人好奇ROW和LIN是什么?三子棋总归会玩腻,那玩腻三子棋,我们想换五子棋,怎么办呢?总不能在码一遍吧,或者把代码里面所有3改成5?这样工作量会不会太大,以后想改回来又会又要改一大堆,那我们是不是可以在game.h里面声明两个常量,一个是ROW(行)一个是LIN(列)。
//行//列
那这样我们要改,只用改这里就全都改了。
(2)打印棋盘
我们要打印出一个棋盘,要想一个棋盘是个什么样子。
棋盘这个样子,一眼过去,有三行格子,两行分割线,那我们怎么打印呢?我们在看,格子行是由空格和 | 组成的,但是 | 到最后面的一个没有打印,所以要判断,那分割线行就是由---和 | 组成,但是也只有两行,所以也要判断,那判断条件是什么呢?我们在来看看5×5的棋盘打印出来什么样子
3×3的棋盘有两行分割线两个 | ,5×5的有四行分割线四个 | ,所以得到结论,打印分割线和 | 的个数是ROW-1和LIN-1,那有了条件我们就可以开始打印了。
voidprint_chess(charchess[ROW][LIN], introw, intlin) { inti=0; for (i=0; i<ROW; i++) { intj=0; for (j=0; j<LIN; j++) { printf(" %c ", chess[i][j]); if (j< (LIN-1)) { printf("|"); } } printf("\n"); if (i< (ROW-1)) { for (j=0; j<LIN; j++) { printf("---"); if (j< (LIN-1)) { printf("|"); } } printf("\n"); } } }
(3)玩家下棋
我们作为一个学过计算机的知道,数组下标从0开始,但是普通玩家不知道,这要怎么去解决呢?我们想,普通玩家觉得从1开始,而下标从0开始,那我们把玩家输入的数字减一在传给数组,是不是就和计算机想的一样了,但是我们还需要判断我们的坐标是否合法,如果不合法,就告诉我们坐标不合法,然后重新选择坐标;因为我们的是三子棋,所以我们的坐标应该是横最大三,纵最大三;然后坐标合法之后,我们就要判断所选坐标是否为空格,如果坐标上的对应的不是空格,已经有‘棋子’,就需要告诉我们该坐标已被占用,然后重新选择;要怎么去实现呢?我们要重新选择,那就需要用循环,因为可能我们会多次输入错误,所以条件要让循环变成死循环,然后到我们‘落子’之后跳出去,想要落子又需要判定坐标,所以需要用到if语言,到落子之后,直接跳出去,我们也不知道棋盘现在的情况,所以在落子跳出去之前,要接着调用一次打印棋盘的函数,然后跳出去,那以上都满足我们的玩家下棋就实现了.。
voidis_people(charchess[ROW][LIN], introw, intlin,charstr) { inti=0; intj=0; while (1) { printf("请选择落子位置:"); scanf_s("%d%d", &i, &j); if (i>0&&i<=ROW&&j>0&&j<=LIN) { if (chess[i-1][j-1] ==' ') { chess[(i-1)][j-1] =str; } else { printf("该坐标已经被占用,请重新选择\n"); continue; } print_chess(chess, ROW, LIN); break; } else { printf("输入错误,重新选择"); } } }
(4)电脑下棋(优化前)
那玩家下棋实现了,电脑的怎么实现呢?这里就需要使用一个东西——时间戳:时间戳就是一种时间表达方式(对于计算机而言),所以时间一种在变,时间戳也是,那我们要让时间戳变成我们想要的就需要一个函数——rand函数,rand函数和srand配套使用,我们让srand把时间戳的值传给rand,生成随机数,然后将随机数与ROW和LIN取模,得到的数就是0到ROW-1/LIN-1,ROW-1/LIN-1正好是我们数组最大的下标,得到这两个数,我们在去判断得到的坐标是否有‘棋子’,如果没有,落子,打印棋盘,然后跳出,如果有,通过循环的方式,在上去调用一次,得到随机坐标,然后接着判断,所以我们还是需要用循环中嵌套选择的方式去实现。
voidis_rand(charchess[ROW][LIN], introw, intlin) { while (1) { inti=rand() %ROW; intj=rand() %LIN; if (chess[i][j] ==' ') { chess[i][j] ='#'; print_chess(chess, ROW, LIN); break; } } }
(5)电脑下棋(优化后,三子棋实现AI,其他n子棋只能实现部分)
那我们不想让电脑这么笨,只能靠运气怎么办呢?我们不妨往简单的想,只要我们扫过每一列、每一行、对角线这些,然后写一个计数器,遇到一个加1,到有离胜利条件差一个的时候去堵住;或者这一行没有换下一行,清零,在检查下一行,是不是就行了呢?上面堵住的时候就需要去记录一下,哪里是空的,如果不记录,到时候可能就会出现你原来下的棋子被电脑下的去覆盖,那这个时候就是一个bug了;那我们怎么去实现这个猜想呢?其实很简单,我们把每一行,每一列的判断分成一块一块,每一块只负责去判断一种,那是不是就能讲这个复杂的问题拆成四个简单的问题,然后我们一个个去慢慢实现,比如行,那判断没一行是不是需要我们行是线固定的,列一直变,然后到列到最右边的时候换下一列,那是不是就实现了,那如果没一行没一列都没有,那我们就再次调用优化前的电脑下棋,让它随机下一个地方。
intis_windows(charchess[ROW][LIN], introw, intlin) { inti=0;//行intj=0;//列printf("电脑下棋\n"); for (i=0; i<ROW; i++)//判断每行的连在一起的 { intcount=0; intx=0; inty=0; for (j=0; j<LIN; j++) { if (chess[i][j] =='*'||chess[i][j] =='#') { count++; } if(chess[i][j]==' ') { x=i; y=j; } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } } for (j=0; j<LIN; j++)//判断每列的连在一起的 { intcount=0; intx=0; inty=0; for (i=0; i<ROW; i++) { if (chess[i][j] =='*'||chess[i][j] =='#') { count++; } if(chess[i][j]==' ') { x=i; y=j; } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } } intcount=0; intx=0; inty=0; for (i=0; i<ROW; i++)//对角线(但功能不完全,只能判断坐标(x,y)相同的) { if (chess[i][i] =='*'||chess[i][i] =='#') { count++; } if(chess[i][j]==' ') { x=i; y=i; } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } x=0; y=0; for (i=0; i<ROW; i++)//反对角线(功能只能作用于三子棋的判断) { intcount=0; if (chess[i][lin-i-1] =='*'||chess[i][lin-i-1] =='#') { count++; } if(chess[i][j]==' ') { x=i; y= (lin-i-1); } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } is_rand(chess, row, lin);//如果找不到目标,就随机打印}
(6)双人对战
双人对战其实很简单,只要我们将电脑下棋的部分不调用,然后在调用玩家二的下棋函数(与玩家一一致),但是要记住传参过去玩家一和玩家二的‘棋子’要不同。
(7)判断胜负
我们可以下棋了,但是怎么去知道谁赢谁输或者平局?那是不是可以用优化后电脑的思路,只不过不需要去落子了,而是检查有没有连成我们所需要获胜达到的棋子数(这里可以也定义一个常量,以便于后面修改,比如:)的一方,如果有就返回这个符号,然后让外面的if语言判断输赢。
voidis_people(charchess[ROW][LIN], introw, intlin,charstr) { inti=0; intj=0; while (1) { printf("请选择落子位置:"); scanf_s("%d%d", &i, &j); if (i>0&&i<=ROW&&j>0&&j<=LIN) { if (chess[i-1][j-1] ==' ') { chess[(i-1)][j-1] =str; } else { printf("该坐标已经被占用,请重新选择\n"); continue; } print_chess(chess, ROW, LIN); break; } else { printf("输入错误,重新选择"); } } }
那怎么判断平局呢?如果我们上面的循环都进去了,但是没有返回值,这个时候就可以进最后的for循环语句然后判断是否还有空格,如果没有,返回‘0’。
charis_win(charchess[ROW][LIN], introw, intlin, charstr) { inti=0; intj=0; intping=0; for (j=0; j<LIN; j++)//判断列 { intcount=0; for (i=0; i<ROW; i++) { if (chess[i][j] ==str) { count++; } if (count==WIN) { returnstr; } } } for (i=0; i<ROW; i++)//判断行 { intcount=0; for (j=0; j<ROW; j++) { if (chess[i][j] ==str) { count++; } if (count==WIN) { returnstr; } } } for (i=0; i<ROW; i++)//对角线 { intcount=0; if (chess[i][i] ==str) { count; } if (count==WIN) { returnstr; } } for (i=0; i<ROW; i++)//反对角线 { intcount=0; if (chess[i][lin-i-1] ==str) { count++; } if (count==WIN) { returnstr; } } for (i=0; i<ROW; i++)//判断平局 { for (j=0; j<LIN; j++) { if (chess[i][j] !=' ') { ping++; } if (ping==PING ) { return'0'; } } } }
程序调用模块
那现在我们就可以开始调用模块,我们知道,要下棋就需要先打印出开始界面,那我们do while循环语句里面要先使用打印菜单的函数,然后去输入1还是0进行选择是开始还是退出,那是不是要先打印菜单,那就先调用我们的打印菜单的函数;打印完菜单了是不是要进行选择,选择是退出还是开始游戏,就用到选择语句了,如果选0,那就退出,如果选1,那是不是要选择单人还是双人,又需要调用我们的另一个开始菜单,不管选择哪个,都会进入游戏,那我们游戏模块是不是先需要初始化棋盘,然后才要去打印棋盘,初始化加打印完了之后,在这局游戏里面是不是就不需要打印初始化的棋盘了,那我们是不是把他们写在循环之外,这里就会有人迷惑,为什么会要循环呢?我们想一下,我们下棋是不是需要下很多步,如果不写循环,但调用下棋的函数,要调用多少次呢?谁也说不清楚,所以当打印完初始化棋盘之后我们就进入循环,因为不知道具体循环多少次,我们就先让它死循环,然后当到达条件之后,break跳出去,那进入循环,就是我们玩家下棋,调用玩家下棋的函数,下完棋要先判断一下,是否赢了或者是否平局,那我们是不是要接着调用判断函数,并且跟着判断输赢的if语句,如果没有,就让电脑或者另一个玩家去下棋,然后和第一个玩家下棋的流程一样,下完判断输赢,这样是不是就调用完我们所写的所有函数并且让程序跑起来成为我们想要的结果了。
voidinter() { printf("***********************\n"); printf("***** 开始:1 *****\n"); printf("***** 退出:0 *****\n"); printf("***********************\n"); } voidinter1() { printf("***********************\n"); printf("***** 单人:1 *****\n"); printf("***** 双人:2 *****\n"); printf("***********************\n"); } //游戏实现模块voidgame() { charchess[ROW][LIN]={0}; init_chess(chess, ROW, LIN);//初始化数组print_chess(chess, ROW, LIN );//打印棋盘while (1) { is_people(chess, ROW, LIN,'*');//玩家下棋charwin=is_win(chess, ROW, LIN, '*');//判断玩家胜利if (win=='*') { printf("恭喜你,取得胜利\n"); break; }//玩家胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环is_windows(chess, ROW, LIN);//电脑下棋charwin2=is_win(chess, ROW, LIN, '#');//判断电脑胜利if (win2=='#') { printf("很遗憾,电脑胜利\n"); break; }//电脑胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环 } } voidgame1() { charchess[ROW][LIN] = { 0 }; init_chess(chess, ROW, LIN);//初始化数组print_chess(chess, ROW, LIN);//打印棋盘while (1) { is_people(chess, ROW, LIN,'*');//玩家1下棋charwin=is_win(chess, ROW, LIN, '*');//判断玩家1胜利if (win=='*') { printf("恭喜你,取得胜利\n"); break; }//玩家1胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环//is_rand(chess, ROW, LIN);is_people(chess, ROW, LIN,'#');//玩家2下棋charwin2=is_win(chess, ROW, LIN, '#');//判断玩家2胜利if (win2=='#') { printf("很遗憾,电脑胜利\n"); break; }//玩家2胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环 } } //实现开始游戏和退出模块voidtest() { inti=0; srand((unsignedint)time(NULL)); do { inter(); printf("请选择:"); scanf_s("%d", &i); if (i==1) { inti=0; inter1(); printf("请选择:"); scanf_s("%d", &i); if (i==1) { printf("开始游戏\n"); game(); } if (i==2) { printf("开始游戏\n"); game1(); } } elseif (i==0) { printf("退出游戏\n"); } else { printf("选择错误重新选择\n"); } } while (i); } intmain() { test(); return0; }
注意!:
我们要记得把每一个函数、头文件、定义的常量声明在game.h里面,这样我们每个模块就只用引game.h一个头文件就行了。
test.c
//开始界面voidinter() { printf("***********************\n"); printf("***** 开始:1 *****\n"); printf("***** 退出:0 *****\n"); printf("***********************\n"); } voidinter1() { printf("***********************\n"); printf("***** 单人:1 *****\n"); printf("***** 双人:2 *****\n"); printf("***********************\n"); } //游戏实现模块voidgame() { charchess[ROW][LIN]={0}; init_chess(chess, ROW, LIN);//初始化数组print_chess(chess, ROW, LIN );//打印棋盘while (1) { is_people(chess, ROW, LIN,'*');//玩家下棋charwin=is_win(chess, ROW, LIN, '*');//判断玩家胜利if (win=='*') { printf("恭喜你,取得胜利\n"); break; }//玩家胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环is_windows(chess, ROW, LIN);//电脑下棋charwin2=is_win(chess, ROW, LIN, '#');//判断电脑胜利if (win2=='#') { printf("很遗憾,电脑胜利\n"); break; }//电脑胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环 } } voidgame1() { charchess[ROW][LIN] = { 0 }; init_chess(chess, ROW, LIN);//初始化数组print_chess(chess, ROW, LIN);//打印棋盘while (1) { is_people(chess, ROW, LIN,'*');//玩家1下棋charwin=is_win(chess, ROW, LIN, '*');//判断玩家1胜利if (win=='*') { printf("恭喜你,取得胜利\n"); break; }//玩家1胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环//is_rand(chess, ROW, LIN);is_people(chess, ROW, LIN,'#');//玩家2下棋charwin2=is_win(chess, ROW, LIN, '#');//判断玩家2胜利if (win2=='#') { printf("很遗憾,电脑胜利\n"); break; }//玩家2胜利跳出循环elseif (win=='0') { printf("平局\n"); break; }//平局跳出循环 } } //实现开始游戏和退出模块voidtest() { inti=0; srand((unsignedint)time(NULL)); do { inter(); printf("请选择:"); scanf_s("%d", &i); if (i==1) { inti=0; inter1(); printf("请选择:"); scanf_s("%d", &i); if (i==1) { printf("开始游戏\n"); game(); } if (i==2) { printf("开始游戏\n"); game1(); } } elseif (i==0) { printf("退出游戏\n"); } else { printf("选择错误重新选择\n"); } } while (i); } intmain() { test(); return0; }
game.c
//数组元素全部替换为空格voidinit_chess(charchess[ROW ][LIN], introw, intlin) { inti=0; for (i=0; i<ROW; i++) { intj=0; for (j=0; j<LIN; j++) { chess[i][j] =' '; } } } //打印棋盘voidprint_chess(charchess[ROW][LIN], introw, intlin) { inti=0; for (i=0; i<ROW; i++) { intj=0; for (j=0; j<LIN; j++) { printf(" %c ", chess[i][j]); if (j< (LIN-1)) { printf("|"); } } printf("\n"); if (i< (ROW-1)) { for (j=0; j<LIN; j++) { printf("---"); if (j< (LIN-1)) { printf("|"); } } printf("\n"); } } } //玩家下棋voidis_people(charchess[ROW][LIN], introw, intlin,charstr) { inti=0; intj=0; while (1) { printf("请选择落子位置:"); scanf_s("%d%d", &i, &j); if (i>0&&i<=ROW&&j>0&&j<=LIN) { if (chess[i-1][j-1] ==' ') { chess[(i-1)][j-1] =str; } else { printf("该坐标已经被占用,请重新选择\n"); continue; } print_chess(chess, ROW, LIN); break; } else { printf("输入错误,重新选择"); } } } //电脑下棋(优化前)voidis_rand(charchess[ROW][LIN], introw, intlin) { while (1) { inti=rand() %ROW; intj=rand() %LIN; if (chess[i][j] ==' ') { chess[i][j] ='#'; print_chess(chess, ROW, LIN); break; } } } //电脑下棋(优化后)intis_windows(charchess[ROW][LIN], introw, intlin) { inti=0;//行intj=0;//列printf("电脑下棋\n"); for (i=0; i<ROW; i++)//判断每行的连在一起的 { intcount=0; intx=0; inty=0; for (j=0; j<LIN; j++) { if (chess[i][j] =='*'||chess[i][j] =='#') { count++; } if(chess[i][j]==' ') { x=i; y=j; } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } } for (j=0; j<LIN; j++)//判断每列的连在一起的 { intcount=0; intx=0; inty=0; for (i=0; i<ROW; i++) { if (chess[i][j] =='*'||chess[i][j] =='#') { count++; } if(chess[i][j]==' ') { x=i; y=j; } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } } intcount=0; intx=0; inty=0; for (i=0; i<ROW; i++)//对角线(但功能不完全,只能判断坐标(x,y)相同的) { if (chess[i][i] =='*'||chess[i][i] =='#') { count++; } if(chess[i][j]==' ') { x=i; y=i; } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } x=0; y=0; for (i=0; i<ROW; i++)//反对角线(功能只能作用于三子棋的判断) { intcount=0; if (chess[i][lin-i-1] =='*'||chess[i][lin-i-1] =='#') { count++; } if(chess[i][j]==' ') { x=i; y= (lin-i-1); } if (count== (WIN-1)) { chess[x][y] ='#'; print_chess(chess, ROW, LIN); return1; } } is_rand(chess, row, lin);//如果找不到目标,就随机打印} //判断胜利charis_win(charchess[ROW][LIN], introw, intlin, charstr) { inti=0; intj=0; intping=0; for (j=0; j<LIN; j++)//判断列 { intcount=0; for (i=0; i<ROW; i++) { if (chess[i][j] ==str) { count++; } if (count==WIN) { returnstr; } } } for (i=0; i<ROW; i++)//判断行 { intcount=0; for (j=0; j<ROW; j++) { if (chess[i][j] ==str) { count++; } if (count==WIN) { returnstr; } } } for (i=0; i<ROW; i++)//对角线 { intcount=0; if (chess[i][i] ==str) { count; } if (count==WIN) { returnstr; } } for (i=0; i<ROW; i++)//反对角线 { intcount=0; if (chess[i][lin-i-1] ==str) { count++; } if (count==WIN) { returnstr; } } for (i=0; i<ROW; i++)//判断平局 { for (j=0; j<LIN; j++) { if (chess[i][j] !=' ') { ping++; } if (ping==PING ) { return'0'; } } } }
geam.h
//头文件//定义的常量//行//列//几个连一起胜利//棋盘有多少个格子//头文件中声明函数voidinit_chess(charchess[ROW][LIN],introw, intlin);//初始化棋盘voidprint_chess(charchess[ROW][LIN], introw, intlin);//打印棋盘voidis_people(charchess[ROW][LIN], introw, intlin,charstr);//玩家下棋voidis_rand(charchess[ROW][LIN], introw, intlin);//电脑随机下棋intis_windows(charchess[ROW][LIN], introw, intlin);//电脑下棋(优化后,仅适用于三子棋)charis_win(charchess[ROW][LIN], introw, intlin, charstr);//判断胜利