各位老铁,相信大家在小时候都玩过这个经典的小游戏吧,你们知道是那个小游戏吗?哈哈,就是三(n)子棋,相信大家对这个小游戏都不陌生吧,那你知道它怎么用C语言实现吗?今天就跟大家分享一下三(n)子棋的实现。
初阶版本:
我们先来看一下最终的效果图
那么我们应该怎么实现这个小游戏呢?接下来直接上干货。
首先我们要创建两个模块,游戏模块和测试模块。分别为如下的游戏模块game.h,game.c和测试模块test.c
首先我们应该把测试模块test.c设计好,就是你这个游戏的每一步需要怎么走,在效果图里我们能看到,首先打印在屏幕上的是一个游戏菜单,那么我们先来写一个打印菜单的函数。
一、打印菜单
void menu(void) { printf("************************\n"); printf("******* 三子棋 *******\n"); printf("*******----------*******\n"); printf("************************\n"); printf("************************\n"); printf("******* 游戏菜单 *******\n"); printf("******* 1.play *******\n"); printf("******* 0.exit *******\n"); printf("************************\n"); }
然后就是选择了,选择1就是play,选择0就是退出游戏。选择其他数就显示报错一下,要求重新选择。
所以我们应该写一个scanf的函数输入,并用switch,case来判断是否开始游戏,由于运行后先玩一把再重新选择是否再玩一把,所以用do,while循环,我们应该像如下这样设计。
二、选择
void test(void) { srand((unsigned int)time(NULL)); int input = 0; do { printf("请选择:>\n"); menu(); scanf("%d", &input); switch (input) { case 1: { game(); break; } case 0: { printf("退出游戏\n"); break; } default: { printf("选择错误,请重新选择:>\n"); break; } } } while (input); }
可以看到上述代码中有这么一句 srand((unsigned int)time(NULL)); 这句是为后面的电脑下棋的随机性的,我们放在后面再讲。
选择了开始游戏之后呢我们就要正式地进入玩游戏的环节了。
我们先来把下棋的整个过程设计出来,再一步一步地实现每一个函数。
void game(void) { printf("开始游戏:>\n"); char board[ROW][COL] = { 0 };//创建二维数组棋盘 char ret = 0; init_board(board, ROW, COL);//初始化二维棋盘 print_board(board, ROW, COL);//打印棋盘 while (1)//下棋环节,这是一个循环的过程,直到有人胜出或者平局才跳出 { player_move(board, ROW, COL);//玩家下棋 print_board(board, ROW, COL); ret= is_win(board, ROW, COL);//判断有没有赢家或者是否平局了,棋盘满了没有赢家就是平局 if (ret != 'c') { break;//有人赢了或者平局了就跳出循环 } Sleep(1000);//停顿一秒 system("cls");//清空屏幕 print_board(board, ROW, COL); computer_move(board, ROW, COL);//电脑下棋 print_board(board, ROW, COL); ret = is_win(board, ROW, COL);//判断 if (ret != 'c') { break; } Sleep(1000); system("cls"); print_board(board, ROW, COL); } if (ret == '*') { printf("玩家赢\n\n"); } else if (ret == '#') { printf("电脑赢\n\n"); } else if (ret == 'q') { printf("双方平局\n\n"); } }
接下来我们就应该逐一实现每一个函数了。
从第一张效果图可以看出,我们应该首先打印一个3*3的小棋盘。先创建一个3*3的二维字符数组并初始化为空格,然后通过打印的方式把棋盘的分隔符和二维字符数组棋盘中需要落子的位置(空格)打印出来。我们分装一个初始化棋盘函数和打印棋盘的函数到游戏模块中。
代码如下:
三、初始化棋盘函数:
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++) { int j = 0; for (j = 0; j < col; j++) { printf(" %c ", board[i][j]);//打印(空格)%c(空格),%c是字符空格 if (j < col - 1)//判断是否为最后一列 { printf("|");//打印分隔符 } } printf("\n"); if (i < row - 1)//判断是否为最后一行,如果是最后一行,则---分隔符就不需要了 { for (j = 0; j < col; j++) { printf("---"); if (j < col - 1) { printf("|"); } } } printf("\n"); } }
我们可以把棋盘中的奇数行的下棋的地方和分隔符分开打印,例如第一行的打印就是
(空格)%c(空格)然后打印 | 写成循环,加一个控制条件最后一列就不用打印 | 就好看了,紧接着就打印行与行的分隔符---,再打印 | ,依然是最后一列就不用打印 | 了。
以此类推,我们就能把棋盘打印好了。具体细节请琢磨代码及添加的注释。
五、玩家下棋:
由于玩家一般不是程序员,所以他们一般不知道数组是从下标为0开始的,所以我们在设计输入坐标的时候就应该注意到这个点。
代码如下:
void player_move(char board[ROW][COL], int row, int col) { int x = 0; int y = 0; printf("玩家走:\n"); while (1) { printf("请输入要下的坐标:>\n"); scanf("%d %d", &x, &y); //判断坐标的合法性n子棋的横纵坐标不能大于n,也不能小于1。 if (x >= 1 && x <= row && y >= 1 && y <= col) { if (board[x - 1][y - 1] == ' ')//数组下标从0开始,但玩家下棋下标从1开始。所以减1 { board[x - 1][y - 1] = '*';//该坐标为空,则下棋进去 break; } else { printf("该坐标已经被占用,请重新输入:>\n"); } } else { printf("坐标非法,请重新输入:>\n"); } } }
每下完一步都需要判断是否有赢家或者棋盘满了的情况。三子棋的胜出规则就是行,列或者对角线三个都相等则胜出,所以我们应该遍历每一行,,每一列和对角线,其中有一个成立即可。
代码如下:
//判断棋盘是否满了 static int is_full(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; } } } return 1; } //判断输赢平局 char is_win(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-1; j++) { if (board[i][j] != board[i][j + 1])//每一行中遇到不相等的就跳出 { break; } } if (j == col-1)//如果上面的循环是遍历结束后跳出的,则证明这一行的字符全部相等 { if (board[i][j] != ' ')//并且都不能等于空格,则返回一个字符方便判断 { return board[i][j]; } } } //判断列相等 for (i = 0; i < col; i++) { int j = 0; for (j = 0; j < row-1; j++) { if (board[j][i] != board[j + 1][i]) { break; } } if (j == row-1)//如果上面的循环是遍历结束后跳出的,则证明这一列的字符全部相等 { if (board[j][i]!=' ')//并且都不能等于空格,则返回一个字符方便判断 { return board[j][i]; } } } //判断主对角线 for (i = 0; i < row-1; i++) { if (board[i][i] != board[i + 1][i + 1])//对角线遇到不相等的字符跳出 { break; } } if (i == row - 1)//如果上面的循环是遍历结束后跳出的,则证明这一主对角线的字符全部相等 { if (board[0][0] != ' ')//并且都不能等于空格,则返回一个字符方便判断 { return board[0][0]; } } //判断副对角线 int j = 0; for (i = row - 1; i > 0; i--) { if (board[i][j] != board[i - 1][j+1]) { break; } j++; } if (i == 0)/如果上面的循环是遍历结束后跳出的,则证明这一副对角线的字符全部相等 { if (board[0][row - 1] != ' ')//并且都不能等于空格,则返回一个字符方便判断 { return board[0][row - 1]; } } int ret = is_full(board, ROW, COL);//判断棋盘是否满,返回一个值,0表示未满,1表示满 if (ret == 0) { return 'c'; } else { return 'q'; } }
六、电脑下棋:
电脑下棋具有随机性,即随机生成一个坐标,然后判断这个坐标是否被占用了,如果被占用,则重新生成,否则就下棋。
代码如下:
void computer_move(char board[ROW][COL], int row, int col) { printf("电脑走:>\n"); while (1) { //前面出现的srand((unsigned int)time(NULL))就是为了在这每次生成随机坐标 //如果不加这句代码的话,则电脑每次运行生成的坐标都是一样的,但是用时间戳作 //为参数,因为时间是每时每刻都在变化的,所以生成的随机值是不同的。但是 //srand((unsigned int)time(NULL))只需要调用一次就行了,所以放在最前面 int x = rand() % row;//生成的随机值%行数,保证坐标合法 int y = rand() % col;//生成的随机值%列数,保证坐标合法 if (board[x][y] == ' ')//如果这个位置为空则下棋,否则,重新生成坐标,直到下棋成功就跳出 { board[x][y] = '#'; break; } } }
电脑下了棋之后当然也要判断输赢和平局啦,就把is_win函数再用一边就好啦,上面几个就是下棋的过程,我们应该写成一个循环,直到有赢家或者平局就结束。
写到这的时候初阶版本的n(n可以是任意的数)子棋就结束了。
整个的参考代码如下:
test.c
#define _CRT_SECURE_NO_WARNINGS 1 //test.c #include "game.h" void menu(void) { printf("************************\n"); printf("******* 三子棋 *******\n"); printf("*******----------*******\n"); printf("************************\n"); printf("************************\n"); printf("******* 游戏菜单 *******\n"); printf("******* 1.play *******\n"); printf("******* 0.exit *******\n"); printf("************************\n"); } void game(void) { printf("开始游戏:>\n"); char board[ROW][COL] = { 0 }; char ret = 0; init_board(board, ROW, COL); print_board(board, ROW, COL); while (1) { player_move(board, ROW, COL); print_board(board, ROW, COL); ret= is_win(board, ROW, COL); if (ret != 'c') { break; } Sleep(1000); system("cls"); print_board(board, ROW, COL); computer_move(board, ROW, COL); print_board(board, ROW, COL); ret = is_win(board, ROW, COL); if (ret != 'c') { break; } Sleep(1000); system("cls"); print_board(board, ROW, COL); } if (ret == '*') { printf("玩家赢\n\n"); } else if (ret == '#') { printf("电脑赢\n\n"); } else if (ret == 'q') { printf("双方平局\n\n"); } } void test(void) { srand((unsigned int)time(NULL)); int input = 0; do { printf("请选择:>\n"); menu(); 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.h
#pragma once //game.h #include <stdio.h> #include <stdlib.h> #include <time.h> #include <windows.h> #define ROW 3 #define COL 3 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); char is_win(char board[ROW][COL], int row, int col); void computer_move(char board[ROW][COL], int row, int col);
game.c
#define _CRT_SECURE_NO_WARNINGS 1 //game.c #include "game.h" //这个是需要一行或者一列或者对角线全部都相等才能判断输赢, //但是事实上当棋盘数目大于等于5*5之后,只要一行或一列或者 //对角线有其中5个相等就能判断输赢了所以当ROW和COL小于等于 // 5的时候用这个game.c否则,则应该用game1.c 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++) { 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) { for (j = 0; j < col; j++) { printf("---"); if (j < col - 1) { printf("|"); } } } printf("\n"); } } static int is_full(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; } } } return 1; } void player_move(char board[ROW][COL], int row, int col) { int x = 0; int y = 0; printf("玩家走:\n"); while (1) { printf("请输入要下的坐标:>\n"); 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"); } } } char is_win(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-1; j++) { if (board[i][j] != board[i][j + 1]) { break; } } if (j == col-1) { if (board[i][j] != ' ') { return board[i][j]; } } } //判断列相等 for (i = 0; i < col; i++) { int j = 0; for (j = 0; j < row-1; j++) { if (board[j][i] != board[j + 1][i]) { break; } } if (j == row-1) { if (board[j][i]!=' ') { return board[j][i]; } } } //判断主对角线 for (i = 0; i < row-1; i++) { if (board[i][i] != board[i + 1][i + 1]) { break; } } if (i == row - 1) { if (board[0][0] != ' ') { return board[0][0]; } } //判断副对角线 int j = 0; for (i = row - 1; i > 0; i--) { if (board[i][j] != board[i - 1][j+1]) { break; } j++; } if (i == 0) { if (board[0][row - 1] != ' ') { return board[0][row - 1]; } } int ret = is_full(board, ROW, COL); if (ret == 0) { return 'c'; } else { return 'q'; } } 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; } } }
这个棋盘的行和列数是可以自己定义的,但是细心的小伙伴已经发现了,如果是3*3的棋盘,即三子棋,那么毫无疑问某一行,某一列和对角线3个棋都要相等才能赢,但是如果是10*10这种呢?如果也要某一行,某一列或者对角线的全部位置的棋都相等的话,显然赢的概率几乎是没有的,所以实际上当n>5之后,某一行,某一列或者对角线只要满足连续5个棋相等就能赢了,所以我们再来写一个进阶的判断输赢的版本,其他的代码是不变的。
进阶版本:
static int is_full(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; } } } return 1; } char is_win(char board[ROW][COL], int row, int col) { int i = 0; //判断行相等 for (i = 0; i < row; i++) { int j = 0; int count = 1;//因为比较5个相邻的棋只有4对,所以count应该初始化为1 for (j = 0; j < col - 1; j++) { if (board[i][j] != board[i][j + 1])//每次遇到不同的棋就把count重新置为1 { count = 1; } else { count++;//遇到相同的棋count就+1 if (count == 5)//直到count=5的时候证明已经有赢家了,返回其中一个棋,判断输赢 { if (board[i][j] != ' ') { return board[i][j]; } } } } } //判断列相等 //和判断行相等同理 for (i = 0; i < col; i++) { int j = 0; int count = 1; for (j = 0; j < row - 1; j++) { if (board[j][i] != board[j + 1][i]) { count = 1; } else { count++; if (count == 5) { if (board[j][i] != ' ') { return board[j][i]; } } } } } //判断主对角线 //和上面判断方法一样,随着主对角线往下比较 int count = 1; for (i = 0; i < row - 1; i++) { if (board[i][i] != board[i + 1][i + 1]) { count = 1; } else { count++; if (count == 5) { if (board[i][i] != ' ') { return board[i][i]; } } } } //判断副对角线 //和上面判断方法一样,随着副对角线往上比较 int j = 0; count = 1; for (i = row - 1; i > 0; i--) { if (board[i][j] != board[i - 1][j + 1]) { count = 1; } else { count++; if (count == 5) { if (board[i][j] != ' ') { return board[i][j]; } } } j++; } int ret = is_full(board, ROW, COL); if (ret == 0) { return 'c'; } else { return 'q'; } }
来到这的时候进阶版本的子棋就完成了大部分了,不知大家有没有发现,我这里的判断对角线的地方只判断了最长的两条,还有剩下的短的对角线是不是也有可能达到5个连续的呢?所以我们也应该把全部的斜线都遍历一遍。这里就留给大家作为拓展的部分了,有兴趣的小伙伴们可以尝试完善一下,然后发到评论区里,大家互相交流经验,互相学习。