前言
之前C语言课的大作业是设计一个可以进行人人对战和人机对战的五子棋程序。我在初期开始写的时候参考过很多份代码,但对于当时我的水平而言不够直观,不容易看懂,所以现在我希望可以更加详细的方式来写一下五子棋程序的实现思路。
现在我想先讲解人人对战部分的实现思路,因为这一部分的实现会简单很多。而人机对战我会在日后进行详细讲解(尽量不鸽),因为人机对战这一部分涉及策略的问题,这包含几个层次,比如让电脑找空位随机落子,更进一步可以在对方活三或其他情况的棋子附近随机落子;更高级的策略可以让计算机考虑到更多的情况和步骤,但是程序也会复杂很多。
此外,因为我懒得考虑情况复杂的禁手,所以代码中也把禁手省略了,有需求的读者可以根据情况自行加入。
显示棋盘
第一步,我们需要先把棋盘绘制出来。为此,用一个15*15的二维数组来储存棋盘上每一个位置的信息(应包括此处为空或者有白字或黑子),把这个数组命名为board,其中每一个元素表示为board[ i ][ j ]。
然后,我们可以用一个函数preboard来对棋盘状态进行初始化。在一开始没有落子的情况下,棋盘是完全由制表符组成的。因此,我们需要将数组board存储的数值与制表符进行一个对应。
void preboard(void){
int i,j;
board[0][0]=1; //┏,棋盘左上角
board[0][SIZE-1]=2; //┓,棋盘右上角
board[SIZE-1][0]=3; //┗,棋盘左下角
board[SIZE-1][SIZE-1]=4; //┛,棋盘右下角
for(j=1;j<SIZE-1;j++){
board[0][j]=5; //┯,棋盘上沿
board[SIZE-1][j]=6; //┷,棋盘下沿
}
for(i=1;i<SIZE-1;i++){
board[i][0]=7; //┠,棋盘左沿
board[i][SIZE-1]=8; //┨,棋盘右沿
}
for(i=1;i<SIZE-1;i++){
for(j=1;j<SIZE-1;j++){
board[i][j]=9; //┼,棋盘中部
}
}
for(i=0;i<SIZE;i++){
for(j=0;j<SIZE;j++){
blackplace[i][j]=0;
whiteplace[i][j]=0; //将未落子的位置统一标记为0
}
}
}
补充注释:SIZE是一个宏,用于表示棋盘的规格,在程序开始之前定义;
blackplace和whiteplace是两个二维数组,分别用于记录当前棋盘黑白子落子的情况。把黑白子的情况分开记录有利于我们后续进行胜负判断时减少工作量。
弄清了棋盘每一个位置的信息的储存方式后,我们就需要一个函数,读取实时的棋盘信息,并根据读取到的信息绘制棋盘,并且使得棋盘的每一个位置能直观地读取到坐标,我们把这个函数命名为displayboard。
void displayboard(void){
int i=0,j=0;
putchar('\n');
for(i=0;i<SIZE;i++){
printf("%2d",i); //标出棋盘纵坐标
for(j=0;j<SIZE;j++){
switch(board[i][j]){
case 1:printf("┏"); break;
case 2:printf("┓"); break;
case 3:printf("┗"); break;
case 4:printf("┛"); break;
case 5:printf("┯"); break;
case 6:printf("┷"); break;
case 7:printf("┠"); break;
case 8:printf("┨"); break;
case 9:printf("┼"); break;
case 10:printf("●");break;
//case 11:printf("▲");break;
case 12:printf("◎");break;
//case 13:printf("△");break;
default: break;
}
}
printf("\n");
}
printf(" A B C D E F G H I J K L M N O \n"); //标出棋盘横坐标
}
绘制出来的棋盘如图所示
在windows的cmd终端运行时可能会出现棋盘比例不正确的情况,这个时候只需要右键-属性-使用旧版控制台,重启终端即可。
执行落子
这里我们用到两个函数,setblack和setwhite,用于执行落子。当这两个函数执行的时候,程序会提示我们输入落子的坐标,当我们输入坐标后,程序需要进行判断这个位置是否在棋盘范围内,是否为空(如果包含禁手规则还需要考虑是否构成禁手)。
判断是否为空的依据就是whiteplace和blackplace相应位置中储存的数字,如果数字是1-9,说明这个位置为空,可以落子;否则不可落子,程序应该做出反馈,提示重新落子。
进行落子时,我们只需要将输入坐标对应的board存储的数值改写即可,同时将最新的落子情况交给blackplace和whiteplace记录下来。最后,使用displayboard绘制棋盘。
void setblack(void){
int xb,ybi;
char yb;
while(1){
xb=ybi=0;
printf("请输入黑子的坐标(格式'3 A'):\n"); //提示落子
scanf("%d %c",&xb,&yb); //输入落子的位置
ybi=trans(yb);
if(xb>=0 && xb<SIZE && ybi>=0 && ybi<SIZE){
if(blackplace[xb][ybi]==0){
blackplace[xb][ybi]=1;
whiteplace[xb][ybi]=2;
board[xb][ybi]=10; //0表示该点为空,1表示该点有黑棋,2表示该点有白棋,10表示在棋盘中落一黑子
displayboard();
rebx=xb;
reby=ybi;
break;
}else{
printf("该点已有棋子,请重试;\n");
}
}else{
printf("超出棋盘范围,请重试;\n");
}
}
}
void setwhite(void){
int xw,ywi;
char yw;
while(1){
xw=ywi=0;
printf("请输入白子的坐标(格式'3 A'):\n"); //提示落子
scanf("%d %c",&xw,&yw); //输入落子的位置
ywi=trans(yw);
if(xw>=0 && xw<SIZE && ywi>=0 && ywi<SIZE){
if(whiteplace[xw][ywi]==0){
whiteplace[xw][ywi]=1;
blackplace[xw][ywi]=2;
board[xw][ywi]=12; //0表示该点为空,1表示该点有白棋,2表示该点有黑棋,12表示在棋盘中落一白子
displayboard();
rewx=xw;
rewy=ywi;
break;
}else{
printf("该点已有棋子,请重试;\n");
}
}else{
printf("超出棋盘范围,请重试;\n");
}
}
}
对函数中的变量做一下简单解释:xb和yb表示落子位置的坐标,xb是整型,yb是字符型,ybi是整型(坐标由数值和字母组成是老师的要求,但表示二维数组元素的i和j必须是整型,因此额外写了一个trans函数将输入的字母坐标转化为整数坐标),xw,yw,ywi同理。
int trans(char c){
switch(c){
case 'A':return 0;
case 'B':return 1;
case 'C':return 2;
case 'D':return 3;
case 'E':return 4;
case 'F':return 5;
case 'G':return 6;
case 'H':return 7;
case 'I':return 8;
case 'J':return 9;
case 'K':return 10;
case 'L':return 11;
case 'M':return 12;
case 'N':return 13;
case 'O':return 14;
}
}
rebx,reby,rewx,rewy用于记录最新一步落子的位置。你可能已经注意到了,在绘制棋盘的时候数值11和13被注释掉了。如果你需要将最新的落子进行强调,你可以使用数值11和13将打印出来的棋子变为三角形,但是在下一步落子后需要改回圆形。
胜负判定
这是整个程序中需要考虑的情况最多的一个部分。我们既需要考虑横向棋子的布局,还需要考虑纵向,更复杂的是还需要考虑斜率分别为-1和1的直线上的落子情况。
void whowins(void){
int i,j,s;
for(i=0;i<SIZE;i++){
for(j=0;j<11;j++){
if(blackplace[i][j]==1 && blackplace[i][j+1]==1 && blackplace[i][j+2]==1 && blackplace[i][j+3]==1 && blackplace[i][j+4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i][j+1]==1 && whiteplace[i][j+2]==1 && whiteplace[i][j+3]==1 && whiteplace[i][j+4]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在每一行寻找连子
for(j=0;j<SIZE;j++){
for(i=0;i<11;i++){
if(blackplace[i][j]==1 && blackplace[i+1][j]==1 && blackplace[i+2][j]==1 && blackplace[i+3][j]==1 && blackplace[i+4][j]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j]==1 && whiteplace[i+2][j]==1 && whiteplace[i+3][j]==1 && whiteplace[i+4][j]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在每一列寻找连子
for(s=4;s<SIZE;s++){
for(i=0,j=s;i+4<=s && j-4>=0;i++,j--){
if(blackplace[i][j]==1 && blackplace[i+1][j-1]==1 && blackplace[i+2][j-2]==1 && blackplace[i+3][j-3]==1 && blackplace[i+4][j-4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j-1]==1 && whiteplace[i+2][j-2]==1 && whiteplace[i+3][j-3]==1 && whiteplace[i+4][j-4]==1){
printf("白棋获胜\n");
exit(1);
}
}
}
for(s=1;s<SIZE-4;s++){
for(i=s,j=SIZE-1;i+4<SIZE && j-4>=s;i++,j--){
if(blackplace[i][j]==1 && blackplace[i+1][j-1]==1 && blackplace[i+2][j-2]==1 && blackplace[i+3][j-3]==1 && blackplace[i+4][j-4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j-1]==1 && whiteplace[i+2][j-2]==1 && whiteplace[i+3][j-3]==1 && whiteplace[i+4][j-4]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在斜率为正的直线上寻找连子
for(s=0;s<SIZE-4;s++){
for(i=0,j=s;i+4<SIZE-s && j+4<SIZE;i++,j++){
if(blackplace[i][j]==1 && blackplace[i+1][j+1]==1 && blackplace[i+2][j+2]==1 && blackplace[i+3][j+3]==1 && blackplace[i+4][j+4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j+1]==1 && whiteplace[i+2][j+2]==1 && whiteplace[i+3][j+3]==1 && whiteplace[i+4][j+4]==1){
printf("白棋获胜\n");
exit(1);
}
}
}
for(s=1;s<SIZE;s++){
for(i=s,j=0;i+4<SIZE && j+4<SIZE-s;i++,j++){
if(blackplace[i][j]==1 && blackplace[i+1][j+1]==1 && blackplace[i+2][j+2]==1 && blackplace[i+3][j+3]==1 && blackplace[i+4][j+4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j+1]==1 && whiteplace[i+2][j+2]==1 && whiteplace[i+3][j+3]==1 && whiteplace[i+4][j+4]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在斜率为负的直线上寻找连子
for(i=0;i<SIZE;i++){
for(j=0;j<SIZE;j++){
if(blackplace[i][j]==0){
goto quit;
}
}
}
printf("平局\n");
exit(1);
quit:s=0;
}
横向和纵向的判断容易理解,斜向的会复杂一些,尤其是在靠近四个角落的地方,因为我们需要保证有足够的空间使得能够有五颗棋子连成一条线,因此在考虑斜向时我又将每种斜率的直线分为了两种情况。我这种判断方法其实是穷举了所有可能的连线情况,就不做深入解释了。我认为我的实现方法不够理想,也欢迎各位提供自己的思路或是对我的方法进行批评指点。
功能综合
以上就是程序最重要的几个部分了,接下来我们只需要按照游戏进程来安排这些函数执行的顺序即可。比如我们需要在程序运行时给玩家选择进行人人对战或是人机对战,执黑或者执白;让计算机依次提醒玩家落子,并且在每一步落子之后进行胜负判断。这些事情要做比较容易,就不再详细解释,直接放完整代码(建议在写这种量较多的代码时用不同的文件储存重要代码片段,最后联合编译)。
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define SIZE 15
char board[SIZE][SIZE]; //该数组用于表示棋盘格局
char whiteplace[SIZE][SIZE]; //该数组用于记录白棋布局
char blackplace[SIZE][SIZE]; //该数组用于记录黑棋布局
void preboard(void); // 该函数用于将棋盘位置分类
void displayboard(void); //该函数用于显示棋盘及落子情况
int rewx,rewy; //记录白子最近一步落子的位置
int rebx,reby; //记录黑子最近一步落子的位置
void setblack(void);
void setwhite(void); //这两个函数用于实现根据指定坐标落子
void chospatn(void); //选择对战模式
void choside(void); //用于选边(人机对战)
void order(void); //用于决定下一步轮到哪一方(人人对战)
void aisetblack(void); //用于AI执黑子时的落子
void aisetwhite(void); //用于AI执白子时的落子
int trans(char c); //用于字母和数字的转换
void pve(void); //pve
void pvp(void); //pvp
void whowins(void); //进行胜负判定
int main(){
preboard();
displayboard();
chospatn();
return 0;
}
void preboard(void){
int i,j;
board[0][0]=1; //┏,棋盘左上角
board[0][SIZE-1]=2; //┓,棋盘右上角
board[SIZE-1][0]=3; //┗,棋盘左下角
board[SIZE-1][SIZE-1]=4; //┛,棋盘右下角
for(j=1;j<SIZE-1;j++){
board[0][j]=5; //┯,棋盘上沿
board[SIZE-1][j]=6; //┷,棋盘下沿
}
for(i=1;i<SIZE-1;i++){
board[i][0]=7; //┠,棋盘左沿
board[i][SIZE-1]=8; //┨,棋盘右沿
}
for(i=1;i<SIZE-1;i++){
for(j=1;j<SIZE-1;j++){
board[i][j]=9; //┼,棋盘中部
}
}
for(i=0;i<SIZE;i++){
for(j=0;j<SIZE;j++){
blackplace[i][j]=0;
whiteplace[i][j]=0; //将未落子的位置统一标记为0
}
}
}
void displayboard(void){
int i=0,j=0;
putchar('\n');
for(i=0;i<SIZE;i++){
printf("%2d",i); //标出棋盘纵坐标
for(j=0;j<SIZE;j++){
switch(board[i][j]){
case 1:printf("┏"); break;
case 2:printf("┓"); break;
case 3:printf("┗"); break;
case 4:printf("┛"); break;
case 5:printf("┯"); break;
case 6:printf("┷"); break;
case 7:printf("┠"); break;
case 8:printf("┨"); break;
case 9:printf("┼"); break;
case 10:printf("●");break;
//case 11:printf("▲");break;
case 12:printf("◎");break;
//case 13:printf("△");break;
default: break;
}
}
printf("\n");
}
printf(" A B C D E F G H I J K L M N O \n"); //标出棋盘横坐标
}
int trans(char c){
switch(c){
case 'A':return 0;
case 'B':return 1;
case 'C':return 2;
case 'D':return 3;
case 'E':return 4;
case 'F':return 5;
case 'G':return 6;
case 'H':return 7;
case 'I':return 8;
case 'J':return 9;
case 'K':return 10;
case 'L':return 11;
case 'M':return 12;
case 'N':return 13;
case 'O':return 14;
}
}
void setblack(void){
int xb,ybi;
char yb;
while(1){
xb=ybi=0;
printf("请输入黑子的坐标(格式'3 A'):\n"); //提示落子
scanf("%d %c",&xb,&yb); //输入落子的位置
ybi=trans(yb);
if(xb>=0 && xb<SIZE && ybi>=0 && ybi<SIZE){
if(blackplace[xb][ybi]==0){
blackplace[xb][ybi]=1;
whiteplace[xb][ybi]=2;
board[xb][ybi]=10; //0表示该点为空,1表示该点有黑棋,2表示该点有白棋,10表示在棋盘中落一黑子
displayboard();
rebx=xb;
reby=ybi;
break;
}else{
printf("该点已有棋子,请重试;\n");
}
}else{
printf("超出棋盘范围,请重试;\n");
}
}
}
void setwhite(void){
int xw,ywi;
char yw;
while(1){
xw=ywi=0;
printf("请输入白子的坐标(格式'3 A'):\n"); //提示落子
scanf("%d %c",&xw,&yw); //输入落子的位置
ywi=trans(yw);
if(xw>=0 && xw<SIZE && ywi>=0 && ywi<SIZE){
if(whiteplace[xw][ywi]==0){
whiteplace[xw][ywi]=1;
blackplace[xw][ywi]=2;
board[xw][ywi]=12; //0表示该点为空,1表示该点有白棋,2表示该点有黑棋,12表示在棋盘中落一白子
displayboard();
rewx=xw;
rewy=ywi;
break;
}else{
printf("该点已有棋子,请重试;\n");
}
}else{
printf("超出棋盘范围,请重试;\n");
}
}
}
void chospatn(void){
int p;
while(1){
p=0;
printf("请选择对战模式:输入0进行人机对战,输入1进行人人对战\n");
scanf("%d",&p);
if(p==0){
pve(); break;
}else if(p==1){
pvp(); break;
}else{
printf("输入不符合规范,请重新输入\n");
}
}
}
void choside(void){ //调用当且进当选择人机对战
int c;
while(1){
c=0;
printf("请选边:输入数字0选择黑棋,输入数字1选择白棋:\n"); //提示选边
scanf("%d",&c);
if(c==0){
while(1){
setblack();
whowins();
aisetwhite();
whowins();
}
}else if(c==1){
while(1){
aisetblack();
whowins();
setwhite();
whowins();
}
}else{
printf("输入不符合规范,请重新输入:\n");
}
}
}
void order(void){ //人人对战的落子顺序
while(1){
setblack();
whowins();
setwhite();
whowins();
}
}
void whowins(void){
int i,j,s;
for(i=0;i<SIZE;i++){
for(j=0;j<11;j++){
if(blackplace[i][j]==1 && blackplace[i][j+1]==1 && blackplace[i][j+2]==1 && blackplace[i][j+3]==1 && blackplace[i][j+4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i][j+1]==1 && whiteplace[i][j+2]==1 && whiteplace[i][j+3]==1 && whiteplace[i][j+4]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在每一行寻找连子
for(j=0;j<SIZE;j++){
for(i=0;i<11;i++){
if(blackplace[i][j]==1 && blackplace[i+1][j]==1 && blackplace[i+2][j]==1 && blackplace[i+3][j]==1 && blackplace[i+4][j]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j]==1 && whiteplace[i+2][j]==1 && whiteplace[i+3][j]==1 && whiteplace[i+4][j]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在每一列寻找连子
for(s=4;s<SIZE;s++){
for(i=0,j=s;i+4<=s && j-4>=0;i++,j--){
if(blackplace[i][j]==1 && blackplace[i+1][j-1]==1 && blackplace[i+2][j-2]==1 && blackplace[i+3][j-3]==1 && blackplace[i+4][j-4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j-1]==1 && whiteplace[i+2][j-2]==1 && whiteplace[i+3][j-3]==1 && whiteplace[i+4][j-4]==1){
printf("白棋获胜\n");
exit(1);
}
}
}
for(s=1;s<SIZE-4;s++){
for(i=s,j=SIZE-1;i+4<SIZE && j-4>=s;i++,j--){
if(blackplace[i][j]==1 && blackplace[i+1][j-1]==1 && blackplace[i+2][j-2]==1 && blackplace[i+3][j-3]==1 && blackplace[i+4][j-4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j-1]==1 && whiteplace[i+2][j-2]==1 && whiteplace[i+3][j-3]==1 && whiteplace[i+4][j-4]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在斜率为正的直线上寻找连子
for(s=0;s<SIZE-4;s++){
for(i=0,j=s;i+4<SIZE-s && j+4<SIZE;i++,j++){
if(blackplace[i][j]==1 && blackplace[i+1][j+1]==1 && blackplace[i+2][j+2]==1 && blackplace[i+3][j+3]==1 && blackplace[i+4][j+4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j+1]==1 && whiteplace[i+2][j+2]==1 && whiteplace[i+3][j+3]==1 && whiteplace[i+4][j+4]==1){
printf("白棋获胜\n");
exit(1);
}
}
}
for(s=1;s<SIZE;s++){
for(i=s,j=0;i+4<SIZE && j+4<SIZE-s;i++,j++){
if(blackplace[i][j]==1 && blackplace[i+1][j+1]==1 && blackplace[i+2][j+2]==1 && blackplace[i+3][j+3]==1 && blackplace[i+4][j+4]==1){
printf("黑棋获胜\n");
exit(1);
}
if(whiteplace[i][j]==1 && whiteplace[i+1][j+1]==1 && whiteplace[i+2][j+2]==1 && whiteplace[i+3][j+3]==1 && whiteplace[i+4][j+4]==1){
printf("白棋获胜\n");
exit(1);
}
}
} //在斜率为负的直线上寻找连子
for(i=0;i<SIZE;i++){
for(j=0;j<SIZE;j++){
if(blackplace[i][j]==0){
goto quit;
}
}
}
printf("平局\n");
exit(1);
quit:s=0;
}
void pvp(void){ //加载人人对战
order();
}
void pve(void){ //加载人机对战
choside();
}
void aisetblack(void){
int i,j;
for(i=0;i<SIZE;i++){
for(j=0;j<SIZE;j++){
if(blackplace[i][j]==0){
blackplace[i][j]=1;
whiteplace[i][j]=2;
board[i][j]=10;
displayboard();
goto out;
}
}
}
out:i=j=0;
}
void aisetwhite(void){
int i,j;
for(i=0;i<SIZE;i++){
for(j=0;j<SIZE;j++){
if(whiteplace[i][j]==0){
whiteplace[i][j]=1;
blackplace[i][j]=2;
board[i][j]=12;
displayboard();
goto out;
}
}
}
out:i=j=0;
}
最后用到的两个函数就是人机对战时计算机所使用的策略,这里就用完全空白处落子来代替复杂的策略。
对这篇文章中出现的错误,在此一并致歉,也欢迎各位读者批评指正或是提出建议。