一.了解三子棋游戏
三子棋是一个古老而又有趣的游戏,在国际上得到了大家的广泛喜爱。初学编程的你也一定迫不及待想要用c语言来设计一个自己的游戏吧!路漫漫其修远兮,我们今天从三子棋讲起。
二.分析游戏实现逻辑
要编写一个游戏程序,首先要明确我们想要达到的效果是什么样,下面我将用vs来为大家演示一下游戏运行时的样子:
首先,打开程序,我们来到菜单界面,提醒用户选择玩游戏还是退出游戏:
当用户选择0时,将会退出游戏,结束程序:
当用户选择1时,将会进入三子棋游戏,然后由玩家选择玩家先手还是电脑先手:
如果玩家选择玩家先手,则会打印棋盘并提醒玩家下棋:
如果选择电脑先手,则电脑会先落子,然后提醒玩家落子:
当玩家选择任意坐标下棋后,在棋盘上该位置会出现一个“*”符号用来代表玩家落子,同时电脑自动下棋,并在该位置出现一个“#”符号来代表电脑落子,接着继续请玩家下棋:
结局1:当玩家完成“三子连棋”后,系统判定玩家获胜,结束这盘游戏并打印菜单,玩家可自由选择是否继续进行下一次游戏:
结局2:当电脑完成“三子连棋”后,系统判定电脑获胜,结束这盘游戏并打印菜单,玩家可自由选择是否继续进行下一次游戏:
结局3:当玩家和电脑都没完成“三子连棋”,系统判定平局,结束这盘游戏并打印菜单,玩家可自由选择是否继续进行下一次游戏:
最后,还有一些小的细节需要我们注意:
1.判断玩家是否输入了在棋盘范围内的坐标,如果坐标非法,要提醒玩家重新输入正确的坐标:
2.判断玩家输入坐标是否已被占用,如果已被玩家或电脑占用,要提醒玩家重新输入正确的坐标:
三.逐步实现游戏及其逻辑详解
!!!注意,该部分的代码只是为了详细介绍某一部分的游戏实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看完整详细代码可以移步本文第四部分。
1.实现菜单功能:
由于我们要实现玩不够可以继续玩的游戏逻辑,因此选择do...while的循环语句来实现这一部分的逻辑,每步的详细解释见代码注释:
void menu()//菜单函数实现打印菜单 { printf("*********************************************\n"); printf("*********************************************\n"); printf("***************** 1.play ****************\n"); printf("***************** 0.exit ****************\n"); printf("*********************************************\n"); printf("*********************************************\n"); } void test()//测试游戏运行 { //srand((unsigned int) time(NULL)); //利用时间戳生成随机数以达到电脑可以实现随机下棋 int input = 0; do//使用do while语句来实现游戏可以连续一直玩 { menu();//菜单函数,打印菜单供玩家选择 printf("请选择:>\n");//提醒玩家选择 scanf("%d", &input);//用scanf接收玩家选择存入变量input中 switch (input)//利用分支语句实现玩家的选择 { case 1://当玩家输入1,运行游戏 game(); break; case 0://当玩家输入0,提醒玩家游戏结束 printf("游戏结束\n"); break; default://当玩家输入了非选项数字时,提醒玩家重新输入 printf("输入错误,请重新选择\n"); break; } } while (input);//用变量input的值作为while循环的判定执行条件 //当input不为0时,该循环都可一直运行下去。 }
2.初始化棋盘逻辑:
由于我们是利用二维数组实现在“棋盘”上下棋的,因此当每局游戏开始时,我们应该先将棋盘全部初始化为空格(“ ”),以便在屏幕上表示目前该棋盘是一个未下棋的状态,该部分实现代码如下:
void init_board(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] = ' '; } } }
该部分的逻辑较为简单,只需要用for循环简单遍历数组元素使之=“ ”即可。
3.打印棋盘逻辑:
由本文第二部分的图示可知,我们需要创建一个二位数组来接受玩家和电脑的落子信息,其次我们要先初始化该二维数组为“空格”,以便能向玩家展示棋盘。最后我们要将棋盘打印出来,供玩家选择:
//游戏主函数 void game() { char board[ROW][COL];//创建一个二维字符组来接收棋子信息 //char ret = 0;//该字符变量后续用来接收系统判断输赢的结果 init_board(board,ROW,COL);//首先需要初始化棋盘函数 print_board(board, ROW, COL);//其次要在屏幕上打印棋盘的函数 } //初始化棋盘函数 void init_board(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < row; i++)//利用第一个for循环实现将二维字符组的每行全部初始化为空格 { int j = 0; for (j = 0; j < col; j++)//利用第二个嵌套的for循环实现将二位字符组的每列全部初始化为空格 { board[i][j] = ' ';//令进入此次循环的字符组元素初始化为空格 } } } void print_board(char board[ROW][COL], int row, int col)//实现打印棋盘函数 { int i = 0; for (i = 0; i < row; i++)//利用for循环完成棋盘的单数行打印 { int j = 0; for (j = 0; j < col; j++)//利用第二个嵌套的for循环完成棋盘的每列打印 { printf(" %c ", board[i][j]); if (j < col - 1) //判定是否打印至该行最后一个字符,之前每隔一个字符都需要打印一个分割“|” printf("|"); } printf("\n"); if (i < row - 1)//利用for循环完成棋盘的双数行打印 { int k = 0; for (k = 0; k < col; k++) { printf("---"); if (k < col - 1) //判定是否打印至该行最后一个字符,之前每隔一个字符都需要打印一个分割“|” printf("|"); } printf("\n"); } } }
要想打印出的棋盘美观,就需要灵活的划分打印字符的逻辑,我们按下图的棋盘打印逻辑来编写循环程序以打印棋盘:
如图,我们将图上的1,2,3行的打印划分为一个空格+一个“|”的逻辑打印(即图上的1,2序号组合)。4,5行的打印划分为“---”+“|”的打印(即图上6,7序号组合)。
但要注意的是,棋盘每组的最后一组的“|”是不需要打印的,否则就会像下图一样:
所以需要专门设置一个if语句来判断是否需要打印“|”。
4.玩家电脑先后手逻辑:
使用goto语句来完成跳过玩家下棋,让程序直接跳转到电脑下棋。
void game() { char board[ROW][COL]; char ret = 0; //初始化一下棋盘 init_board(board, ROW, COL); //玩家选择先后手 int hand = 0; printf("请选择先手:>\n"); printf("1---玩家先手 0---电脑先手\n"); scanf("%d", &hand); //打印一下棋盘 print_board(board, ROW, COL); if (hand == 0) { goto hands;//使用goto语句,来达到可以让电脑先手的功能 } while (1) { //玩家下棋 player_move(board, ROW, COL); //玩家下棋后打印棋盘 print_board(board, ROW, COL); //判断输赢 ret = is_win(board, ROW, COL); if (ret != 'C') { break; } hands: //如果前面玩家选择的是0,那么就会跳入循环中的这里让电脑先落子 //电脑下棋 computer_move(board, ROW, COL); //电脑下棋后打印棋盘 print_board(board, ROW, COL); //判断输赢 ret = is_win(board, ROW, COL); if (ret != 'C') { break; } }
由代码可见,在循环下棋阶段,hands将玩家和电脑的下棋逻辑分为了非常相似的两部分。在使用goto语句的时候大家一定要多调试来观察该部分的代码语句运行顺序是否合适,因为有时循序安排不当很有可能让程序陷入死循环。这是我们要极力避免的。
5.判断棋盘有没有被下满:
因为我们的棋盘是有限大的,所以每次落子之前,都需要先判断一下棋盘有没有被下满,当棋盘满了的时候,无论玩家或电脑有没有完成“三子连棋”,这场对局都无法再继续了,俗称“平局”。而平局的判断依据,就是以这个判断棋盘是否被下满的函数为基础的:
//判断棋盘有没有被下满 int is_full(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++) { if (board[i][j] == ' ') return 0; } } return 1; }
6.玩家下棋逻辑:
首先我们要理解,“下棋”的过程其实就是一个改变二维数组内容的过程,棋盘上出现的“*”实际是二维数组的内容由“空格”(“ ”)变为“*”的过程。
理解了这个逻辑,我们就将一个实际问题变得易于编程了,因为“下棋”无非就是将玩家输入的坐标(即数组下标)所在的数组元素由“空格”(“ ”)改为"*"即可。
但在这里我们还需要注意之前在第二部分提到的,那就是注意要判断玩家输入的是否是非法坐标,以及玩家输入的坐标是否已被占用。
//玩家下棋代码逻辑 void player_move(char board[ROW][COL], int row, int col) { printf("玩家下棋\n"); printf("请输入您要下棋的坐标:>"); while (1)//创建循环的目的在于使玩家即便输入了错误的坐标也可以一直输入 { int x = 0; int y = 0; 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("该坐标已被占用,请重新输入\n");//不是空的,提醒玩家重新输入 } } else { printf("坐标非法,请重新输入\n");//坐标不在棋盘内,提醒玩家重新输入 } } }
7.电脑下棋逻辑:
由于我们今天编写的程序只是处于C语言的入门阶段练习,因此在电脑下棋逻辑这部分不深入太难的算法逻辑,仅仅让电脑随机生成坐标来下棋。(当然有兴趣给电脑编写更加智能的下棋逻辑的同学可以考虑结合8.判断输赢里的逻辑来使电脑检测是否有行和列符合拥有两个“*”及一个“ ”的,若有,则使电脑下在空格的位置上。)
//电脑随机下棋逻辑 void computer_move(char board[ROW][COL], int row, int col) { printf("电脑下棋\n"); while (1) { int x = rand() % row;//利用rand函数随机生成一个数作为坐标 int y = rand() % col;//因为%了row,因此生成的坐标恒为合法坐标 if (board[x][y] == ' ')//同样要检测该坐标是否被占用 { board[x][y] = '#'; break; } } }
让电脑生成随机数需要用到rand函数,该函数是一个非常好用的c语言生成随机数的函数,如果有对该函数的使用还不太清楚的同学可以先移步《rand函数详解》,这里不再过多赘述了。
8.判断输赢逻辑:
最后一部分,判断输赢,这部分可以称之为整个程序中最繁琐的一部分了,但我们可以将这部分分成四个小部分来逐步实现:①总思路②判断每行③判断每列④判断对角线
①总思路:首先,由于我们在每次玩家或电脑落子后都需要判断输赢,而在每次落子后,程序都会有四种可能的走向:1.玩家赢了2.电脑赢了3.平局4.前三种均不满足,则继续下棋。因此我们不妨设计个暗号,来告诉while循环到底是谁赢了,还是平局还是继续游戏。
我们不妨将“*”代表玩家胜利,“#”代表电脑胜利,“Q”代表平局,而“C”代表游戏继续。因此,我们的判断输赢函数最终要给我们返回这四种情况的其中一种。我们再根据它返回的数据来决定程序下一步的走向。
//跟在前面讲goto时的代码后面,一旦跳出下棋循环,就对暗号 if (ret == '*') printf("恭喜,你赢了\n"); else if (ret == '#') printf("太可惜啦,电脑赢了\n"); else if (ret == 'Q') printf("平局啦\n");
有了设计思路,接下来就该编写主程序了,根据我们的思路,主函数的逻辑应该是先判断每行有没有赢的,再判断每列有没有赢的,再判断对角线有没有赢的。如果都没有,判断是否平局,如果还没有到达平局的条件(即棋盘还没有下满),那么就让游戏继续。主函数代码如下:
char is_win(char board[ROW][COL], int row, int col) { char judege1 = 0; char judege2 = 0; char judege3 = 0; judege1=row_win(board,ROW,COL);//判断每行有没有赢 judege2=col_win(board, ROW, COL);//判断每列有没有赢 judege3=dia_win(board, ROW, COL);//判断对角线有没有赢 if (judege1 !='x')//如果行赢了,返回行的数据 { return judege1; } else if(judege2 != 'x')//如果列赢了,返回列的数据 { return judege2; } else if (judege3 != 'x')//如果对角线赢了,返回对角线的数据 { return judege3; } //如果都没有,那么判断是否棋盘满了平局了 else if (is_full(board, row, col) == 1) { return 'Q'; } //最后,没有玩家或电脑赢,也没有平局,那么游戏继续 return 'C'; }
②判断每行:判断思路很简单,就是判断每行从开始的两个一直到最后的两个是否都相等,即下图的①=②,②=③,③=④,④=⑤,由这四个等式也易知,当每行有n个元素时,我们需要判断n-1次,同时要特别注意的是,①②③④⑤中的任意一个都不能为“空格”(“ ”)!
代码如下:
char row_win(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < col; i++) { int count = 0; int j = 0; for (j = 0; j < row-1; j++) { if (board[i][j] == board[i][j + 1] && board[i][j] != ' ') { count++; } } if (count == row - 1) { return board[i][j]; } } return 'x'; }
③判断每列:判断思路与每行一致,看连续的n个是否两两相等且不为“空格”(“ ”)。即下图的①=②,②=③,③=④,④=⑤。同样需要判断n-1次。
虽然代码和上一步相似,但有时在写for循环嵌套时难免会感到有些混乱,这时给大家列个可能会好理解一点:
列表还有个好处就是可以防止自己出现逻辑错误,如将i和j的位置写反,这在后期报错是让我们很难找的,因此谨慎一点最好是一次就写正确,会为我们后期省去很多麻烦。本段代码如下:
//判断每列有没有赢 char col_win(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < col; i++) { int count = 0; int j = 0; for (j = 0; j < row-1; j++) { if (board[j][i] == board[j + 1][i] && board[j][i] != ' ') { count++; } } if (count == row - 1) { return board[j][i]; } } return 'x'; }
④判断对角线:这部分可以说是让很多朋友最头疼的部分了,但其实我们只需要一点点初中数学就可以完美解决这个问题:
如图,我们可以将棋盘上的右下到左上对角线(“\”)也即图中的①拟合成函数y=x的形式,带入程序就可以写成([i],[i]),代码如下:
char dia_win(char board[ROW][COL], int row, int col) { int i = 0; int count = 0; //判断‘\’ for (i = 0; i < col-1; i++) { if (board[i][i] == board[i + 1][i+1]&&board[i][i]!=' ') { count++; } } if (count== row - 1)//利用变量count来判定循环是否完成了n-1次等式的判断 { return board[i][i]; } //下边是判断另一条对角线的程序,见下文:
而②这条一次函数的坐标变化我们可以轻易拟合成y=a-x的形式,很明显函数的左右交点坐标为(row-1),即a=row-1,即坐标等式可以写为:
board[i][row - 1 - i] == board[i + 1][row - 2 - i]
综上所述,判断左下到右上的对角线的代码如下:
//判断‘/’ count = 0; for (i = 0; i < row - 1; i++) { if (board[i][row - 1 - i] == board[i + 1][row - 2 - i] && board[i][row - 1 - i] != ' ' ) //这步类似与y=a-x的函数图像,很明显函数的左右交点坐标为(row-1),即a=row-1 //这样理解这步会容易一些 { count++; } } if (count == col - 1) { return board[i][row - 1 - i]; } return 'x'; }
至此,恭喜大家已经成功构建了整个三子棋程序所需要的全部代码,接下来就是整合了 。
四.整和代码运行测试及总结
我们将数量庞大的代码分为三个区域存放,分别是:test.c——game.c——game.h,如图:
test.c中的全部代码:
#include"game.h" void menu() { printf("*********************************************\n"); printf("*********************************************\n"); printf("***************** 1.play ****************\n"); printf("***************** 0.exit ****************\n"); printf("*********************************************\n"); printf("*********************************************\n"); } void game() { char board[ROW][COL]; char ret = 0; //初始化一下棋盘 init_board(board, ROW, COL); //玩家选择先后手 int hand = 0; printf("请选择先手:>\n"); printf("1---玩家先手 0---电脑先手\n"); scanf("%d", &hand); //打印一下棋盘 print_board(board, ROW, COL); if (hand == 0) { goto hands; } while (1) { //玩家下棋 player_move(board, ROW, COL); //玩家下棋后打印棋盘 print_board(board, ROW, COL); //判断输赢 ret = is_win(board, ROW, COL); if (ret != 'C') { break; } hands: //电脑下棋 computer_move(board, ROW, COL); //电脑下棋后打印棋盘 print_board(board, ROW, COL); //判断输赢 ret = is_win(board, ROW, COL); if (ret != 'C') { break; } } if (ret == '*') printf("恭喜,你赢了\n"); else if (ret == '#') printf("太可惜啦,电脑赢了\n"); else if (ret == 'Q') printf("平局啦\n"); } //判断输赢的逻辑 //判断输赢的代码需要告诉while循环:玩家赢了?电脑赢了?平局了?游戏继续? //玩家赢了,返回暗号:* //电脑赢了,返回暗号:# //平局了,返回暗号:Q //游戏继续,返回暗号:C void test()//测试游戏逻辑 { srand((unsigned int)time(NULL)); int input = 0; do { menu(); printf("请选择:>\n"); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("游戏结束\n"); break; default: printf("输入错误,请重新选择\n"); break; } } while (input); } int main() { test(); return 0; }
game.c中的全部代码:
#include"game.h" //初始化棋盘 void init_board(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 print_board(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < row; i++) { //printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]); 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) { // printf("---|---|---\n"); int k = 0; for (k = 0; k < col; k++) { printf("---"); if (k < col -1) printf("|"); } printf("\n"); } } } //玩家下棋 void player_move(char board[ROW][COL], int row, int col) { printf("玩家下棋\n"); printf("请输入您要下棋的坐标:>"); while (1) { int x = 0; int y = 0; 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("该坐标已被占用,请重新输入\n"); } } else { printf("坐标非法,请重新输入\n"); } } } //电脑下棋 //下棋思路:随机生成坐标,只要坐标没有被占用,就下棋 void computer_move(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; } } } //判断棋盘有没有满 int is_full(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++) { if (board[i][j] == ' ') return 0; } } return 1; } //判断输赢 char is_win(char board[ROW][COL], int row, int col) { char judege1 = 0; char judege2 = 0; char judege3 = 0; //判断三行 judege1=row_win(board,ROW,COL); judege2=col_win(board, ROW, COL); judege3=dia_win(board, ROW, COL); if (judege1 !='x') { return judege1; } //判断三列? else if(judege2 != 'x') { return judege2; } //判断对角线? // "\" else if (judege3 != 'x') { return judege3; } //判断平局? else if (is_full(board, row, col) == 1) { return 'Q'; } //判断继续? //没有玩家或电脑赢,也没有平局,游戏继续 return 'C'; } char row_win(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < col; i++) { int count = 0; int j = 0; for (j = 0; j < row-1; j++) { if (board[i][j] == board[i][j + 1] && board[i][j] != ' ') { count++; } } if (count == row - 1) { return board[i][j]; } } return 'x'; } char col_win(char board[ROW][COL], int row, int col) { int i = 0; for (i = 0; i < col; i++) { int count = 0; int j = 0; for (j = 0; j < row-1; j++) { if (board[j][i] == board[j + 1][i] && board[j][i] != ' ') { count++; } } if (count == row - 1) { return board[j][i]; } } return 'x'; } char dia_win(char board[ROW][COL], int row, int col) { int i = 0; int count = 0; //判断‘\’ for (i = 0; i < col-1; i++) { if (board[i][i] == board[i + 1][i+1]&&board[i][i]!=' ') { count++; } } if (count== row - 1) { return board[i][i]; } //判断‘/’ count = 0; for (i = 0; i < row - 1; i++) { if (board[i][row - 1 - i] == board[i + 1][row - 2 - i] && board[i][row - 1 - i] != ' ' ) //这步类似与y=a-x的函数图像,很明显函数的左右交点坐标为(row-1),即a=row-1 //这样理解这步会容易一些 { count++; } } if (count == col - 1) { return board[i][row - 1 - i]; } return 'x'; }
game.h中的全部代码:
注:在game.h的宏定义处可任意修改棋盘的行和列大小来实现n子棋的效果
#pragma once #define _CRT_SECURE_NO_WARNINGS 1 //设定棋盘大小,可叫玩家选择要玩几子棋 #define ROW 5 #define COL 5 //头文件中声明函数 #include<stdio.h> #include<stdlib.h> #include<time.h> //初始化棋盘 void init_board(char board[ROW][COL], int row, int col); //打印棋盘 void print_board(char board[ROW][COL], int row, int col); //玩家下棋 void player_move(char board[ROW][COL], int row, int col); //电脑下棋 void computer_move(char board[ROW][COL], int row, int col); //判断输赢 char is_win(char board[ROW][COL], int row, int col); //判输赢细分 char row_win(char board[ROW][COL], int row, int col); char col_win(char board[ROW][COL], int row, int col); char dia_win(char board[ROW][COL], int row, int col);
总结:编写程序的过程就是一个先在脑袋里想象实现一个什么功能然后动手编码实现的过程,这个过程有时长,有时短,有时让人感到枯燥乏味,有时又让人感到心潮澎湃,会有疲惫但也会有成就感,愿大家都可以在代码的世界创造出一番自己的天地。