计算概论 A 大作业-简易版不围棋实验报告
核心算法
以蒙特卡洛方法为核心算法。solve.cpp
中实现了对于输入的一个棋盘状态,输出下一步下在哪个位置,用于支持电脑玩家(AI)对玩家对战。
具体地,对于每次输入的一个棋盘状态,我们新建一棵蒙特卡洛树。树上的每个节点都对应了一个棋盘状态,若节点 \(x\) 是节点 \(y\) 的父亲,则在 \(x\) 对应的棋盘上再下一步就可以得到 \(y\) 对应的棋盘。每个节点 \(x\) 还有两个参数, \(v_x\) 表示若干次更新的权值总和,\(n_x\) 表示更新的次数,在之后的 UCB 公式中会用到。
初始时树中仅有根节点,其状态即为输入的棋盘状态。随后进行若干次拓展操作(用 clock
函数控制总用时)。
每次拓展操作从根节点出发进行探索,若当前探索到的节点为终止节点(即棋盘上没有合法的位置可以继续下棋,胜负已分),则结束本次拓展操作。
否则,若当前节点先前未被探索过,就找出它所有可能的子节点(即下一步合法的棋能到达的状态),将这些节点加入蒙特卡洛树中,并从这些节点中随机选一个继续探索;若当前节点已经被探索过,则根据 UCB 公式选取一个最优的子节点继续探索。UCB 公式为
该公式给出了子节点 \(i\) 的 UCB 值 \(UCB_i\) 的计算方法,其中 \(\bar v\) 表示该子节点的 \(\frac v n\),\(N\) 表示当前节点所有子节点 \(n\) 的总和,\(c\) 为常数因子,本程序中取 \(c=0.2\) 实现。子节点的 \(\bar v\) 越大,说明走到该子节点更可能获得较大的价值,而子节点的 \(n\) 越大,说明该子节点被探索的程度更充分,应考虑探索其他探索较不充分的子节点。UCB 公式给出了对这两个参数的一个均衡,我们在计算每个子节点的 UCB 值后,选取 UCB 值最大的子节点继续探索。
当拓展到终止节点时结束本次探索,用一个估价函数对这个最终状态进行评估:
其中 \(t_1\) 表示最终状态下我方还能落子的位置数目,\(t_2\) 表示最终状态下对方还能落子的位置数目。在计算出最终状态的价值 \(val\) 后,回溯更新终止节点到根节点路径上每个节点的 \(v\) 与 \(n\) 。
在所有的拓展操作结束后,从根节点的所有子节点中选出 \(\bar v\) 最大的子节点,算法给出的这一步棋即为从根节点到该子节点的一步棋。
主要功能以及实现方法
规则介绍
参考了 Botzone Wiki 上对 Nogo 游戏规则的介绍。
存盘,读盘,复盘
一个棋局的状态只需要记录当前棋盘上每个格子被黑子占据/被白子占据/未被占据,以及下一步应该下黑棋还是白棋。于是可以将一个棋局的状态用一个 9 * 9
的二维数组,以及一个 int
进行存储,并封装在结构体中。
利用一个结构体数组即可存储若干个相互独立的棋局,用于实现存盘与读盘。
玩家可以在轮到自己下棋时暂停此局游戏,并进行存档,之后可以继续此局游戏。
对于一局已经结束的游戏,玩家可以进行回放(复盘),程序会依次输出该局游戏中玩家与电脑下每一步后的棋盘状态。为了避免输出过快,利用 sleep
函数(windows 系统下为 Sleep
函数)控制输出速度。
棋盘与棋子的可视化
使用字符画画出棋盘,对于棋盘中每个格子,若未落子则输出空格,否则输出该位置上对应的棋子字符。
在棋盘的边缘上在棋盘边缘输出了每行每列对应的行号列号,便于玩家确定每个格子对应的坐标(行列编号)。
玩家落子
轮到玩家(人类)落子时,控制台出现提示落子的文字,玩家(人类)输入欲下棋位置的横纵坐标即可完成操作。
若输入不合法,则会提示玩家重新输入。
电脑或玩家每下一步后,都用 system("clear");
清空控制台的输出,并重新输出新的棋盘状态。
提示
棋局下到中后盘时,棋盘中合法的落子位置会较少,玩家可以通过提示获得所有可落子的合法位置。
悔棋
在游戏结束前,轮到玩家下棋时,若不为第一步,则玩家可以选择悔棋,但每次只能悔一步棋。
胜负判定
solve.cpp
中实现了对于一个棋盘状态,判断当前行动方是否有合法的下棋位置,若无位置可下,则另一方获胜。双方每下一步后,都进行一次判断,当分出胜负时结束该局游戏,并重新回到初始界面。