关于评论区提出的问题,我补充一下,这篇代码是pku同学《计算概论A2020》的大作业,代码是需要提交在botzone上的,文章中有些代码是与botzone的交互,具体交互过程与规则见维基百科botzone不围棋的介绍。
1. 不围棋规则简介
某日,助教MorroWind遇到了围棋十连败。他看着因为被围失气被迫出具的我方棋子,感觉围棋这个游戏十分不讲武德,武林要以和为贵。MorrowWind越想越气,很快啊,他“啪”的一声站了起来,怒目圆睁,指着对手的鼻子吼道:
“天天围我的子搞偷袭,算什么能耐,有本事咱们谁都不围对手的棋子,谁围上谁输!” 他的对手歪嘴一笑 “年轻人,你要耗子尾汁”
各位同学,你们能写一个程序,帮一帮MorroWind来打败他的对手么?
目标:想方设法不要让自己提走对方的棋子(围住),尽可能让自己的棋子被对方提走(围住)。
什么是“气:**要理解“提走”,首先要理解什么是“气”。一个棋子在棋盘上,与它 直线紧邻的空点是这个棋子的“气”。 棋子直线紧邻的点上,如果有同色棋子存在,则 它们便相互连接成一个不可分割的整体。它们的气也应一并计算。
什么是“提走”:当一个棋子没有“气”的时候它就要被提走。棋子直线紧邻的点上,如果有异色棋子存在,这口气就不复存在。如所有的气均为对方所占据,便呈无气状态。无气状态的棋子不能在棋盘上存在,也就是提子。把无气之子提出盘外的手段叫“提子”。
棋盘的规格:如图所示,不围棋的棋盘大小是99。注意,这里的99 指的是格点的数目,并不是格子的数量,因为棋子要下在格点上。
落子先后:黑子先手,双方轮流落子,落子后棋子不可移动。
判负条件:不围棋没有平局。一方输掉比赛,当且仅当以下至少一条发生: 1)如果一方落子后吃掉了对方的棋子,则落子一方判负; 2)对弈禁止自杀,落子自杀一方判负; 3)对弈禁止空手(pass),空手一方判负。
开局限制:黑棋第一手禁止下在棋盘正中央(待议)。
2. 思路提示
【最直接的人工策略设计】 助教MorroWind话音刚落,就有高手“啪”地站起来了:
“棋盘上的格点,要么黑方下或白方下必输,要么随便下不影响输赢。最简单的想法,你让棋盘上只剩下那些对方一下就输的格点就完了。不过,有些格点不管黑的还是白的,谁下都会输。棋盘长成这种类型,你就这样……”
高手到黑板上开始画棋谱,这些棋谱都体现出了一些特征,根据这些特征,有一些应对的策略。这些策略,有的很直观,有的很难懂。很快啊,黑板就被画满了。有勤奋的同学就把棋谱抄了下来,写成程序让助教去PK。面对有备而来的AI,助教一次都没赢过。
不过后来这些棋谱失传了,江湖上只剩下了“眼”,“打吃”等不知道是什么意思的名词。【简单步法搜索】 一位同学说,这样设计规则太不“计算机”了,比的是谁下棋能力强。咱们应该让计算机干更多的活。 鲁迅曾经说过,第一层的打不过第五层的。想要起飞就必须懂得这个道理。具体来说,你在做决策的时候必须要考虑到对手的决策,自己根据对手的决策做出的决策,对手根据你对于对手的决策做出的决策所做出的决策,,,如此套娃。如果两个人都足够聪明,只不过记忆力有差别,你比他能够记住多套一层的结果,那么你就赢了。如果现在是你要下棋,你面对的场面是n,你应该考虑你的对手将如何对你的决定进行反应。假设你可以做出m种合法的下子方法,下子方法i对应的棋盘场面是n!,那么你的对手一定会采取在场面n!下对他最有利的走法,假设这种走法对他的收益是v!。显而易见,你下棋的目的是为了让对手即便使用了最好的策略也得收获最少的好处,因此你在这一步做的决策应该是让棋盘变成对手采用最优解获得的收益最小的那个场面的决策。对手收益最小,你的收益就是最大的,而你下棋的决策就应该是让你获得最大收益的决策。假设对手最小的收益是v
= min{v", v#,…v$},那么你的收益就是−v。但你如何获得v!,你就要想象你是对手,正在根据你的决策,运用同样的决策过程进行他的选择。如此往复循环下去,直到你不想继续了,或者游戏结束了,分出胜负了。显然这是一个递归的过程,以上的方法叫做负极大值搜索。不过这样设计有两个问题。其一,搜索空间太大了。这一点我们可以设计剪枝和限制搜索层数来部分解决;其二,根据课上学的,这里的搜索是一个递归的过程,递归要有一个终结的点,也就是搜索树的叶子节点,那叶子节点的值我们怎样评估呢? MorroWind说: “我要是会,我还要问你?”
【MCTS 搜索】
有同学说,我规则不会写,剪枝又不会判,只能在第二层,别人都在第五层,只能退课才能维持的了绩点这样子(可能这个作业发布的时候退课已经截止了)
不过题还是要做的。我们想一想,比别人层数低的原因是什么,不就是追求了搜索的覆盖面导致不够深入。那么我们为什么不收紧一些搜索的宽度,去追求一些搜索的深度呢?
一个最基础的想法是,从当前局面出发,我们进行K个完整的对局,每个对局的每一步的产生,可以是随机的,也可以是根据一定规则的(比如著名的UCT算法)。接下来我们要选择我们的动作。假如我们要选择的动作是a!,那么我们就看看这完成的K个对局中有哪些是以a!开始的,这些以a!开始的对局中有哪些是胜利的,这样我们就粗略地估计了a!的胜率。我们选择胜率最高的那个动作即可。
不过这里也有很多技巧,比如说,我们进行的对局数越多越好,但是时间不允许,我们是不是可以每次不把对局下完,用其他的估值方式取代对胜率的计算;或者是,在模拟对局时采用的生成的动作的方法更精细一些,或者……
说到这里,一些同学开始Google各种论文,一些同学默默地退出了直播间,掏出草纸,认真地构造剪枝方法、估值函数和棋盘特征。
“菜鸡助教,你等着,你这辈子都打不赢我的AI。”一个讨厌这个装杯助教的学生如是说。
3. 作业要求
【基本要求】
- 有菜单选择(选择,新开始,存盘,读盘,结束)
- 用字符实现画棋盘和棋子。
- 一方选手是人员(助教),另一方选手是计算机AI。
- 程序输入是人员落子(x", y")。程序要根据输入,在棋盘上显示变化。程序根据算法决定AI方下一步棋子所放的位置,并显示新的棋盘布局。
- 允许中途停止游戏。
- 有复盘的功能(玩到一半,存储棋盘的状态)
【分组】:- 可以一个人一组,最多两人一组。
- 两个人一组时,最多只能一位同学的成绩是优秀。
- 鼓励两人一组,程度好的同学帮助基础差一些的同学。优秀率向着两人一组的情况倾斜。
【成绩评定】- 程序质量:完成基本要求的基础上,鼓励自行发挥。欢迎同学们多动脑,做出好的实验题。
- 工作量:分工要明确,两个人一组时,每个人的工作的最小单位是函数,在源程序上注明每个函数的完成人,以便提问。
- 提交内容:将源程序或程序包(包含源程序)压缩,提交到网站上。
- 实验报告:对程序的设计思路和功能做一个大概的说明,尤其自己认为有独特的地方,在实验报告中突出出来,提交到网站上。
- 验收形式:在规定的时间内,到机房找助教,演示程序,并回答助教提出的问题。
- 评分标准:满分10 分。助教会根据程序质量、回答问题的正确性、功能的完善等指标评定分数。没有参加botzone 比赛的作品不能超过8 分 【提示】
- 在word文档中,把制表符拷贝下来,粘贴到C程序里。用cout输出,可以画出棋盘
- 用数组记录棋盘上的位置
- 每次输出棋盘的状态,都要用刷新命令system(“cls”);
关于不围棋(NOGO)的更多规则以及botzone交互,请见维基百科的不围棋规则与交互
这里有四种不同层次的代码,看你能理解到哪一层了!加油!!!
首先是随机走棋,代码来源botzone,有一定的修改并添加了注释:
// 不围棋(NoGo)样例程序
// 随机策略
// 作者:fffasttime
// 游戏信息:http://www.botzone.org/games#NoGo
#include "jsoncpp/json.h"
#include
#include
#include
#include
using namespace std;
int board[9][9]; //棋盘,表示有无棋子,1是黑子,-1是白子,0是无子
bool dfs_air_visit[9][9]; //棋盘上每一点有没有气,false表示有气,true表示没有气
const int cx[] = {-1, 0, 1, 0};
const int cy[] = {0, -1, 0, 1};
bool inBorder(int x, int y) { return x >= 0 && y >= 0 && x < 9 && y < 9; } //判断是否在边界以内
//true: has air有气
bool air_judge(int fx, int fy)
{
dfs_air_visit[fx][fy] = true; //标记,表示这个位置已经搜过有无气了
bool flag = false;
for (int dir = 0; dir < 4; dir++)
{
int dx = fx + cx[dir], dy = fy + cy[dir];
if (inBorder(dx, dy)) //界内
{
if (board[dx][dy] == 0) //旁边这个位置没有棋子
flag = true;
if (board[dx][dy] == board[fx][fy] && !dfs_air_visit[dx][dy]) //旁边这个位置是没被搜索过的同色棋
if (air_judge(dx, dy))
flag = true;
}
}
return flag;
}
//true: available
bool judgeAvailable(int fx, int fy, int col) //col-color表示棋子的颜色
{
if (board[fx][fy]) //该位置有棋子了
return false;
board[fx][fy] = col;
memset(dfs_air_visit, 0, sizeof(dfs_air_visit));
if (!air_judge(fx, fy)) //fx,fy没气
{
board[fx][fy] = 0;
return false;
}
for (int dir = 0; dir < 4; dir++)
{
int dx = fx + cx[dir], dy = fy + cy[dir];
if (inBorder(dx, dy))
{
if (board[dx][dy] && !dfs_air_visit[dx][dy])
if (!air_judge(dx, dy))
{
board[fx][fy] = 0;
return false;
}
}
}
board[fx][fy] = 0; //回溯
return true;
}
int main()
{
srand((unsigned)time(0));
string str;
int x, y;
// 读入JSON
getline(cin, str);
//getline(cin, str);
Json::Reader reader;
Json::Value input;
reader.parse(str, input);
// 分析自己收到的输入和自己过往的输出,并恢复状态
int turnID = input["responses"].size();
for (int i = 0; i < turnID; i++) //下一回合,复原上一回合的棋局
{
x = input["requests"][i]["x"].asInt(), y = input["requests"][i]["y"].asInt();
if (x != -1)
board[x][y] = 1;
x = input["responses"][i]["x"].asInt(), y = input["responses"][i]["y"].asInt();
if (x != -1)
board[x][y] = -1;
}
x = input["requests"][turnID]["x"].asInt(), y = input["requests"][turnID]["y"].asInt();
if (x != -1)
board[x][y] = 1;
// 输出决策JSON
Json::Value ret;
Json::Value action;
//以下为随机策略
vector<int> available_list; //合法位置表
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++)
if (judgeAvailable(i, j, x == -1 ? 1 : -1))
available_list.push_back(i * 9 + j);
int result = available_list[rand() % available_list.size()];
action["x"] = result / 9;
action["y"] = result % 9;
ret["response"] = action;
Json::FastWriter writer;
cout << writer.write(ret) << endl;
return 0;
}
贪心算法,比随机走棋多了一点点“启发性”,然而“鼠目寸光”的特点让它往往“格局小了”。。。
//不围棋(NoGo)
//贪心算法
//作者:Hoven Chen
#include "jsoncpp/json.h"
#include
#include
#include
#include
using namespace std;
int board[9][9] = {0}; //棋盘,黑子1,白子-1,没有棋子是0
bool visited_by_air_judge[9][9] = {false}; //在air_judge函数判断某一点有无气时作标记,防止重复而死循环
int value[9][9] = {0}; //储存每个位置的“权利值
int dx[4] = {-1, 0, 1, 0}; //行位移
int dy[4] = {0, -1, 0, 1}; //列位移
//对手的color
int opponent_color(int color)
{
if (color == 1)
return -1;
else
return 1;
}
//判断点(x,y)是否在棋盘内
bool inBoard_judge(int x, int y) { return 0 <= x && x < 9 && 0 <= y && y < 9; }
//判断是否有气
bool air_judge(int x, int y)
{
visited_by_air_judge[x][y] = true; //标记,表示这个位置已经搜过有无气了
bool flag = false;
for (int dir = 0; dir < 4; dir++)
{
int x_dx = x + dx[dir], y_dy = y + dy[dir];
if (inBoard_judge(x_dx, y_dy)) //界内
{
if (board[x_dx][y_dy] == 0) //旁边这个位置没有棋子
flag = true;
if (board[x_dx][y_dy] == board[x][y] && !visited_by_air_judge[x_dx][y_dy]) //旁边这个位置是没被搜索过的同色棋
if (air_judge(x_dx, y_dy))
flag = true;
}
}
return flag;
}
//判断能否下颜色为color的棋
bool put_available(int x, int y, int color) //no problem
{
if (board[x][y]) //如果这个点本来就有棋子
return false;
board[x][y] = color;
memset(visited_by_air_judge, 0, sizeof(visited_by_air_judge)); //重置
if (!air_judge(x, y)) //如果下完这步这个点没气了,说明是自杀步,不能下
{
board[x][y] = 0;
return false;
}
for (int i = 0; i < 4; i++) //判断下完这步周围位置的棋子是否有气
{
int x_dx = x + dx[i], y_dy = y + dy[i];
if (inBoard_judge(x_dx, y_dy)) //在棋盘内
{
if (board[x_dx][y_dy] && !visited_by_air_judge[x_dx][y_dy]) //对于有棋子的位置(标记访问过避免死循环)
if (!air_judge(x_dx, y_dy)) //如果导致(x_dx,y_dy)没气了,则不能下
{
board[x][y] = 0; //回溯
return false;
}
}
}
board[x][y] = 0; //回溯
return true;
}
//估值函数,对当前局面进行评估,计算颜色为color的一方比另一方可落子的位置数目多多少(权利值比较)
int evaluate(int color)
{
int right = 0;
int op_color = opponent_color(color);
for (int x = 0; x < 9; x++)
{
for (int y = 0; y < 9; y++)
{
if (put_available(x, y, color))
right++;
if (put_available(x, y, op_color))
right--;
}
}
return right;
}
int main()
{
srand((unsigned)time(0));
string str;
int x, y;
// 读入JSON
getline(cin, str);
int start = clock(); //时间
int timeout = (int)(0.9 * (double)CLOCKS_PER_SEC);
//getline(cin, str);
Json::Reader reader;
Json::Value input;
reader.parse(str, input);
// 分析自己收到的输入和自己过往的输出,并恢复状态
int turnID = input["responses"].size();
//复原棋盘
for (int i = 0; i < turnID; i++) //下一回合,复原上一回合的棋局
{
x = input["requests"][i]["x"].asInt(), y = input["requests"][i]["y"].asInt();
if (x != -1)
board[x][y] = 1;
x = input["responses"][i]["x"].asInt(), y = input["responses"][i]["y"].asInt();
if (x != -1)
board[x][y] = -1;
}
x = input["requests"][turnID]["x"].asInt(), y = input["requests"][turnID]["y"].asInt();
if (x != -1)
board[x][y] = 1;
// 输出决策JSON
Json::Value ret;
Json::Value action;
//以下是搜索策略:贪心算法
int color = -1;
int max_value = INT_MIN;
int best_i[81] = {0}, best_j[81] = {0}, best_num = 0;
memset(value, 0, sizeof(value));
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
if (put_available(i, j, color))
{
board[i][j] = color;
value[i][j] = evaluate(color);
if (value[i][j] > max_value)
max_value = value[i][j];
board[i][j] = 0;
}
else
value[i][j] = INT_MIN;
if (clock() - start > timeout)
break;
}
//if (clock() - start > timeout)
//break;
//cout << clock() - start << endl;
}
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++)
if (value[i][j] == max_value)
{
best_i[best_num] = i;
best_j[best_num] = j;
best_num++;
}
int random = rand() % best_num; //在所有最大value里面随机选
int decision_x = best_i[random];
int decision_y = best_j[random];
action["x"] = decision_x;
action["y"] = decision_y;
ret["response"] = action;
Json::FastWriter writer;
cout << writer.write(ret) << endl;
//cout << clock() - start << endl;
//cout<
return 0;
}
采用极小化极大值算法,能够模拟两到三层的节点,但由于这种方法采用的是“深度优先遍历“且缺少一种高效的遍历策略树的方式,总是机械地采用前序周游的方式遍历策略树,因此仅供参考,不是特别推荐。(我第一次写是踩坑了这种算法,最后发现这种方法存在致命性的缺点导致打不过用蒙特卡洛搜索树的bot呜呜呜)
//不围棋(NoGo)
//极大极小值算法+alpha-beta剪枝
//作者:Hoven Chen
#include "jsoncpp/json.h"
#include
#include
#include
#include
using namespace std;
int MaxDepth = 8; //回形遍历的层数,如果觉得浪费可以设计一个函数在前期(已行步数)适当减小,但后期一定要保证等于8,不然可能会导致出错
int start = 0; //时间
int timeout = (int)(0.90 * (double)CLOCKS_PER_SEC);
int board[9][9] = {0}; //棋盘,黑子1,白子-1,没有棋子是0
bool visited_by_air_judge[9][9] = {false}; //在air_judge函数判断某一点有无气时作标记,防止重复而死循环
int dx[4] = {-1, 0, 1, 0}; //行位移
int dy[4] = {0, -1, 0, 1}; //列位移
//打印棋盘,用于调试
/*void cout_board()
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
cout << setw(3) << board[j][i];
cout << endl;
}
cout << endl;
}*/
//对手的color
int opponent_color(int color)
{
if (color == 1)
return -1;
else
return 1;
}
//判断点(x,y)是否在棋盘内
bool inBoard_judge(int x, int y) { return 0 <= x && x < 9 && 0 <= y && y < 9; }
//判断是否有气
bool air_judge(int x, int y)
{
visited_by_air_judge[x][y] = true; //标记,表示这个位置已经搜过有无气了
bool flag = false;
for (int dir = 0; dir < 4; dir++)
{
int x_dx = x + dx[dir], y_dy = y + dy[dir];
if (inBoard_judge(x_dx, y_dy)) //界内
{
if (board[x_dx][y_dy] == 0) //旁边这个位置没有棋子
flag = true;
if (board[x_dx][y_dy] == board[x][y] && !visited_by_air_judge[x_dx][y_dy]) //旁边这个位置是没被搜索过的同色棋
if (air_judge(x_dx, y_dy))
flag = true;
}
}
return flag;
}
//判断能否下颜色为color的棋
bool put_available(int x, int y, int color) //no problem
{
if (!inBoard_judge(x, y))
return false;
if (board[x][y]) //如果这个点本来就有棋子
return false;
board[x][y] = color;
memset(visited_by_air_judge, 0, sizeof(visited_by_air_judge)); //重置
if (!air_judge(x, y)) //如果下完这步这个点没气了,说明是自杀步,不能下
{
board[x][y] = 0;
return false;
}
for (int i = 0; i < 4; i++) //判断下完这步周围位置的棋子是否有气
{
int x_dx = x + dx[i], y_dy = y + dy[i];
if (inBoard_judge(x_dx, y_dy)) //在棋盘内
{
if (board[x_dx][y_dy] && !visited_by_air_judge[x_dx][y_dy]) //对于有棋子的位置(标记访问过避免死循环)
if (!air_judge(x_dx, y_dy)) //如果导致(x_dx,y_dy)没气了,则不能下
{
board[x][y] = 0; //回溯
return false;
}
}
}
board[x][y] = 0; //回溯
return true;
}
//估值函数,对当前局面进行评估,计算颜色为color的一方比另一方可落子的位置数目多多少(权利值比较)
int evaluate(int color)
{
int right = 0;
int op_color = opponent_color(color);
for (int x = 0; x < 9; x++)
{
for (int y = 0; y < 9; y++)
{
if (put_available(x, y, color))
right++;
if (put_available(x, y, op_color))
right--;
}
}
return right;
}
//Alpha剪枝和Beta剪枝+MaxMin搜索
int AlphaBeta(int color, int depth, int nAlpha, int nBeta, int op_LastMove_x, int op_LastMove_y)
{
if (depth == 0)
return evaluate(-1); //叶子节点返回估值
//还需要继续往下搜索
int now_available_list_x[81] = {0}, now_available_list_y[81] = {0}; //可下位置
int now_available_num = 0;
//回型遍历可下的位置,在对局前期能够先遍历对手刚刚落子的点附近的可行点,提高搜索效率
for (int level = 1; level <= MaxDepth; level++) //i是往外的层数
{
int testMove_x = 0;
int testMove_y = 0;
//正方形左上顶点
testMove_x = op_LastMove_x - level;
testMove_y = op_LastMove_y - level;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
//正方形左边
for (int i = -level + 1; i < level; i++)
{
testMove_x = op_LastMove_x - level;
testMove_y = op_LastMove_y + i;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
else //一条边要么都在board,要么都不在(端点特判)
break;
}
//左下端点
testMove_x = op_LastMove_x - level;
testMove_y = op_LastMove_y + level;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
//正方形的下边
for (int i = -level + 1; i < level; i++)
{
int testMove_x = op_LastMove_x + i;
int testMove_y = op_LastMove_y + level;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
else
break;
}
//右下端点
testMove_x = op_LastMove_x + level;
testMove_y = op_LastMove_y + level;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
//正方形的右边
for (int i = level - 1; i > -level; i--)
{
int testMove_x = op_LastMove_x + level;
int testMove_y = op_LastMove_y + i;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
else //一条边要么都在board,要么都不在(端点特判)
break;
}
//右上端点
testMove_x = op_LastMove_x + level;
testMove_y = op_LastMove_y - level;
if (inBoard_judge(testMove_x, testMove_y))
{
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
//正方形的上边
for (int i = level - 1; i > -level; i--)
{
int testMove_x = op_LastMove_x + i;
int testMove_y = op_LastMove_y - level;
if (inBoard_judge(testMove_x, testMove_y))
if (put_available(testMove_x, testMove_y, color))
{
now_available_list_x[now_available_num] = testMove_x;
now_available_list_y[now_available_num] = testMove_y;
now_available_num++;
}
}
}
//胜负已分,返回估值
if (now_available_num == 0)
return evaluate(-1);
if (color == 1) //判断 节点类型
{ // 极小值节点 MIN层
int score = INT_MAX;
for (int i = 0; i < now_available_num; i++)
{
board[now_available_list_x[i]][now_available_list_y[i]] = color; //生成新节点
int temp_score = AlphaBeta(opponent_color(color), depth - 1, nAlpha, nBeta, now_available_list_x[i], now_available_list_y[i]); //递归搜索子节点
board[now_available_list_x[i]][now_available_list_y[i]] = 0; //撤销搜索过的节点
if (temp_score < score)
score = temp_score;
if (score < nBeta)
nBeta = score; //取极小值
//if (nAlpha >= nBeta)
// break; //alpha剪枝,抛弃后继节点
//没时间了,撤!
//if (clock() - start >= timeout)
// return nBeta;
}
return nBeta; //返回最小值
}
else
{ //取极大值的节点 MAX层
int score = INT_MIN;
for (int i = 0; i < now_available_num; i++) //对每个子节点
{
board[now_available_list_x[i]][now_available_list_y[i]] = color; //生成新节点
int temp_score = AlphaBeta(opponent_color(color), depth - 1, nAlpha, nBeta, now_available_list_x[i], now_available_list_y[i]); //递归搜索子节点
board[now_available_list_x[i]][now_available_list_y[i]] = 0; //撤销搜索过的节点
if (temp_score > score)
score = temp_score;
if (score > nAlpha)
nAlpha = score; //取极大值
if (nAlpha >= nBeta)
break; //nBeta剪枝,抛弃后继节点
//没时间了,快跑!
if (clock() - start >= timeout)
return nAlpha;
}
return nAlpha; //返回最大值
}
}
//end of AlphaBeta pseudocode
int main()
{
srand((unsigned)time(0));
string str;
int x, y;
// 读入JSON
getline(cin, str);
start = clock();
//getline(cin, str);
Json::Reader reader;
Json::Value input;
reader.parse(str, input);
// 分析自己收到的输入和自己过往的输出,并恢复状态
int turnID = input["responses"].size();
//复原棋盘
for (int i = 0; i < turnID; i++) //下一回合,复原上一回合的棋局
{
x = input["requests"][i]["x"].asInt(), y = input["requests"][i]["y"].asInt();
if (x != -1)
board[x][y] = 1; //对方棋子
//cout << evaluate(-1) << endl;
x = input["responses"][i]["x"].asInt(), y = input["responses"][i]["y"].asInt();
if (x != -1)
board[x][y] = -1; //我方棋子
}
x = input["requests"][turnID]["x"].asInt(), y = input["requests"][turnID]["y"].asInt();
if (x != -1)
board[x][y] = 1;
//cout << evaluate(-1) << endl;
//cout_board();
// 输出决策JSON
Json::Value ret;
Json::Value action;
//搜索层数增加
//MaxDepth += (int)(0.04 * turnID);
//以下是搜索策略:极大极小算法
int color = -1; //我方棋子颜色
int max_value = INT_MIN;
int op_LastMove_x = x, op_LastMove_y = y; //对手上一步下的位置
int available_list_x[81] = {0}, available_list_y[81] = {0}; //可下位置
int available_num = 0;
int best_i[81] = {0}, best_j[81] = {0}, best_num = 0;
//如果是黑棋的第一步,那就假设上一步白棋走在左上角(这样设置胜率会高一点点,也可以根据自己兴趣设置)
if (x == -1)
op_LastMove_x = 0, op_LastMove_y = 1;
//搜索可下位置,回型往外遍历,从最接近对手上一步的地方开始
for (int level = 1; level < 9; level++) //i是往外的层数
{
//正方形左边
for (int i = -level; i < level; i++)
{
int testMove_x = op_LastMove_x - level;
int testMove_y = op_LastMove_y + i;
if (inBoard_judge(testMove_x, testMove_y))
{
/* if (testMove_x == 0 && testMove_y == 3)
{
for (int i = 0; i < 9; i++)
{
cout << endl;
for (int j = 0; j < 9; j++)
cout << board[i][j] << ' ';
}
}*/
if (put_available(testMove_x, testMove_y, color))
{
available_list_x[available_num] = testMove_x;
available_list_y[available_num] = testMove_y;
available_num++;
}
}
}
//正方形的下边
for (int i = -level; i < level; i++)
{
int testMove_x = op_LastMove_x + i;
int testMove_y = op_LastMove_y + level;
if (inBoard_judge(testMove_x, testMove_y))
if (put_available(testMove_x, testMove_y, color))
{
available_list_x[available_num] = testMove_x;
available_list_y[available_num] = testMove_y;
available_num++;
}
}
//正方形的右边
for (int i = level; i > -level; i--)
{
int testMove_x = op_LastMove_x + level;
int testMove_y = op_LastMove_y + i;
if (inBoard_judge(testMove_x, testMove_y))
if (put_available(testMove_x, testMove_y, color))
{
available_list_x[available_num] = testMove_x;
available_list_y[available_num] = testMove_y;
available_num++;
}
}
//正方形的上边
for (int i = level; i > -level; i--)
{
int testMove_x = op_LastMove_x + i;
int testMove_y = op_LastMove_y - level;
if (inBoard_judge(testMove_x, testMove_y))
if (put_available(testMove_x, testMove_y, color))
{
available_list_x[available_num] = testMove_x;
available_list_y[available_num] = testMove_y;
available_num++;
}
}
}
//在限定时间内挑选出最优解
for (int i = 0; i < available_num; i++)
{
board[available_list_x[i]][available_list_y[i]] = color;
int temp = AlphaBeta(color, MaxDepth, INT_MIN, INT_MAX, op_LastMove_x, op_LastMove_y);
if (max_value < temp)
{
max_value = temp;
memset(best_i, 0, sizeof(best_i));
memset(best_j, 0, sizeof(best_j));
best_num = 0;
best_i[best_num] = available_list_x[i];
best_j[best_num] = available_list_y[i];
best_num++;
}
else if (max_value == temp)
{
best_i[best_num] = available_list_x[i];
best_j[best_num] = available_list_y[i];
best_num++;
}
board[available_list_x[i]][available_list_y[i]] = 0;
if (clock() - start >= timeout)
break;
//cout << clock() - start << endl;
}
//在所有最优解里面随机选
int random = rand() % best_num;
int decision_x = best_i[random];
int decision_y = best_j[random];
action["x"] = decision_x;
action["y"] = decision_y;
ret["response"] = action;
Json::FastWriter writer;
cout << writer.write(ret) << endl;
//cout << clock() - start << endl;
//cout << max_value << endl;
return 0;
}
MCTS算法的优越性在于使用了UCB公式,利用概率学的知识对“赢面更大”的点分配更多的模拟机会(启发式搜索),在时间有限的情况下做出一点让步,从而更快找到“近似最优解”。
如果要了解蒙特卡洛树搜索和UCB公式,建议先观看这个视频学习一下(众所周知,b站是个学习网站):
MCTS知识——From bilibili
//不围棋(NoGo)
//蒙特卡洛树搜索(MCTS),UCB算法
//作者:Hoven Chen
#pragma GCC optimize(3)
#include "./jsoncpp/json.h"
#include
#include
#include
#include
#include
#include
#include
#define MAXBranchNum 81
using namespace std;
int dx[4] = {-1, 0, 1, 0}; //行位移
int dy[4] = {0, -1, 0, 1}; //列位移
bool visited_by_air_judge[9][9] = {false}; //在air_judge函数判断某一点有无气时作标记,防止重复而死循环
//判断是否在棋盘内
bool inBoard_judge(int x, int y) { return 0 <= x && x < 9 && 0 <= y && y < 9; }
//判断是否有气
bool air_judge(int board[9][9], int x, int y)
{
visited_by_air_judge[x][y] = true; //标记,表示这个位置已经搜过有无气了
bool flag = false;
for (int dir = 0; dir < 4; ++dir)
{
int x_dx = x + dx[dir], y_dy = y + dy[dir];
if (inBoard_judge(x_dx, y_dy)) //界内
{
if (board[x_dx][y_dy] == 0) //旁边这个位置没有棋子
flag = true;
if (board[x_dx][y_dy] == board[x][y] && !visited_by_air_judge[x_dx][y_dy]) //旁边这个位置是没被搜索过的同色棋
if (air_judge(board, x_dx, y_dy))
flag = true;
}
}
return flag;
}
//判断能否下颜色为color的棋
bool put_available(int board[9][9], int x, int y, int color)
{
if (!inBoard_judge(x, y))
return false;
if (board[x][y]) //如果这个点本来就有棋子
return false;
board[x][y] = color;
memset(visited_by_air_judge, 0, sizeof(visited_by_air_judge)); //重置
if (!air_judge(board, x, y)) //如果下完这步这个点没气了,说明是自杀步,不能下
{
board[x][y] = 0;
return false;
}
for (int i = 0; i < 4; ++i) //判断下完这步周围位置的棋子是否有气
{
int x_dx = x + dx[i], y_dy = y + dy[i];
if (inBoard_judge(x_dx, y_dy)) //在棋盘内
{
if (board[x_dx][y_dy] && !visited_by_air_judge[x_dx][y_dy]) //对于有棋子的位置(标记访问过避免死循环)
if (!air_judge(board, x_dx, y_dy)) //如果导致(x_dx,y_dy)没气了,则不能下
{
board[x][y] = 0; //回溯
return false;
}
}
}
board[x][y] = 0; //回溯
return true;
}
//找到能下的位置,result[9][9]表示各个位置的情况,0不能下,1可以下;该函数返回值是可下的位置数,也即result==1的点数
int getValidPositions(int board[9][9], int result[9][9])
{
memset(result, 0, MAXBranchNum * 4);
int right = 0;
for (int x = 0; x < 9; ++x)
{
for (int y = 0; y < 9; ++y)
{
if (put_available(board, x, y, 1))
{
right++;
result[x][y] = 1;
}
}
}
return right;
}
//关于类如果有不清楚的知识点建议访问菜鸟教程:
//[类的相关知识](https://www.runoob.com/cplusplus/cpp-classes-objects.html)
//类定义树节点
class treeNode
{
public:
treeNode *parent; //父节点
treeNode *children[MAXBranchNum]; //子节点
int board[9][9];
int childrenAction[MAXBranchNum][2];
int childrenCount;
int childrenCountMax;
double value; //该节点的总value
int n; //当前节点探索次数,UCB中的ni
double UCB; //当前节点的UCB值
int *countPointer; //总节点数的指针
//构造函数
treeNode(int parentBoard[9][9], int opp_action[2], treeNode *parentPointer, int *countp) //构造函数 treeNode *p是父类指针, int *countp应该是总探索次数的指针
{
for (int i = 0; i < 9; ++i) //把棋盘反过来,要落子方是1 ,对手是-1
{
for (int j = 0; j < 9; ++j)
{
board[i][j] = -parentBoard[i][j];
}
}
if (opp_action[0] >= 0 && opp_action[0] < 9 && opp_action[1] >= 0 && opp_action[1] < 9)
board[opp_action[0]][opp_action[1]] = -1;
parent = parentPointer;
value = 0;
n = 0;
childrenCount = 0; //已经拓展的子节点数
countPointer = countp; //count的指针
evaluate(); //计算能下的位置,修改了childrenCountMax、childrenAction
}
treeNode *treeRules() //搜索法则
{
//如果没有位置下了(终局)
if (childrenCountMax == 0)
{
return this; //到达终局当前叶节点
}
//如果是叶节点,Node Expansion,拓展下一层节点
if (childrenCountMax > childrenCount)
{
treeNode *newNode = new treeNode(board, childrenAction[childrenCount], this, countPointer); //拓展一个子节点
children[childrenCount] = newNode;
childrenCount++; //已拓展的子节点数++
return newNode;
}
//计算当前节点的每个子节点的UCB值(点亮某个节点)
for (int i = 0; i < childrenCount; ++i)
{
children[i]->UCB = children[i]->value / double(children[i]->n) + 0.2 * sqrt(log(double(*countPointer)) / double(children[i]->n)); //UCB公式
}
int bestChild = 0;
double maxUCB = 0;
//找出所有子节点中UCB值最大的子节点
for (int i = 0; i < childrenCount; ++i)
{
if (maxUCB < children[i]->UCB)
{
bestChild = i;
maxUCB = children[i]->UCB;
}
}
return children[bestChild]->treeRules(); //对UCB最大的子节点进行下一层搜索
}
//模拟
double simulation()
{
int board_opp[9][9]; //对手棋盘
int res[9][9];
for (int i = 0; i < 9; ++i)
{
for (int j = 0; j < 9; ++j)
{
board_opp[i][j] = -board[i][j];
}
}
int x = getValidPositions(board, res); //落子方可下位置数
int y = getValidPositions(board_opp, res); //非落子方可下位置数
return x - y;
}
void backup(double deltaValue) //回传估值,从当前叶节点以及往上的每一个父节点都加上估值
{
treeNode *node = this;
int side = 0;
while (node != nullptr) //当node不是根节点的父节点时
{
if (side == 1) //落子方
{
node->value += deltaValue;
side--;
}
else //非落子方
{
node->value -= deltaValue;
side++;
}
node->n++; //当前节点被探索次数++
node = node->parent;
}
}
private:
void evaluate() //计算能下的位置,修改了childrenCountMax、childrenAction
{
int result[9][9];
int validPositionCount = getValidPositions(board, result); //能下的位置数
int validPositions[MAXBranchNum]; //能下的位置坐标
int availableNum = 0;
for (int i = 0; i < 9; ++i)
{
for (int j = 0; j < 9; ++j)
{
if (result[i][j])
{
validPositions[availableNum] = i * 9 + j; //可下的位置
availableNum++; //可下的位置数
}
}
}
childrenCountMax = validPositionCount; //总共能下的位置数
for (int i = 0; i < validPositionCount; ++i)
{
childrenAction[i][0] = validPositions[i] / 9;
childrenAction[i][1] = validPositions[i] % 9;
}
}
};
//类定义结束 end of class definition
int main()
{
int count = 0; //总计算的节点数(总探索次数,UCB中的N)
int board[9][9] = {0};
srand(clock());
string str;
getline(cin, str);
int start = clock();
int timeout = (int)(0.98 * (double)CLOCKS_PER_SEC);
Json::Reader reader;
Json::Value input;
reader.parse(str, input);
int turnID = input["responses"].size();
int x, y;
for (int i = 0; i < turnID; ++i)
{
x = input["requests"][i]["y"].asInt(), y = input["requests"][i]["x"].asInt();
if (x != -1)
board[x][y] = 1;
x = input["responses"][i]["y"].asInt(), y = input["responses"][i]["x"].asInt();
if (x != -1)
board[x][y] = -1;
}
x = input["requests"][turnID]["y"].asInt(), y = input["requests"][turnID]["x"].asInt();
int opp_action[2] = {x, y}; //对面上一步走了哪里
treeNode rootNode(board, opp_action, nullptr, &count); //创建根节点,根节点的父节点为空
while (clock() - start < timeout)
{
count++; //计算的节点数++
treeNode *node = rootNode.treeRules(); //拓展一次,node指向的是一次拓展的叶节点
double result = node->simulation(); //结果估值
node->backup(result);
}
int bestChildren[MAXBranchNum] = {0}; //所有最优子节点的序号
int bestChildrenNum = 0; //最优子节点个数
int maxValue = INT_MIN; //当前最优子节点分数
for (int i = 0; i < rootNode.childrenCount; ++i)
{
if (maxValue < rootNode.children[i]->value)
{
//重置
memset(bestChildren, 0, sizeof(bestChildren));
bestChildrenNum = 0;
bestChildren[bestChildrenNum++] = i;
maxValue = rootNode.children[i]->value;
}
else if (maxValue == rootNode.children[i]->value)
{
bestChildren[bestChildrenNum++] = i;
}
}
int random = rand() % bestChildrenNum; //在所有最优中任选一个
int *bestAction = rootNode.childrenAction[bestChildren[random]]; //最优子节点对应走法
Json::Value ret;
Json::Value action;
action["x"] = bestAction[1];
action["y"] = bestAction[0];
ret["response"] = action;
char buffer[4096];
sprintf(buffer, "搜索节点数:%d,平均value:%.5f,用时:%.3f", count, (((double)(rootNode.children[bestChildren[random]]->value)) / ((double)rootNode.children[bestChildren[random]]->n) + 1.0) * 0.5, (double)(clock() - start) / 1000);
ret["debug"] = buffer;
Json::FastWriter writer;
cout << writer.write(ret) << endl;
}
以上就是关于《不围棋》的代码,欢迎大家在评论区批评指正!