关于评论区提出的问题,我补充一下,这篇代码是pku同学《计算概论A2020》的大作业,代码是需要提交在botzone上的,文章中有些代码是与botzone的交互,具体交互过程与规则见维基百科botzone不围棋的介绍。



目录

  • 1. 不围棋规则简介
  • 2. 思路提示
  • 3. 作业要求
  • 4. 代码
    • 随机策略(random)
    • 贪心算法(greedy algorithm)
    • 极小化极大算法(Minimax算法 )+ α-β剪枝
    • 蒙特卡洛树搜索(MCTS算法)

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. 作业要求

【基本要求】

  1. 有菜单选择(选择,新开始,存盘,读盘,结束)
  2. 用字符实现画棋盘和棋子。
  3. 一方选手是人员(助教),另一方选手是计算机AI。
  4. 程序输入是人员落子(x", y")。程序要根据输入,在棋盘上显示变化。程序根据算法决定AI方下一步棋子所放的位置,并显示新的棋盘布局。
  5. 允许中途停止游戏。
  6. 有复盘的功能(玩到一半,存储棋盘的状态)
    【分组】:
  7. 可以一个人一组,最多两人一组。
  8. 两个人一组时,最多只能一位同学的成绩是优秀。
  9. 鼓励两人一组,程度好的同学帮助基础差一些的同学。优秀率向着两人一组的情况倾斜。
    【成绩评定】
  10. 程序质量:完成基本要求的基础上,鼓励自行发挥。欢迎同学们多动脑,做出好的实验题。
  11. 工作量:分工要明确,两个人一组时,每个人的工作的最小单位是函数,在源程序上注明每个函数的完成人,以便提问。
  12. 提交内容:将源程序或程序包(包含源程序)压缩,提交到网站上。
  13. 实验报告:对程序的设计思路和功能做一个大概的说明,尤其自己认为有独特的地方,在实验报告中突出出来,提交到网站上。
  14. 验收形式:在规定的时间内,到机房找助教,演示程序,并回答助教提出的问题。
  15. 评分标准:满分10 分。助教会根据程序质量、回答问题的正确性、功能的完善等指标评定分数。没有参加botzone 比赛的作品不能超过8 分 【提示】
  16. 在word文档中,把制表符拷贝下来,粘贴到C程序里。用cout输出,可以画出棋盘
  17. 用数组记录棋盘上的位置
  18. 每次输出棋盘的状态,都要用刷新命令system(“cls”);

关于不围棋(NOGO)的更多规则以及botzone交互,请见维基百科的不围棋规则与交互

4. 代码

这里有四种不同层次的代码,看你能理解到哪一层了!加油!!!

随机策略(random)

首先是随机走棋,代码来源botzone,有一定的修改并添加了注释:
// 不围棋(NoGo)样例程序
// 随机策略
// 作者:fffasttime
// 游戏信息:http://www.botzone.org/games#NoGo
#include "jsoncpp/json.h"
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
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;// 读入JSONgetline(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;// 输出决策JSONJson::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;
}

贪心算法(greedy algorithm)

贪心算法,比随机走棋多了一点点“启发性”,然而“鼠目寸光”的特点让它往往“格局小了”。。。
//不围棋(NoGo)
//贪心算法
//作者:Hoven Chen
#include "jsoncpp/json.h"
#include <climits>
#include <cstring>
#include <ctime>
#include <iostream>
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;elsereturn 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;// 读入JSONgetline(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;// 输出决策JSONJson::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;}elsevalue[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<<max_value << endl;return 0;
}

极小化极大算法(Minimax算法 )+ α-β剪枝

采用极小化极大值算法,能够模拟两到三层的节点,但由于这种方法采用的是“深度优先遍历“且缺少一种高效的遍历策略树的方式,总是机械地采用前序周游的方式遍历策略树,因此仅供参考,不是特别推荐。(我第一次写是踩坑了这种算法,最后发现这种方法存在致命性的缺点导致打不过用蒙特卡洛搜索树的bot呜呜呜)

//不围棋(NoGo)
//极大极小值算法+alpha-beta剪枝
//作者:Hoven Chen
#include "jsoncpp/json.h"
#include <climits>
#include <cstring>
#include <ctime>
#include <iostream>
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;elsereturn 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++;}}elsebreak;}//右下端点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 pseudocodeint main()
{srand((unsigned)time(0));string str;int x, y;// 读入JSONgetline(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();// 输出决策JSONJson::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算法)

MCTS算法的优越性在于使用了UCB公式,利用概率学的知识对“赢面更大”的点分配更多的模拟机会(启发式搜索),在时间有限的情况下做出一点让步,从而更快找到“近似最优解”。

如果要了解蒙特卡洛树搜索和UCB公式,建议先观看这个视频学习一下(众所周知,b站是个学习网站):
MCTS知识——From bilibili

//不围棋(NoGo)
//蒙特卡洛树搜索(MCTS),UCB算法
//作者:Hoven Chen
#pragma GCC optimize(3)
#include "./jsoncpp/json.h"
#include <climits>
#include <cstring>
#include <ctime>
#include <iostream>
#include <math.h>
#include <random>
#include <string>
#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;      //该节点的总valueint n;             //当前节点探索次数,UCB中的nidouble 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 definitionint 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;
}

以上就是关于《不围棋》的代码,欢迎大家在评论区批评指正!

基于C++的不围棋NOGO代码-PKU计算概论A大作业-MCTS算法Minimax算法相关推荐

  1. 基于eNSP中大型校园/企业网络规划与设计_ensp综合大作业(ensp综合实验)

    作者:BSXY_19计科_陈永跃 BSXY_信息学院 注:未经允许禁止转发任何内容 基于eNSP中大型校园/企业网络规划与设计_综合大作业(ensp综合实验) 前言及技术/资源下载说明( **未经允许 ...

  2. 基于JavaSwing开发小区物业信息房屋出租管理系统 课程设计 大作业

    基于JavaSwing开发小区物业信息房屋出租管理系统:   (大作业) 开发工具: MyEclipse/Eclipse+Jdk+MySQL数据库 运行视频: 基于JavaSwing开发小区物业信息管 ...

  3. 基于Matlab的交通限速标志的识别系统 数字图像处理大作业

    本大作业为基于Matlab的交通限速标志的识别系统, 考虑到在科技发展的今天,智能汽车行业发展迅速,所以交通限速标志的快速检测及识别对车辆的安全行驶极为重要,因此本系统可以检测到图中的交通限速标志并对 ...

  4. 基于JavaSwing开发讯友桌面通讯录管理软件 课程设计 大作业源码 毕业设计

    基于JavaSwing开发讯友桌面通讯录管理软件:  (毕业设计/大作业) 开发环境: Windows操作系统 开发工具:MyEclipse+Jdk+SQLServer数据库 运行效果图: 基于Jav ...

  5. HTML+CSS仿写京东登陆页面附代码(web前端期末大作业)

    cc 本来想加上JS实现选项卡功能的,奈何本人水平实在有限,用JavaScript用到一半就放弃了,真的不会使用啊!!!给你们看看效果吧. 整体布局 个人认为我这个整体布局还是比较科学的,把这个界面分 ...

  6. 基于Lucene、Servlet新闻搜索引擎——国科大信息检索导论(王斌)大作业

    GitHub地址:https://github.com/mJackie/LTY-Search 详细文档参见 设计文档 作业要求 (1-7个选课学生组成1队),完成以下任务(12月份末考查): 新闻及评 ...

  7. 基于JavaSwing开发学生信息管理系统(SQLServer数据库版本) 毕业设计 课程设计 大作业

    基于JavaSwing开发学生信息管理系统(SQLServer数据库版本):   (大作业) 开发环境: Windows操作系统 开发工具: MyEclipse+Jdk+SQLServer数据库 运行 ...

  8. div+css静态网页设计——海贼王动漫主题(6页) 影视主网页HTML代码 学生网页课程设计期末作业下载 动漫大学生网页设计制作成品下载 漫画网页作业代码下载

    HTML5期末大作业:影视主题网站设计--海贼王动漫主题(6页) 影视主网页HTML代码 文章目录 HTML5期末大作业:影视主题网站设计--海贼王动漫主题(6页) 影视主网页HTML代码 一.作品展 ...

  9. 基于eNSP的IPv4加IPv6的企业/校园网络规划设计(综合实验/大作业)

    作者:BSXY_19计科_陈永跃 BSXY_信息学院_名片v位于结尾处 注:未经允许禁止转发任何内容 基于eNSP的IPv4加IPv6的企业/校园网络规划设计_综合实验/大作业 前言及技术/资源下载说 ...

最新文章

  1. htc one m7刷Linux,HTC One M7刷机教程 HTC One M7线刷教程
  2. spring mvc中filter的设计与实现
  3. lightGBM GPU支持的安装、验证方法
  4. 批量保存到mysql_关于保存批量数据进入mysql
  5. spring学习(四) ———— 整合web项目(SSH)
  6. 百度360之争的背后
  7. zabbix 3.2.3 appliance默认用户名及密码
  8. 剑指Offer面试题:1.实现单例模式
  9. Windows系统中搭建Python编译环境
  10. atitit 指令集概论原理导论 艾提拉著 目录 2. 2.3 CISC和RISC 复杂指令集 1 1. 指令集(IA:InstructionSet)是指CPU指令系统所能识别(翻译)执行的全部指令
  11. win10红警2黑屏_win10每次重启黑屏假死
  12. WAV和WM8978
  13. 苦涩又难理解的IO<1>
  14. java 对汉字(中文)的汉语拼音(发音)进行排序工具类(代码实现)
  15. *** Cisco路由器
  16. Linux下gunicorn用法
  17. jitter单位_抖动(jitter)测量
  18. RDD -- Transformation算子分析
  19. Python - 在for循环体内修改i值
  20. 计算机组成原理基础题库,计算机组成原理自习题库.docx

热门文章

  1. LOCATOR.CONTROL 的变元无效:ORG_LOCATOR_CONTROL=‘‘
  2. 少年强则互联网强!编程猫这份报告藏着一座新金矿
  3. delphi txt文件读写
  4. xshell优化与远程控制服务器
  5. python接口自动化12-流量回放神器:mitmproxy(下)
  6. Xshell下载激活使用心得
  7. vim 自动补全插件YCM 安装 配置
  8. html5调用摄像头功能
  9. Win10+vm15.5.6虚拟机+unlocker3.0.3+“Intel VT-x处于禁用状态”解决方案+安装Sketch软件+汉化(mac系统)
  10. Excel如何插入中国地图进行可视化