先大致了解一下黑白棋:
规则
如果玩家在棋盘上没有地方可以下子,则该玩家对手可以连下。双方都没有棋子可以下时棋局结束,以棋子数目来计算胜负,棋子多的一方获胜。
在棋盘还没有下满时,如果一方的棋子已经被对方吃光,则棋局也结束。将对手棋子吃光的一方获胜。
翻转棋类似于棋盘游戏“奥赛罗 (Othello)”,是一种得分会戏剧性变化并且需要长时间思考的策略性游戏。
翻转棋的棋盘上有 64 个可以放置黑白棋子的方格(类似于国际象棋和跳棋)。游戏的目标是使棋盘上自己颜色的棋子数超过对手的棋子数。
该游戏非常复杂,其名称就暗示着结果的好坏可能会迅速变化。
当游戏双方都不能再按规则落子时,游戏就结束了。通常,游戏结束时棋盘上会摆满了棋子。结束时谁的棋子最多谁就是赢家。
玩法
每个“翻转棋”游戏开始时,棋盘上已经交叉放好了四颗棋子。其中两颗是黑棋,另两颗是白棋。黑棋总是先走。
当您的棋子在某一直线方向包围了对手的棋子时,就可以翻转这些棋子的颜色,使它们成为您方的颜色。例如,如果您执黑棋,并且看到在一排白棋的某一端是一颗黑棋,那么当您将一颗黑棋放在这一排的另一端时,所有的白棋都将翻转并变为黑棋!
所有的直线方向均有效:水平、垂直和斜线方向。
走棋的唯一规则是只能走包围并翻转对手的棋子。每一回合都必须至少翻转一颗对手的棋子。
按规则不能再走棋时,这一回合弃权。这一步的行棋权将被交给对方。
由以上可知,在做黑白棋人机对战时,AI要遵守游戏规则。根据
感知(Sense)→思考(Think)→行动(Act)
这个基本架构去设计AI
感知玩家下的棋子位置;
思考我方下子后,增加多少分,玩家接下来走的位置,会减少我方多少分,以相差最高为标准,确定下棋位置;
行动落子;
根据以上分析,编写程序,代码如下:
#include <stdio.h>
//显示棋盘上棋子的状态
void Output(char chessboard[][8])
{
int row, col;
printf("\n ");
//输出列标号
for (col = 0; col < 8; col++)
{
printf(" %c ", ‘A‘ + col);
}
printf("\n");
//输出项部横线
printf(" ┌");
//输出一行
for (col = 0; col < 7; col++)
{
printf("─┬");
}
printf("─┐\n");
for (row = 0; row < 8; row++)
{
//输出行号
printf("%2d│", row + 1);
//输出棋盘各单元格中棋子的状态
for (col = 0; col < 8; col++)
{
if (chessboard[row][col] == 1)//白棋
{
printf("○│");
}
else if (chessboard[row][col] == -1)//黑棋
{
printf("●│");
}
else//未下子处
{
printf(" │");
}
}
printf("\n");
if (row < 8 - 1)
{
printf(" ├"); //输出交叉线
//输出一行
for (col = 0; col < 8 - 1; col++)
{
printf("─┼");
}
printf("─┤\n");
}
}
printf(" └");
//最后一行的横线
for (col = 0; col < 8 - 1; col++)
{
printf("─┴");
}
printf("─┘\n");
}
//检查某一方是否还有下子的地方
int Check(char chessboard[][8], int isDown[][8], char player)
{
int rowdelta, coldelta, row, col, x, y = 0;
int iStep = 0;
char opponent = (player == 1) ? -1 : 1; //对方棋子
char myplayer = -1 * opponent; //我方棋子
//将isDown数组全部清0
for (row = 0; row < 8; row++)
{
for (col = 0; col < 8; col++)
{
isDown[row][col] = 0;
}
}
//循环判断棋盘中哪些单元格可以下子
for (row = 0; row < 8; row++)
{
for (col = 0; col < 8; col++)
{
//若棋盘上对应位置不为空(表示已经有子)
if (chessboard[row][col] != 0)
{
continue;//继续处理下一个单元格
}
//循环检查上下行
for (rowdelta = -1; rowdelta <= 1; rowdelta++)
{
//循环检查左右列
for (coldelta = -1; coldelta <= 1; coldelta++)
{
//检查若坐标超过棋盘 或为当前单元格
if (row + rowdelta < 0 || row + rowdelta >= 8
|| col + coldelta < 0 || col + coldelta >= 8
|| (rowdelta == 0 && coldelta == 0))
{
continue; //继续循环
}
//若(row,col)四周有对手下的子
if (chessboard[row + rowdelta][col + coldelta] == opponent)
{
//以对手下子位置为坐标
x = row + rowdelta;
y = col + coldelta;
//对对手下子为起始点,向四周查找自己方的棋子,以攻击对方棋子
while(1)
{
//对手下子的四周坐标
x += rowdelta;
y += coldelta;
//超过棋盘
if (x < 0 || x >= 8 || y < 0 || y >= 8)
{
break; //退出循环
}
//若对应位置为空
if (chessboard[x][y] == 0)
{
break;
}
//若对应位置下的子是当前棋手的
if (chessboard[x][y] == myplayer)
{
//设置移动数组中对应位置为1 (该位置可下子,形成向对手进攻的棋形)
isDown[row][col] = 1;
iStep++; //累加可下子的位置数量
break;
}
}
}
}
}
}
}
//返回可下的位置数量(若返回值为0,表示没地方可下)
return iStep;
}
//在指定位置下子
void PlayStep(char chessboard[][8], int row, int col, char player)
{
int rowdelta = 0;
int coldelta = 0;
int x = 0;
int y = 0;
char opponent = (player == 1) ? -1 : 1; //对方棋子
char myplayer = -1 * opponent; //我方棋子
chessboard[row][col] = myplayer; //保存所下的棋子
//检查所下子四周的棋子
for (rowdelta = -1; rowdelta <= 1; rowdelta++)
{
for (coldelta = -1; coldelta <= 1; coldelta++)
{
//若坐标超过棋盘界限
if (row + rowdelta < 0 || row + rowdelta >= 8 || col + coldelta < 0
|| col + coldelta >= 8 || (rowdelta == 0 && coldelta == 0))
{
continue; //继续下一位置
}
//若该位置是对手的棋子
if (chessboard[row + rowdelta][col + coldelta] == opponent)
{
//以对手棋为坐标
x = row + rowdelta;
y = col + coldelta;
//在对手棋子四周寻找我方棋子
while(1)
{
x += rowdelta;
y += coldelta;
//若坐标超过棋盘
if (x < 0 || x >= 8 || y < 0 || y >= 8)
{
break; //退出循环
}
//若对应位置为空
if (chessboard[x][y] == 0)
{
break; //退出循环
}
//若对应位置是我方模子
if (chessboard[x][y] == myplayer)
{
//循环处理
while (chessboard[x -= rowdelta][y -= coldelta] == opponent)
{
//将中间的棋子都变成我方棋子
chessboard[x][y] = myplayer;
}
break; //退出循环
}
}
}
}
}
}
//获取分数
int GetMaxScore(char chessboard[][8], char player)
{
int Score, row, col;
char opponent = (player == 1) ? -1 : 1; //对方棋子
char myplayer=-1*opponent;
for (row = 0; row < 8; row++) //循环
{
for (col = 0; col < 8; col++)
{
//若棋盘对应位置是对手下的棋子,从总分中减1
Score -= chessboard[row][col] == opponent;
//若棋盘对应位置是我方的棋子,总分中加1分
Score += chessboard[row][col] == myplayer;
}
}
return Score;//返回分数
}
//获取最佳下子位置
int BestPlay(char chessboard[][8], int isDown[][8], char player)
{
int row, col, i, j;
//定义一个临时数组
char chessboard1[8][8] = { 0 };
int MaxScore = 0; //保存最高分
int Score = 0;
char opponent = (player == 1) ? -1 : 1; //对手下的棋子
//循环检查每个单元格
for (row = 0; row < 8; row++)
{
for (col = 0; col < 8; col++)
{
//若该位置不可下子
if (!isDown[row][col])
{
continue; //继续
}
//复制棋盘各单元格下子的状态到临时数组
for (i = 0; i < 8; i++)
{
for (j = 0; j < 8; j++)
{
chessboard1[i][j] = chessboard[i][j];
}
}
//在临时数组中的指定行列下子
PlayStep(chessboard1, row, col, player);
//获取下子后可得到的分数
Score = GetMaxScore(chessboard1, player);
//若原方案得到的分数小于本次下子的分数
if (MaxScore < Score)
{
MaxScore = Score; //保存最高分
}
}
}
return MaxScore;//返回得到的最高分
}
//AI自动下子
void AutoPlayStep(char chessboard[][8], int isDown[][8], char player)
{
int row, col, row1, col1, i, j;
//对方可下子提到的分数和最小分数
int Score = 0, MinScore = 100;
//临时数组,保存棋盘下子位置
char chessboard1[8][8];
//临时数组,保存可下子位置
int isDown1[8][8];
char opponent = (player == 1) ? -1 : 1; //对手下的棋子
for (row = 0; row < 8; row++) //循环检查棋盘每个单元格
{
for (col = 0; col < 8; col++)
{
//若不可下子
if (isDown[row][col] == 0)
{
continue;//继续下一个位置
}
//将棋盘原来的棋子复制到临时数组中
for (i = 0; i < 8; i++)
{
for (j = 0; j < 8; j++)
{
chessboard1[i][j] = chessboard[i][j];
}
}
//试着在临时棋盘中的一个位子下子
PlayStep(chessboard1, row, col, player);
//检查对手是否有地方可下子
Check(chessboard1, isDown1, opponent);
//获得临时棋盘中对方下子的得分情况
Score = BestPlay(chessboard1, isDown1, opponent);
//保存对方得分最低的下法
if (Score < MinScore)
{
MinScore = Score;
row1 = row;
col1 = col;
}
}
}
//AI按最优下法下子
PlayStep(chessboard, row1, col1, player);
}
int main()
{
//保存棋盘中各单元格下子的状态
char chessboard[8][8];
//保存棋盘中各位置是否可以下子,可下子的位置为1,其余位置为0
int isDown[8][8] = { 0 };
int row, col, x, y;
//已下棋子数量
int iCount = 0;
int player = 0; //下棋方
//跳过下子的次数,若为2,表示双方都不能下子
int SkipPlay = 0;
//保存AI和游戏者的得分
int Score[2];
char select;
printf("黑白棋\n\n");
printf("游戏者执黑先下,AI执白,按回车键开始:\n");
scanf("%c", &select);
do
{
//计算下棋方(0表示游戏者,1表示AI)
if (player == 0)
{
player = 1;
}
else
{
player = 0;
}
iCount = 4; //累计下子数
//棋盘各位置清空
for (row = 0; row < 8; row++)
{
for (col = 0; col < 8; col++)
{
chessboard[row][col] = 0;
}
}
//在棋盘中间位置放置白棋
chessboard[3][3] = chessboard[4][4] = 1;
//在棋盘中间位置放置黑棋
chessboard[3][4] = chessboard[4][3] = -1;
printf("\n棋盘初始状态:\n");
//显示初始棋盘下子的状况
Output(chessboard);
do
{
//若是游戏者下棋(下白子)
if (player == 1)
{
player = 0;
//判断是否可下黑子
if (Check(chessboard, isDown, 2))
{
//死循环,直到用户输入正确的坐标为止
while(1)
{
fflush(stdin);
printf("输入下子的位置(行 列):");
scanf("%d%c", &x, &y);
x--; //计算行坐标位置
if(y >= ‘a‘)
{
y = y - ‘a‘ + 1;
}
else
{
y = y - ‘A‘ + 1;
}
y--; //计算列位置
//若行列坐标输入有效
if (x >= 0 && y >= 0 && x < 8 && y < 8 && isDown[x][y])
{
//在指定坐标位置下黑子
PlayStep(chessboard, x, y, 2);
iCount++; //累加下子数
break;
}
else
{
printf("坐标输入错误,请重新输入。\n");
}
}
printf("\n你下子后的状态:\n");
Output(chessboard); //显示棋子状态
printf("按任意键AI下子。\n");
getch();
}
//若无效下子的次数小于2
else if (++SkipPlay < 2)
{
fflush(stdin); //清除输入缓冲区
printf("你没位置可下,按回车键让对方下子。");
scanf("%c", &select);
} else
{
printf("双方都没地方下子,游戏结束!\n");
}
}
//若是AI下棋(下黑子)
else
{
player = 1;
//检查是否可下白子
if (Check(chessboard, isDown, 1))
{
SkipPlay = 0; //清除无效下子次数
//AI下一个白子
AutoPlayStep(chessboard, isDown, 1);
iCount++; //累加下子数
printf("\nAI下子后的状态:\n");
Output(chessboard); //显示棋子状态
}
else
{
//若无效下子次数小于2
if (++SkipPlay < 2)
{
printf("我没位置可走,请你走。\n");
}
else
{
printf("双方都没地方下子,游戏结束!");
}
}
}
}
//下子数量小于64 且无效下子的次数小于2
while (iCount < 64 && SkipPlay < 2);
//显示各双方棋子的状况
Output(chessboard);
Score[0] = Score[1] = 0;//清空计分变量
//循环统计各单元格黑白棋子的数量
for (row = 0; row < 8; row++)
{
for (col = 0; col < 8; col++)
{
//统计黑子数
Score[0] += chessboard[row][col] == -1;
//统计白子数
Score[1] += chessboard[row][col] == 1;
}
}
printf("最终成绩:\n");
printf("AI:%d\n游戏者:%d\n", Score[0], Score[1]);
fflush(stdin); //清空输入缓冲区
printf("继续下一局(y/n)?:");
scanf("%c", &select);
}while (select == ‘y‘ || select == ‘Y‘);
printf("Game Over!\n");
return 0;
}
运行演示:
参考资源:
《零基础学算法》 第三版 戴艳等编 机械工业出版社
代码下载地址
欢迎关注我的微信个人订阅号
每天多学一点0.0