本文将介绍三子棋(井字棋)的实现方法,以游戏的流程为主线,运用模块化编程的思想,分为game.c, game.h , test.c三个文件完成代码。先看一下最后的效果吧
三子棋的游戏大家应该都不陌生,但这里还是有必要介绍一下,毕竟熟悉游戏的流程思路才会清晰。
游戏规则要求:电脑(随机)和玩家一次下一子,双方轮流交替下子,直到电脑和玩家任意一方获胜,或者棋盘满了,结束游戏。
获胜判定:只要电脑或者玩家任意一方的棋子有三子连在一起,可以为横连、纵连、斜连,则该方获胜,游戏结束。若棋盘满了,则判定为平局。
加粗样式
整个游戏流程可分为以下这几步
1.给玩家一个菜单,让其选择进入游戏或者退出游戏
接下来是一个循环的过程
2.玩家选择进入游戏后,接下来呈现一个棋盘
3.系统提示玩家落子,玩家输入一个坐标
4.屏幕上打印棋盘,展现落子情况
5.电脑落子
6.打印棋盘,展现落子情况
7.游戏分出结果(平局,玩家胜,电脑胜)
8.回到第一步
上述流程中我们可以看到,游戏的核心部分是2到7步,因此我们将这些步骤的实现封装在一个game函数里,game函数里需要用到的函数就在game.h文件中声明,在game.c文件中定义。有了这些思考我们就可以开工了。
首先打印一个菜单,给出两个选项,玩家的选择值用一个变量接收。
接着使用多分支switch case语句,若玩家选择开始游戏就进入game函数,若选择退出就结束程序,如果选择不符合要求就让他重新输入。
整个过程用循环结构套起来,当一套流程走完后用户又可以重新选择。这里我们选择do while循环,让这个逻辑至少走一遍。
另外这里接收用户输入的值时需要进行合法性判断。比如我期望接收一个数字,但你却输入一个字母,如果没有处理这种情况程序就会陷入死循环(至于为什么下文分析)
上代码:
void menu()
{
printf("*****************************************\n");
printf("******* 1. 三子棋游戏 *****\n");
printf("******* 2. 退出游戏 *****\n");
printf("*************************************** \n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择>:");
while (scanf("%d", &input) != 1)
{
printf("输入非法,请重新输入>:");
while (getchar() != '\n')
{
;
}
}
switch (input)
{
case 1:
printf("三子棋游戏开始\n");
game1();
break;
case 2:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input != 2);
return 0;
}
这里的do while循环和switch case语句的配套使用基本上可以作为固定写法形成模板,以后执行类似的逻辑时直接套用它就行了。
这里讲一讲用户输入数据的合法性判断。
这里我期望用户输入一个数字,只要是数字就行,要求不高,哪怕不是1或者2都行,因为我的default可以处理。
但总有一些叛逆的玩家不这么干,给你丢一个字母放上面,按下回车,scanf函数于是开始读取数据,但是我要求接收的是%d啊,你咋给我放个字符啊。scanf函数罢工,接收失败,input也没有放入新的值,还是初值0。程序往下走,来到default这儿,提示选择错误。input != 2,再次进入do while循环。
因为上次循环时那个字母读取失败了嘛,所以还留在缓冲区里,这里scanf函数往缓冲区内一瞅,发现里面有东西,所以不会停下来等待用户输入,直接开始读取,结果又读取失败,input的值还是最初的0。所以就这样陷入死循环。
这里的解决方法就是给scanf函数套上一个while循环,当你接收成功就跳出去,如果接收不成功还要刷新缓冲区以便再次接收,关于刷新缓冲区的方法之前有讲过,感兴趣的老铁可以翻看我以前的文章
void game1()
{
//存放数据需要一个3*3的二维数组
char board[ROW][COL] = { 0 };
//初始化棋盘
InitBoard(board, ROW, COL);
//显示棋盘
DisplayBoard(board, ROW, COL);
char ret = 0;
while (1)
{
//玩家下棋
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);//显示棋盘
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);//显示棋盘
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if ('*' == ret)
{
printf("恭喜你,你赢了\n");
}
else if ('#' == ret)
{
printf("很遗憾,你输了\n");
}
else
printf("平局,再接再厉\n");
}
这是game函数的实现,可以看到game函数里嵌套了许多函数,前文讲过,这些嵌套函数的定义和声明分别放在game.c和game.h文件中。接下来我将逐步介绍game函数里的每个步骤,同时讲解这些函数在game.c中是如何实现的
//存放数据需要一个3*3的二维数组
char board[ROW][COL] = { 0 };
首先我们想到玩家和电脑下棋,那我们需要一个棋盘并记录他们落子的位置。我们创建一个3*3的二维数组来存储数据,玩家落子后就在这个位置放*
,电脑落子后就放 #
这里我们使用宏定义(放在game.h中),万一我们以后想做五子棋,想要一个更大的棋盘,只需更改ROW和COL的·数值即可。
//初始化棋盘
InitBoard(board, ROW, COL);
什么是初始棋盘?也就是给数组board的每个元素都赋值为空格这个字符。
有老铁可能会有疑问,有这个必要吗?不是一开始赋初值为0了,玩家和电脑落子后再把相应的位置替换成#或者*,这不是刚刚好?
回答是有必要的,因为board这个数组不仅是记录落子信息的,后面我们要打印棋盘也要用到它。
如果一开始每个元素都是0,也就是’\0‘,空字符,打印出来是不占位置的,而 *和 #要占据一个位置,到时候棋盘打印出来岂不是会参差不齐?所以我们把每个元素都换成空格,就算这个地方没有落子,打印出来也好歹会占个位置。
//初始化期棋盘
void InitBoard(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++)
{
board[i][j] = ' ';
}
}
}
//显示棋盘
DisplayBoard(board, ROW, COL);
我们预想的效果就是这个样子,棋盘由横杠,竖杠,空格和数组元素组成,棋盘旁边有行列标号以便玩家更好的输入自己要下的位置。
怎么打印这个图案呢?我们首先想到的肯定是循环,不然一个一个打印岂不是要累成狗。
我们做如下分解:
首先打印列号,一个for循环搞定,格式就是”空格%-2d空格"
(注意竖杠也占据一个位置),这里-2d表示域宽为2并且左对齐。为什么这里要使用域宽而不是直接用空格呢,这是为了方便以后打印更大的棋盘时,列号超过了9,出现两位数,如果不用域宽打印效果就没有那么好。当然在for循环前还要打印几个空格以便对齐。
然后我们可以将含有数据的行(简称数据行)和”—|—|—“行(简称分割行)看成一组,那么总共就有三组,其中最后一组没有分割行,那么我们又可以用一个for循环搞定,加上if语句限制分割行打印
接着我们单独看数据行。最先我们需要打印一个行号,这里我们同样使用域宽打印,原因和上面一样。然后可以把“空格%c空格”和‘|’看成一组,总共有三组其中最后一组没有‘|’,这样就可以用一个for循环配套if语句搞定。
分割行思路和数据行类似。
上代码
//显示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
//打印列号
printf(" ");
for (i = 1; i <= col; i++)
{
printf(" %d ", i);
}
printf("\n");
//数据行和分割行看成一组,共有三组,最后一组没有分割行
for (i = 0; i < row; i++)
{
//打印行号
printf("%d", i + 1);
//打印数据行
//将元素与“|”看成一组,共有三组,最后一组没有“|”
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
printf(" ");
//打印分隔行
//将“---”与“|”看成一组,共三组,最后一组没有“|”
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
}
}
}
//玩家下棋
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);//显示棋盘
玩家下棋,即让玩家选择坐标,也就是行号和列号,因此我们需要两个变量x和y来存储信息。
scanf函数接收时,我们同样要进行判断看它是否接收成功,方法前文提过,这里不再赘述。
数据接收成功后,我们还要对坐标进行判断,看横纵坐标是否超出范围,例如我输入一个4 4
,系统就要提示坐标非法并能重新输入。
如果坐标合法我们还要进行一次判断,看该位置是否已经落子。
如果一切条件都满足后,最后才能把这个位置相对应的数组元素改成*
,注意这里可不敢直接写成board[x][y]哈,横纵坐标都要减去一。
上代码
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("该你走了\n");
while (1)
{
printf("请输入坐标落子>:");
while (scanf("%d %d", &x, &y) != 2)
{
printf("输入非法,请重新输入>:");
while (getchar() != '\n')
{
;
}
}
//判断坐标合法性
if (x >= 1 && x <= 3 && y >= 1 && y <= 3)
{
if (' ' == board[x - 1][y - 1])
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("该位置已经落子,请重新选择\n");
}
}
else
{
printf("坐标非法,请重新选择\n");
}
}
接下来是一段循环的过程,玩家下棋——电脑下棋——玩家下棋,直至分出结果,所以套上while循环
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
{
break;
}
玩家落子之后电脑是否要落子呢?这要看是否已经出了结果,所以我们要进行判断。
我们写一个判断输赢的函数,如果
玩家胜,返回 *
,
电脑胜,返回#
,
平局返回Q
未分出结果返回C
这里有个巧妙的设计,玩家和电脑胜利返回的符号,和数组里双方棋子代表的符号相同耶!为什么这么设计呢,先卖个关子。
玩家胜利和电脑胜利其实又可以归纳为一种情况,也就是分出了胜负,什么时候会分出胜负?当棋盘上三子一线时。所以我们把所有请情况都列举出来,横着三条线用for循环,竖着三条线再用for循环,斜着的两条线想不到好办法就单独用两个if语句判断。
若分出胜负,我们直接返回那条线上中的任意棋子的符号就行。例如第一行有三个#
,就可以直接返回board[0][0]
这就时那个巧妙的设计,若不这样设计,我们还得判断是哪三个棋子连成一线,然后分情况返回。
若没有分出结果,则继续判断是否是平局。什么时候平局呢?当棋盘满了并且仍未分出胜负时
因为就是没有分出胜负才来判断是否平局,所以我们只需看棋盘是否已经满了。可以设计一个IsFull函数,如果棋盘满了就返回1,没满就返回0。
最后,如果上述两种情况都不满足,那就是游戏还要继续,返回C
上代码
//棋盘是否下满
int IsFull(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;
}
//判断输赢
//玩家赢返回*
//电脑赢返回#
//平局返回Q
//继续返回C
char IsWin(char board[ROW][COL], int row, int col)
{
//分出胜负
int i = 0;
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2]
&& board[i][1] != ' ')
{
return board[i][0];
}
}
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i]
&& board[1][i] != ' ')
{
return board[1][i];
}
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2]
&& board[1][1] != ' ')
{
return board[1][1];
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0]
&& board[1][1] != ' ')
{
return board[1][1];
}
//平局
//棋盘满了返回1,没满返回0;
if (IsFull(board, row, col) == 1)
{
return 'Q';
}
//继续
return 'C';
}
//电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);//显示棋盘
这里电脑下棋,其实就是随机产生横坐标和纵坐标,因此我们也要两个变量x和y来存储随机值。
产生随机值要用到rand函数,它产生的是一个0~32767之间的数字,而我们需要的是1到3之间的数字,因此我们要限制范围。怎么限制呢?难道像玩家下棋一样,用if语句判断,如果不在1到3就重新生成吗?这个方法不太好,因为生成1到3的概率差不多是万分之一,虽然最后肯定能实现,但效率就大打折扣了。
所以我们用取模的方法,随机数模上3,范围是0到2,再加上1,不就是我们要的1~3吗?
然后就是和玩家下棋一样,还要判断这个地方是否已经落子。
上代码
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
//判断坐标合法性
while (1)
{
x = rand() % row;
y = rand() % col;
if (' ' == board[x][y])
{
board[x][y] = '#';
break;
}
}
}
这里顺便提一下,rand函数一般和srand函数搭配是使用,同时还要用到时间戳
,这样才能产生“真正的随机数”,所以我们要在main函数开头加上这样一条语句。关于rand函数的具体用法有兴趣的童鞋自行去了解
srand((unsigned int)time(NULL));
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
{
break;
}
Iswin函数的实现上面已经写过,这里不再重复
if ('*' == ret)
{
printf("恭喜你,你赢了\n");
}
else if ('#' == ret)
{
printf("很遗憾,你输了\n");
}
else
printf("平局,再接再厉\n");
这个地方可以自由发挥,如果玩家输了可以无情嘲讽他,“不是吧不是吧,连人工智障都下不过~”等等
不知道你有没有注意到我这儿if语句里的写法,常量放在左边,变量放在右边,包括前面的一些位置也是,其实这都是刻意为之的。
因为对与新手而言,写着写着就容易把等号写成赋值符号,那么括号里的条件真假取决于复制号右边的量,如果是0就恒为假,非0恒为真,并且这种错误较为隐蔽,编译器不会报错。
我把常量放在左边后,对常量赋值的操作是不允许的,编译器就会报错,这也是一种编程的良好习惯或者技巧。
我们其实还可以对这个程序做一些优化。例如当用户选择开始游戏后,可以让系统停顿一会并显示加载中…………
等字样。
轮到电脑下棋时,玩家还没反应过来电脑就已经下完了并且重新打印了棋盘,所以我们可以让系统停顿机几秒并显示电脑思考中…………
等字样增强游戏体验感。
另外整个过程屏幕上会打印很多内容显得不简洁,所以我们可以在适当的位置加上清屏命令。
其实还有很多地方可以优化,比如把双方棋子换成不同颜色打印出来,这样会更醒目一些,这些内容都可以通过查阅资料的方式进行拓展。
完整代码
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game1.h"
void menu()
{
printf("*****************************************\n");
printf("******* 1. 三子棋游戏 *****\n");
printf("******* 2. 退出游戏 *****\n");
printf("*************************************** \n");
}
void game1()
{
//存放数据需要一个3*3的二维数组
char board[ROW][COL] = { 0 };
//初始化棋盘
InitBoard(board, ROW, COL);
//显示棋盘
DisplayBoard(board, ROW, COL);
char ret = 0;
while (1)
{
//玩家下棋
PlayerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);//显示棋盘
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
{
break;
}
//电脑下棋
ComputerMove(board, ROW, COL);
DisplayBoard(board, ROW, COL);//显示棋盘
//判断输赢
ret = IsWin(board, ROW, COL);
if (ret != 'C')
{
break;
}
}
if ('*' == ret)
{
printf("恭喜你,你赢了\n");
}
else if ('#' == ret)
{
printf("很遗憾,你输了\n");
}
else
printf("平局,再接再厉\n");
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("三子棋游戏开始\n");
printf("玩命加载中……………………\n");
Sleep(3000);
system("cls");
game1();
break;
case 2:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input != 2);
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game1.h"
//初始化期棋盘
void InitBoard(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++)
{
board[i][j] = ' ';
}
}
}
//显示棋盘
void DisplayBoard(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
//打印列号
printf(" ");
for (i = 1; i <= col; i++)
{
printf(" %d ", i);
}
printf("\n");
//数据行和分割行看成一组,共有三组,最后一组没有分割行
for (i = 0; i < row; i++)
{
//打印行号
printf("%d", i + 1);
//打印数据行
//将元素与“|”看成一组,共有三组,最后一组没有“|”
for (j = 0; j < col; j++)
{
printf(" %c ", board[i][j]);
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
printf(" ");
//打印分隔行
//将“---”与“|”看成一组,共三组,最后一组没有“|”
if (i < row - 1)
{
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
{
printf("|");
}
}
printf("\n");
}
}
}
//玩家下棋
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("该你走了\n");
while (1)
{
printf("请输入坐标落子>:");
while (scanf("%d %d", &x, &y) != 2)
{
printf("输入非法,请重新输入>:");
while (getchar() != '\n')
{
;
}
}
//判断坐标合法性
if (x >= 1 && x <= 3 && y >= 1 && y <= 3)
{
if (' ' == board[x - 1][y - 1])
{
system("cls");
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("该位置已经落子,请重新选择\n");
}
}
else
{
printf("坐标非法,请重新选择\n");
}
}
}
//电脑下棋
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("该电脑走了,思考中………………\n");
Sleep(2000);
system("cls");
//判断坐标合法性
while (1)
{
x = rand() % row;
y = rand() % col;
if (' ' == board[x][y])
{
board[x][y] = '#';
break;
}
}
}
int IsFull(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;
}
//判断输赢
//玩家赢返回*
//电脑赢返回#
//平局返回Q
//继续返回C
char IsWin(char board[ROW][COL], int row, int col)
{
//分出胜负
int i = 0;
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2]
&& board[i][1] != ' ')
{
return board[i][0];
}
}
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i]
&& board[1][i] != ' ')
{
return board[1][i];
}
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2]
&& board[1][1] != ' ')
{
return board[1][1];
}
if (board[0][2] == board[1][1] && board[1][1] == board[2][0]
&& board[1][1] != ' ')
{
return board[1][1];
}
//平局
//棋盘满了返回1,没满返回0;
if (IsFull(board, row, col) == 1)
{
return 'Q';
}
//继续
return 'C';
}
game.h
#pragma once
#define ROW 3
#define COL 3
#include
#include
#include
#include
//函数声明
//初始化棋盘
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 IsWin(char board[ROW][COL], int row, int col);
以上就是三子棋的全部内容了,码字不易~~如果觉得这篇文章对你有帮助的话,别忘记留下你的哟,文章中有错漏指出欢迎留在下方评论区