(文中代码皆是按设计三子棋的思路排放的,包含每个函数的实现。)
(完整代码,也就是可运行代码,在文章最末!)
当我们一门编程语言学到一定的阶段,我们就要去应用它做一些有意思的东西,这样不仅可以巩固你对语言的理解,还可以加强你的编程思维。同时,如果自己独立完成了这件事,你收获的一定是满足与开心。因为我就是如此,那我们接下来,来一起分析三子棋吧。
我们要做三子棋,首先需要了解三子棋。三子棋,见名知意,三颗棋子连成线就会获胜。一般三子棋是在3×3的棋盘上来玩的,因为如果棋盘太大的话很容易就会获胜,这样的话三子棋也没了意义。因此我们定义了棋盘的行数和列数如下:
#define ROW 3
#define COL 3
我们用#define 定义行和列,是为了使代码的可修改性更好,想要扩大棋盘的时候不用一个一个修改数据,只需要修改#define 后定义的数据。
请看下图,三子棋获胜情况:
图1-1 对角线获胜情况 图1-2 横行获胜情况 图1-3 竖行获胜情况
在了解完三子棋的样子和获胜情况之后,我们来研究如何设计三子棋。
一、我们要在棋盘上下棋,我们就要输入数据,我们所下的棋都下在方格之中,也就是说我们的数据要存储在我们肉眼看到的方格的地方,因此我们就可以联想到二维数组。二维数组就可以完成我们的数据存储。然后,我们应该想到,棋盘上如果乱糟糟的,我们下棋都分不清敌我双方谁的棋子的话那肯定是不行的,因此我们要打扫棋盘。所以可以知道,第一步,我们要有一个干净的棋盘,两个因素。
★★★创建二维数组(棋盘)并初始化二维数组(打扫干净)
我们为了让棋盘看起来干净,所以我们将二维数组的每个元素初始化为空格。
char board[ROW][COL];
InitBoard(board,ROW,COL);
void InitBoard(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
二、我们创建了棋盘并且打扫了棋盘,但是这个棋盘我们看不到,因此,我们下一步就是将棋盘显示出来,也就是打印棋盘。我们打印棋盘的效果图如下:
图2-1 棋盘最终效果图
也就是说我们可以在第一行打印 | | ,第二行打印---|---|---,然后循环ROW次。
图2-2 棋盘过程效果图1
但是通过图2-2我们发现,---|---|---,打印了三行,最后一行不应该出现,但是出现了。
因此我们要加限制条件,来控制它最后一次循环不打印---|---|---。因此,我们就可以得到图2-1的效果图。但是如果我们将棋盘改成9×9的表格,也就是将ROW和COL都改为9,我们再打印棋盘的时候,结果就是一个9×3的效果图(可自主测试)。因此我们要再对我们的代码进行优化。也就是将打印 | | 和打印---|---|---改为一个循环,将 |和---|作为一个整体,循环COL次。
图2-3 棋盘过程效果图2
我们发现图2-3和图2-1还是有差别,最后一列的时候多了一列 | 因此我们还是要控制,在循环的最后一次不再打印 |和---|,而是打印 和---。这样就得到了图2-1所示的最终棋盘效果图。
★★★打印棋盘
PrintBoard(board, ROW, COL);
void PrintBoard(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (j != col - 1) //对最后一列不打印 |而打印 的控制
printf(" %c |", board[i][j]);
else
printf(" %c \n",board[i][j]);
}
if (i != row - 1) //对最后一行不打印---|---|---的控制
{
for (int j = 0; j < col; j++)
{
if (j != col - 1) //对最后一列不打印---|而打印---的控制
printf("---|");
else
printf("---\n");
}
}
}
}
三、下棋
下棋分为两部分,玩家下棋和电脑下棋,两者是交换下棋的。
我们先来独立的考虑玩家下棋的部分:
我们要下棋,就要输入坐标,输入的坐标就要存入二维数组,但是我们要考虑三个问题,第一个问题就是,我们的坐标,如果我们不小心按错了,输入的坐标在创建的二维数组中找不到的情况。 第二个问题就是,我们输入的坐标我们以前已经输入过了。第三个问题就是,我们的游戏玩家不是程序员,不知道数组的下标从0开始,它们输入坐标的时候左上角坐标就是1,1 右下角坐标就是3,3这样的。
因此,我们就应该有了思路:首先我们应该接收一个坐标,然后判断坐标的有效性,也就是坐标所表示的位置我们在二维数组中是可以找到的。如果坐标是无效的,那么就请玩家重新下棋,如果这个坐标是有效的,那么就判断一下这个位置下没下过棋,如果这个位置下过棋,那么就告诉玩家,这个位置有棋,请重新下。如果这个位置没有棋,那么就在这个位置下棋,并且,玩家下棋结果。
玩家下棋函数如下:
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要下棋的坐标>\n");
scanf("%d%d", &x, &y);
if ((x > 0 && x < 4) && (y > 0 && y < 4))
{
if (board[x - 1][y - 1] == ' ')
{
board[x - 1][y - 1] = '*';
break;
}
else
{
printf("已有棋子,请重新下!\n");
}
}
else
{
printf("坐标输入不合法,请重新输入!\n");
}
}
}
我们再来考虑电脑下棋的情况:
电脑下棋和思路和玩家下棋的思路大致相同。首先我们要给一个坐标,这个坐标是用rand()这个函数随机生成的。玩家下棋下一步需要判断坐标的有效性,但是电脑随机生成的坐标我们可以让它生成有效范围内的坐标,因此,我们就不需要判断坐标的有效性,而可以直接到下一步,判断该位置有无棋子,如果有棋子,那么重新生成坐标,再进行判断。如果没有棋子,那么电脑就在该位置下棋,然后电脑下棋结束。
电脑下棋函数如下:
void ComputerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
printf("电脑下棋!\n");
while (1)
{
x = rand() % ROW;
y = rand() % COL;
if (board[x][y] == ' ')
{
board[x][y] = '#';
break;
}
}
}
在此,我们需要对随机数生成函数rand()做一个说明。 rand()函数生成值的范围是0~RAND_MAX(这个值取决于编程环境)。但是当我们第一次运行、第二次运行、第三次运行后发现,rand()生成的随机数每次都是一样的。这是因为,随机数生成的“种子”是一样的,在没有指定种子的时候,默认种子是常量1。
因此,我们知道,生成随机数的“种子”是相同的时候,我们每一次运行生成的随机数总是相同的。而我们想要得到每一次运行都不相同的随机数,就需要种子每次不同,这就是说,我们要拿随机数当“种子”来生成随机数。这是矛盾的,所以我们只能找一个在变化的数来当“种子”以起到生成所谓的伪随机数的作用。我们一般就是把运行程序时的时间当作种子,这样就能生成每次都不一样的随机数。
rand()函数需要引用头文件#include<stdlib.h> 种子生成器的写法是:srand((unsigned int)time(NULL)) 需要注意的是,种子生成器需要放在main()函数中,不能放在rand()调用之前,否则起不到该有的作用。
在考虑完玩家下棋和电脑下棋的方式之后,我们要考虑一个新的问题,下棋是一回合就结束的么?很显然不是的,所以我们需要在循环里调用玩家下棋函数和电脑下棋函数来组成下棋这一模块。既然要用循环,那循环几次呢?我们不好确定,因为我们可能三个回合就赢了,也可能下满棋盘平局了。所以,我们又发现,我们可以结束循环的情况有三种,输了、赢了、平局,也就是说要下出一个结果来,才会停止。所以我们可以让循环一直循环,而结束循环的事,交给下一部分,判断输赢。所以,我们来讨论最后一部分,判断输赢。
四、上一部分中,我们考虑了回合制的下棋,但是我们有一个问题没有解决,那就是如何使下棋结束。通过分析,我们知道,下棋结束有三种情况,分别是输了、赢了、平局,因此,我们可以设计一个函数来判断每走完一步棋之后是何种结果。其中,这个函数不应该只包含输了、赢了、平局这三种情况,还应该有继续下棋这一步。所以我们设计这个函数,如果玩家赢了返回“ * ”、如果电脑赢了返回“ # ”、如果平局返回“Q”、如果没有结束,也就是继续下棋,返回“C”。
我们通过刚开始对三子棋的赢棋情况,写了如下函数,代码如下:
char IsWin(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][2] != ' ')
return board[i][0];
}
for (int i = 0; i < row; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[2][i] != ' ')
return board[0][i];
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[2][2] != ' ')
return board[0][0];
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[2][0] != ' ')
return board[0][2];
if (IsFull(board,ROW,COL) == 1)
return 'Q';
return 'C';
}
代码中分别判断了三种横着连成线的情况、三种竖着连成线的情况和两种对角线情况。然后判断了棋盘是否满了,也就是平局的情况,这里我们又调用了一个函数,如果棋盘满了,那么这个函数就返回1,如果没满,那么这个函数就返回0。这个函数的代码如下:
int IsFull(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 0;
}
}
return 1;
}
这样,我们判断下棋输赢的函数就讨论完了。
然后我们将第三部分下棋和第四部分判断输赢的,函数调用代码展示一下,因为它们是在一个循环里面的,所以在第三部分没有单独展示,代码如下:
while (1)
{
PlayerMove(board,ROW,COL);
PrintBoard(board, ROW, COL);
if (IsWin(board,ROW,COL) != 'C')
break;
ComputerMove(board,ROW,COL);
PrintBoard(board,ROW,COL);
if (IsWin(board,ROW,COL) != 'C')
break;
}
if (IsWin(board,ROW,COL) == '*')
printf("玩家赢!!!\n");
else if (IsWin(board,ROW,COL) == '#')
printf("电脑赢!!!\n");
else if (IsWin(board,ROW,COL) == 'Q')
printf("平局!!!\n");
}
五、至此,我们的三子棋游戏已经设计完了。但是,既然是游戏,我们还是要设计一个游戏界面的。具体的游戏界面效果图如下:
图5-1 游戏界面
这个游戏界面是写了一个menu()函数实现的,具体的实现代码如下:
void menu()
{
printf("*****************************\n");
printf("****** 1、开始游戏 ********\n");
printf("****** 0、退出游戏 ********\n");
printf("*****************************\n");
}
既然有了游戏界面,那就需要实现背后的选择逻辑,比如,你输入1时,是开始游戏,你输入0时,是退出游戏。具体的选择代码,我直接放在下面,因为这一部分比较简单,并不做过多的赘述了,直接看代码:
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择>");
scanf("%d",&input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
}
我们再对这个代码多说一句,就是input这个变量,这个程序的优势之处就在于,如果你选择0的时候,就会进入case 0这个分支,打印退出游戏。与此同时,input=0,也结束了循环。一个变量就完成了结束游戏的所有工作,这是我认为比较优势的地方。
六、我们从菜单到游戏,已经完全设计完成,至此,游戏已经可以正常玩了。但是,我们还可以在此基础上进行略微的优化,我们可以利用Sleep()函数和system("cls")命令,对我们的游戏进行优化。具体的优化过程,是没有好的方法的,需要通读代码,知道代码执行一些程序后屏幕是什么样子的,然后根据自己想要的样子进行清屏或者睡眠。大体调整之后,我们要测试代码,看看与自己的设想是否相同,如果不同,我们再找到相应的地方,去调整清屏或睡眠的位置。
下面是我完整的代码,我将代码分文件编写,函数的实现都放在了fun.c中,函数的声明都在game.h中,游戏的主体在game.c中。代码如下:
fun.c文件
#include"game.h"
void InitBoard(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
void PrintBoard(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf(" %c |", board[i][j]);
/*if (j != col - 1)
printf(" %c |", board[i][j]);
else
printf(" %c \n",board[i][j]);*/
}
printf("\n");
if (i != row - 1)
{
for (int j = 0; j < col; j++)
{
printf("---|");
/*if (j != col - 1)
printf("---|");
else
printf("---\n");*/
}
printf("\n");
}
}
}
void PlayerMove(char board[ROW][COL], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要下棋的坐标>\n");
scanf("%d%d", &x, &y);
if ((x > 0 && x < 4) && (y > 0 && y < 4))
{
if (board[x - 1][y - 1] == ' ')
{
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");
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)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 0;
}
}
return 1;
}
char IsWin(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][2] != ' ')
return board[i][0];
}
for (int i = 0; i < row; i++)
{
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[2][i] != ' ')
return board[0][i];
}
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[2][2] != ' ')
return board[0][0];
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[2][0] != ' ')
return board[0][2];
if (IsFull(board,ROW,COL) == 1)
return 'Q';
return 'C';
}
game.h文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#define ROW 3
#define COL 3
void InitBoard(char board[ROW][COL],int row,int col);
void PrintBoard(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);
game.c文件
#include"game.h"
void menu()
{
printf("*****************************\n");
printf("****** 1、开始游戏 ********\n");
printf("****** 0、退出游戏 ********\n");
printf("*****************************\n");
}
void game()
{
//初始化棋盘
char board[ROW][COL];
InitBoard(board,ROW,COL);
system("cls");
//打印棋盘
PrintBoard(board, ROW, COL);
//玩家下棋
//电脑下棋
while (1)
{
PlayerMove(board,ROW,COL);
system("cls");
PrintBoard(board, ROW, COL);
//判断输赢
if (IsWin(board,ROW,COL) != 'C')
break;
ComputerMove(board,ROW,COL);
system("cls");
PrintBoard(board,ROW,COL);
if (IsWin(board,ROW,COL) != 'C')
break;
}
if (IsWin(board,ROW,COL) == '*')
printf("玩家赢!!!\n");
else if (IsWin(board,ROW,COL) == '#')
printf("电脑赢!!!\n");
else if (IsWin(board,ROW,COL) == 'Q')
printf("平局!!!\n");
Sleep(2000);
system("cls");
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择>");
scanf("%d",&input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
}