深度解析黑白棋AI代码原理(蒙特卡洛搜索树MCTS+Roxanne策略)
文章目录
黑白棋规则
黑白棋是通过相互翻转对方的棋子,最后看棋盘上谁的棋子多来判定胜负的游戏。
黑白棋棋盘共有8行8列共64格
开局时,棋盘正中央的4洛先置放黑自相隔的
4枚棋子
通常黑子先行,双方轮流落子
只要落子和棋盘上任意一枚己方的棋子在一条直线上(横,直,斜线皆可)夹着对方棋
就能将对方的这些棋子转变为己方
如果在任意位置落子都不能夹住对方的任意一颗棋子,就要让对手下子。
当双放皆不能下子时,游戏结束,子多的一方胜利
传统黑白棋策略
传统黑白棋策略包括以下几类:
- a) 贪心策略。
每一步走子都选择使得棋盘上子最多的一步,而不考虑最终的胜负; - b) 确定子策略。
某些子一旦落子后就再也不会被翻回对方的子,最典型的是四个角上的子,这类子被称为确定子(Stable Discs)。每一步走子都选择使得棋盘上己方的确定子最多的一部。 - c) 位置优先策略。
考虑到角点的重要性,把棋盘上的每一个子都赋予一个优先级,每一步从可走子里选择优先级最高的一个子。 - d) 机动性策略(mobility)。
黑白棋每一步的可走子都是有限的,机动性策略是指走子使得对手的可走子较少,从而逼迫对手不得不走出差的一步(bad move),使得自己占据先机。 - e) 消失策略(evaporation, less is more)。
在棋盘比试的前期,己方的子越少往往意味着局势更优。因此在前期可采用使己方的子更少的走子。 - f) 奇偶策略(parity)。
走子尽量走在一行或一列有奇数个空子的位置。 以上只列举了一些常见的黑白棋策略或原则,事实上还有很多更为复杂的策略,此处不进行列举。
蒙特卡洛搜索树
基本概念
蒙特卡洛树搜索(简称 MCTS)是 Rémi Coulom 在 2006 年在它的围棋人机对战引擎 「Crazy Stone」中首次发明并使用的的 ,并且取得了很好的效果。
我们先讲讲它用的原始 MCTS 算法(ALphago 有部分改进)
蒙特卡洛树搜索,首先它肯定是棵搜索树
我们回想一下我们下棋时的思维——并没有在脑海里面把所有可能列出来,而是根据「棋感」在脑海里大致筛选出了几种「最可能」的走法,然后再想走了这几种走法之后对手「最可能」的走法,然后再想自己接下来「最可能」的走法。这其实就是 MCTS 算法的设计思路。
上面这段很重要,可以反复品味
它经历下面 3 个过程(重复千千万万次)
- 选择(Selection):
从根结点R开始,选择连续的子结点向下至叶子结点L。下面的结点有更多选择子结点的方法,使游戏树向最优点扩展移动,这是蒙特卡洛树搜索的本质。 - 扩展 (expansion):
除非任意一方的输赢导致游戏结束,否则L会创建一个或多个子结点或从结点C中选择。 - 模拟/仿真(Simluation):在结点C中进行随机布局。
- 回溯/反向传播(Backpropagation):使用布局结果更新从C到R的路径上的结点信息。
模拟(Simluation)
我们不按顺序,先讲模拟,模拟借鉴了我们上面说的蒙特卡洛方法,快速走子,只走一盘,分出个胜负。
我们每个节点(每个节点代表每个不同的局面)都有两个值,代表这个节点以及它的子节点模拟的次数和赢的次数,比如模拟了 10 次,赢了 4 盘,记为 4/10。
我们再看多一次这幅图,如图,每个节点都会标上这两个值。
选择(Selection)
我们将节点分成三类:
未访问:还没有评估过当前局面
未完全展开:被评估过至少一次,但是子节点(下一步的局面)没有被全部访问过,可以进一步扩展
完全展开:子节点被全部访问过
我们找到目前认为「最有可能会走到的」一个未被评估的局面(双方都很聪明的情况下),并且选择它。
什么节点最有可能走到呢?最直观的想法是直接看节点的胜率(赢的次数/访问次数),哪个节点最大选择哪个,但是这样是不行的!因为如果一开始在某个节点进行模拟的时候,尽管这个节点不怎么好,但是一开始随机走子的时候赢了一盘,就会一直走这个节点了。
因此人们造了一个函数
Q(v) 是该节点赢的次数,N(v) 是该节点模拟的次数,C 是一个常数。
因此我们每次选择的过程如下——从根节点出发,遵循最大最小原则,每次选择己方 UCT 值最优的一个节点,向下搜索,直到找到一个「未完全展开的节点」,根据我们上面的定义,未完全展开的节点一定有未访问的子节点,随便选一个进行扩展。
这个公式虽然我们造不出来,但是我们可以观赏它的巧妙之处,首先加号的前面部分就是我们刚刚说的胜率,然后加号的后面部分函数长这样:
随着访问次数的增加,加号后面的值越来越小,因此我们的选择会更加倾向于选择那些还没怎么被统计过的节点,避免了我们刚刚说的蒙特卡洛树搜索会碰到的陷阱——一开始走了歪路。
扩展(expansion)
将刚刚选择的节点加上一个统计信息为「0/0」的节点,然后进入下一步模拟(Simluation)
回溯(Backpropagation)
Backpropagation 很多资料翻译成反向传播,不过我觉得其实极其类似于递归里的回溯,就是从子节点开始,沿着刚刚向下的路径往回走,沿途更新各个父节点的统计信息。
再放一次这个图,可以观察一下在模拟过后,新的 0/0 节点,比如这里模拟输了,变成了 0/1,然后它的到根节点上的节点的统计信息的访问次数全部加 1,赢的次数不变。
算法什么时候可以终止
取决于你什么时候想让他停止,比如说你可以设定一个时间,比如五秒后停止计算。
一般来说最佳走法就是具有最高访问次数的节点,这点可能稍微有点反直觉。这样评估的原因是因为蒙特卡洛树搜索算法的核心就是,越优秀的节点,越有可能走,反过来就是,走得越多的节点,越优秀。
蒙特卡洛黑白棋
经过策略的实践发现,单一的贪心策略和消失策略效果并不理想,确定子策略的算法比较复杂,程序效率较低,而机动性策略难以用程序体现,奇偶策略的效果也不佳,而位置优先策略在UCT算法中表现优异。 程序中采用两种优先级策略。
第一种优先级如下图:
这个优先级表的首要策略是占据角点,所有优先级的设置都为此服务。
第二种优先级如下图:
这个优先级表由Roxanne提出,结合了多种策略,同时也结合了Mobility的特性,因为中间子的优先级较高,会提高自己的Mobility而限制对手的可走步数。
Roxanne 策略 详见 《Analysis of Monte Carlo Techniques in Othello》
提出者:Canosa, R. Roxanne canosa homepage. https://www.cs.rit.edu/~rlc/
代码:
# -*- coding = utf-8 -*-
# @Time : 2021/4/1 20:06
# @Author : Leokadia
# @File : Chess.py
# @Software : PyCharm
'''
a) 选择(Selection):从根结点R开始,选择连续的子结点向下至叶子结点L。
下面的结点有更多选择子结点的方法,使游戏树向最优点扩展移动,这是蒙特卡洛树搜索的本质。
b) 扩展(Expansion):除非任意一方的输赢导致游戏结束,否则L会创建一个或多个子结点或从结点C中选择。
c) 仿真(Simulation):在结点C中进行随机布局。
d) 反向传播(Backup):使用布局结果更新从C到R的路径上的结点信息。
'''
from func_timeout import func_timeout, FunctionTimedOut
import datetime # datetime模块用于操作时间
import random # random模块用于生成随机数
from math import log, sqrt # math数学计算库导入对数函数log,开方函数sqrt
from time import time # 时间模块用于时间的设置
#
from copy import deepcopy # 从copy模块导入深度拷贝方法
# 棋盘类
class ReversiBoard(object):
def __init__(self):
"""
初始化棋盘,棋盘大小为8*8,黑棋用 X 表示,白棋用 O 表示,未落子时用 . 表示
"""
self.board_init()
def board_init(self):
"""
重置棋盘
"""
self.empty = '.' # 未落子状态
self._board = [[self.empty for _ in range(8)] for _ in range(8)] # 规格:8*8,构造8*8个.
self._board[3][4], self._board[4][3] = 'X', 'X' # 黑棋棋子初始状态
self._board[3][3], self._board[4][4] = 'O', 'O' # 白棋棋子初始状态
# 棋局开始时黑棋位于E4和D5,白棋位于D4和E5
def display(self, step_time=None, total_time=None):
"""
打印棋盘
:param step_time: 每一步的耗时, 比如:{"X":1,"O":0},默认值是None
:param total_time: 总耗时, 比如:{"X":1,"O":0},默认值是None
:return:
"""
board = self._board
print(' ', ' '.join(list('ABCDEFGH'))) # 打印列名,开头空一格
for i in range(8): # 打印行名和棋盘
# print(board)
print(str(i + 1), ' '.join(board[i]))
''' Display time. '''
if (not step_time) or (not total_time): # 如果step_time=None, total_time=None
# 棋盘初始化时展示的时间
step_time = {"X": 0, "O": 0}
total_time = {"X": 0, "O": 0}
print("统计棋局: 棋子总数 / 每一步耗时 / 总时间 ")
print("黑 棋: " + str(self.count('X')) + ' / ' + str(step_time['X']) + ' / ' + str(total_time['X']))
print("白 棋: " + str(self.count('O')) + ' / ' + str(step_time['O']) + ' / ' + str(total_time['O']) + '\n')
else:
# 比赛时展示时间
print("统计棋局: 棋子总数 / 每一步耗时 / 总时间 ")
print("黑 棋: " + str(self.count('X')) + ' / ' + str(step_time['X']) + ' / ' + str(total_time['X']))
print("白 棋: " + str(self.count('O')) + ' / ' + str(step_time['O']) + ' / ' + str(total_time['O']) + '\n')
def count(self, color):
"""
统计 color 一方棋子的数量。(O:白棋, X:黑棋, .:未落子状态)
:param color: [O,X,.] 表示棋盘上不同的棋子
:return: 返回 color 棋子在棋盘上的总数
"""
count = 0
for y in range(8):
for x in range(8):
if self._board[x][y] == color:
count += 1
return count
def get_winner(self):
"""
判断黑棋和白旗的输赢,通过棋子的个数进行判断
:return: 0-黑棋赢,1-白旗赢,2-表示平局,黑棋个数和白旗个数相等
"""
# 定义黑白棋子初始的个数
black_count, white_count = 0, 0
for i in range(8):
for j in range(8):
# 统计黑棋棋子的个数
if self._board[i][j] == 'X':
black_count += 1
# 统计白旗棋子的个数
if self._board[i][j] == 'O':
white_count += 1
if black_count > white_count:
# 黑棋胜
return 0, black_count - white_count
elif black_count < white_count:
# 白棋胜
return 1, white_count - black_count
elif black_count == white_count:
# 表示平局,黑棋个数和白旗个数相等
return 2, 0
def _move(self, action, color):
"""
落子并获取反转棋子的坐标
:param action: 落子的坐标 可以是 D3 也可以是(2,3)
:param color: [O,X,.] 表示棋盘上不同的棋子
:return: 返回反转棋子的坐标列表,落子失败则返回False
"""
# 判断action 是不是字符串,如果是则转化为数字坐标
if isinstance(action, str):
action = self.board_num(action)
fliped = self._can_fliped(action, color)
if fliped:
# 有就反转对方棋子坐标
for flip in fliped:
x, y = self.board_num(flip)
self._board[x][y] = color # 将待翻转列表全部变成自己的color
# 落子坐标
x, y = action # 落子坐标改为我要下的地方坐标
# 更改棋盘上 action 坐标处的状态,修改之后该位置属于 color[X,O,.]等三状态
self._board[x][y] = color
return fliped
else:
# 没有反转子则落子失败
return False
# 从子节点开始,沿着刚刚向下的路径往回走
# 沿途更新各个父节点的统计信息。
def backpropagation(self, action, flipped_pos, color):
"""
回溯
:param action: 落子点的坐标
:param flipped_pos: 反转棋子坐标列表
:param color: 棋子的属性,[X,0,.]三种情况
:return:
"""
# 判断action 是不是字符串,如果是则转化为数字坐标
if isinstance(action, str):
action = self.board_num(action)
# 还原棋盘
self._board[action[0]][action[1]] = self.empty
# 如果 color == 'X',则 op_color = 'O';否则 op_color = 'X'
op_color = "O" if color == "X" else "X"
for p in flipped_pos:
# 判断action 是不是字符串,如果是则转化为数字坐标
if isinstance(p, str):
p = self.board_num(p)
self._board[p[0]][p[1]] = op_color
def is_on_board(self, x, y):
"""
判断坐标是否出界
:param x: row 行坐标
:param y: col 列坐标
:return: True or False
"""
return x >= 0 and x <= 7 and y >= 0 and y <= 7
def _can_fliped(self, action, color):
"""
检测落子是否合法,如果不合法,返回 False,否则返回反转子的坐标列表
:param action: 下子位置
:param color: [X,0,.] 棋子状态
:return: False or 反转对方棋子的坐标列表
"""
# 判断action 是不是字符串,如果是则转化为数字坐标
if isinstance(action, str):
action = self.board_num(action)
xstart, ystart = action
# 如果该位置已经有棋子或者出界,返回 False
if not self.is_on_board(xstart, ystart) or self._board[xstart][ystart] != self.empty:
return False
# 临时将color放到指定位置
self._board[xstart][ystart] = color
# 棋手
op_color = "O" if color == "X" else "X"
# 要被翻转的棋子
flipped_pos = [] # 数字坐标
flipped_pos_board = [] # 棋盘坐标
for xdirection, ydirection in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]]: #八个方向依次遍历
x, y = xstart, ystart
x += xdirection
y += ydirection
# 如果(x,y)在棋盘上,而且为对方棋子,则在这个方向上继续前进,否则循环下一个角度。
if self.is_on_board(x, y) and self._board[x][y] == op_color:
x += xdirection
y += ydirection
# 进一步判断点(x,y)是否在棋盘上,如果不在棋盘上,继续循环下一个角度,如果在棋盘上,则进行while循环。
if not self.is_on_board(x, y):
continue
# 一直走到出界或不是对方棋子的位置
while self._board[x][y] == op_color:
# 如果一直是对方的棋子,则点(x,y)一直循环,直至点(x,y)出界或者不是对方的棋子。
x += xdirection
y += ydirection
# 点(x,y)出界了和不是对方棋子
if not self.is_on_board(x, y):
break
# 出界了,则没有棋子要翻转OXXXXX
if not self.is_on_board(x, y):
continue
# 是自己的棋子OXXXXXXO
if self._board[x][y] == color:
while True:
x -= xdirection
y -= ydirection
# 回到了起点则结束
if x == xstart and y == ystart:
break
# 需要翻转的棋子
flipped_pos.append([x, y])
# 将前面临时放上的棋子去掉,即还原棋盘(可以下的棋子已经在要翻转的列表中了)
self._board[xstart][ystart] = self.empty # restore the empty space
# 没有要被翻转的棋子,则走法非法。返回 False
if len(flipped_pos) == 0:
return False
for fp in flipped_pos:
flipped_pos_board.append(self.num_board(fp))
# 走法正常,返回翻转棋子的棋盘坐标
return flipped_pos_board
def get_legal_actions(self, color): #我的字的颜色
"""
按照黑白棋的规则获取棋子的合法走法
:param color: 不同颜色的棋子,X-黑棋,O-白棋
:return: 生成合法的落子坐标,用list()方法可以获取所有的合法坐标
"""
# 表示棋盘坐标点的8个不同方向坐标,比如方向坐标[0][1]则表示坐标点的正上方。
direction = [(-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1)]
op_color = "O" if color == "X" else "X"
# 统计 op_color 一方邻近的未落子状态的位置
op_color_near_points = []
board = self._board
for i in range(8):
# i 是行数,从0开始,j是列数,也是从0开始
for j in range(8):
# 判断棋盘[i][j]位子棋子的属性,如果是op_color,则继续进行下一步操作,
# 否则继续循环获取下一个坐标棋子的属性
if board[i][j] == op_color:
# dx,dy 分别表示[i][j]坐标在行、列方向上的步长,direction 表示方向坐标
for dx, dy in direction:
x, y = i + dx, j + dy
# 表示x、y坐标值在合理范围,棋盘坐标点board[x][y]为未落子状态,
# 而且(x,y)不在op_color_near_points 中,统计对方未落子状态位置的列表才可以添加该坐标点
if 0 <= x <= 7 and 0 <= y <= 7 and board[x][y] == self.empty and (
x, y) not in op_color_near_points:
op_color_near_points.append((x, y))
l = [0, 1, 2, 3, 4, 5, 6, 7]
for p in op_color_near_points:
# 判断落位是否合法,合法则进行下一步
if self._can_fliped(p, color):
# 判断p是不是数字坐标,如果是则返回棋盘坐标
# p = self.board_num(p)
if p[0] in l and p[1] in l: #判断p是不是数字坐标,如果是则返回棋盘坐标
p = self.num_board(p)
yield p
def board_num(self, action):
"""
棋盘坐标转化为数字坐标
:param action:棋盘坐标,比如A1
:return:数字坐标,比如 A1 --->(0,0)
"""
row, col = str(action[1]).upper(), str(action[0]).upper()
if row in '12345678' and col in 'ABCDEFGH':
# 坐标正确
x, y = '12345678'.index(row), 'ABCDEFGH'.index(col) # 转化为对应的索引
return x, y
def num_board(self, action):
"""
数字坐标转化为棋盘坐标
:param action:数字坐标 ,比如(0,0)
:return:棋盘坐标,比如 (0,0)---> A1
"""
row, col = action
l = [0, 1, 2, 3, 4, 5, 6, 7]
if col in l and row in l:
return chr(ord('A') + col) + str(row + 1) # ord()函数:返回字符对应的十进制整数;chr():返回当前整数对应的 ASCII 字符
class Game(object):
def __init__(self, black_player, white_player):
self.game_init()
def game_init(self):
self.board = ReversiBoard() # 棋盘
self.current_player = None
self.black_player = black_player # 黑棋一方
self.white_player = white_player # 白棋一方
self.black_player.color = "X"
self.white_player.color = "O"
def switch_player(self, black_player, white_player):
"""
游戏过程中切换玩家
:param black_player: 黑棋
:param white_player: 白棋
:return: 当前玩家
"""
# 如果当前玩家是 None 或者 白棋一方 white_player,则返回 黑棋一方 black_player;
if self.current_player is None:
return black_player
else:
# 如果当前玩家是黑棋一方 black_player 则返回 白棋一方 white_player
if self.current_player == self.black_player:
return white_player
else:
return black_player
def print_winner(self, winner):
"""
打印赢家
:param winner: [0,1,2] 分别代表黑棋获胜、白棋获胜、平局3种可能。
:return:
"""
print(['黑棋获胜!', '白棋获胜!', '平局'][winner])
def force_loss(self, is_timeout=False, is_board=False, is_legal=False):
"""
落子3个不合符规则和超时则结束游戏,修改棋盘也是输
:param is_timeout: 时间是否超时,默认不超时
:param is_board: 是否修改棋盘
:param is_legal: 落子是否合法
:return: 赢家(0,1),棋子差 0
"""
if self.current_player == self.black_player:
win_color = '白棋 - O'
loss_color = '黑棋 - X'
winner = 1
else:
win_color = '黑棋 - X'
loss_color = '白棋 - O'
winner = 0
if is_timeout:
print('\n{} 思考超过 60s, {} 胜'.format(loss_color, win_color))
if is_legal:
print('\n{} 落子 3 次不符合规则,故 {} 胜'.format(loss_color, win_color))
if is_board:
print('\n{} 擅自改动棋盘判输,故 {} 胜'.format(loss_color, win_color))
diff = 0
return winner, diff
def run(self):
"""
运行游戏
:return:
"""
# 定义统计双方下棋时间
total_time = {"X": 0, "O": 0}
# 定义双方每一步下棋时间
step_time = {"X": 0, "O": 0}
# 初始化胜负结果和棋子差
winner = None
diff = -1
# 游戏开始
print('\n=====开始游戏!=====\n')
# 棋盘初始化
self.board.display(step_time, total_time)
while True:
# 切换当前玩家,如果当前玩家是 None 或者白棋 white_player,则返回黑棋 black_player;
# 否则返回 white_player。
self.current_player = self.switch_player(self.black_player, self.white_player)
start_time = datetime.datetime.now()
# 当前玩家对棋盘进行思考后,得到落子位置
# 判断当前下棋方
color = "X" if self.current_player == self.black_player else "O"
# 获取当前下棋方合法落子位置
legal_actions = list(self.board.get_legal_actions(color))
# print("%s合法落子坐标列表:"%color,legal_actions)
if len(legal_actions) == 0:
# 判断游戏是否结束
if self.game_over():
# 游戏结束,双方都没有合法位置
winner, diff = self.board.get_winner() # 得到赢家 0,1,2
break
else:
# 另一方有合法位置,切换下棋方
continue
board = deepcopy(self.board._board)
# legal_actions 不等于 0 则表示当前下棋方有合法落子位置
try:
for i in range(0, 3):
# 获取落子位置
action = func_timeout(60, self.current_player.get_move,
kwargs={'board': self.board})
# 如果 action 是 Q 则说明人类想结束比赛
if action == "Q":
# 说明人类想结束游戏,即根据棋子个数定输赢。
break
if action not in legal_actions:
# 判断当前下棋方落子是否符合合法落子,如果不合法,则需要对方重新输入
print("你落子不符合规则,请重新落子!")
continue
else:
# 落子合法则直接 break
break
else:
# 落子3次不合法,结束游戏!
winner, diff = self.force_loss(is_legal=True)
break
except FunctionTimedOut:
# 落子超时,结束游戏
winner, diff = self.force_loss(is_timeout=True)
break
# 结束时间
end_time = datetime.datetime.now()
if board != self.board._board:
# 修改棋盘,结束游戏!
winner, diff = self.force_loss(is_board=True)
break
if action == "Q":
# 说明人类想结束游戏,即根据棋子个数定输赢。
winner, diff = self.board.get_winner() # 得到赢家 0,1,2
break
if action is None:
continue
else:
# 统计一步所用的时间
es_time = (end_time - start_time).seconds
if es_time > 60:
# 该步超过60秒则结束比赛。
print('\n{} 思考超过 60s'.format(self.current_player))
winner, diff = self.force_loss(is_timeout=True)
break
# 当前玩家颜色,更新棋局
self.board._move(action, color)
# 统计每种棋子下棋所用总时间
if self.current_player == self.black_player:
# 当前选手是黑棋一方
step_time["X"] = es_time
total_time["X"] += es_time
else:
step_time["O"] = es_time
total_time["O"] += es_time
# 显示当前棋盘
self.board.display(step_time, total_time)
# 判断游戏是否结束
if self.game_over():
# 游戏结束
winner, diff = self.board.get_winner() # 得到赢家 0,1,2
break
print('\n=====游戏结束!=====\n')
self.board.display(step_time, total_time)
self.print_winner(winner)
# 返回'black_win','white_win','draw',棋子数差
if winner is not None and diff > -1:
result = {0: 'black_win', 1: 'white_win', 2: 'draw'}[winner]
# return result,diff
def game_over(self):
"""
判断游戏是否结束
:return: True/False 游戏结束/游戏没有结束
"""
# 根据当前棋盘,判断棋局是否终止
# 如果当前选手没有合法下棋的位子,则切换选手;如果另外一个选手也没有合法的下棋位置,则比赛停止。
b_list = list(self.board.get_legal_actions('X'))
w_list = list(self.board.get_legal_actions('O'))
is_over = len(b_list) == 0 and len(w_list) == 0 # 返回值 True/False
return is_over
class HumanPlayer:
"""
人类玩家
"""
def __init__(self, color):
"""
玩家初始化
:param color: 下棋方,'X' - 黑棋,'O' - 白棋
"""
self.color = color
def get_move(self, board):
"""
根据当前棋盘输入人类合法落子位置
:param board: 棋盘
:return: 人类下棋落子位置
"""
# 如果 self.color 是黑棋 "X",则 player 是 "黑棋",否则是 "白棋"
if self.color == "X":
player = "黑棋"
else:
player = "白棋"
# 人类玩家输入落子位置,如果输入 'Q', 则返回 'Q'并结束比赛。
# 如果人类玩家输入棋盘位置,e.g. 'A1',
# 首先判断输入是否正确,然后再判断是否符合黑白棋规则的落子位置
while True:
action = input(
"请'{}-{}'方输入一个合法的坐标(e.g. 'D3',若不想进行,请务必输入'Q'结束游戏。): ".format(player,
self.color))
# 如果人类玩家输入 Q 则表示想结束比赛
if action == "Q" or action == 'q':
return "Q"
else:
row, col = action[1].upper(), action[0].upper()
# 检查人类输入是否正确
if row in '12345678' and col in 'ABCDEFGH':
# 检查人类输入是否为符合规则的可落子位置
if action in board.get_legal_actions(self.color):
return action
else:
print("你的输入不合法,请重新输入!")
def oppo(color):
"""
交换棋手
:return: 切换下一步落子棋手
"""
if color == 'X':
return 'O'
return 'X'
class TreeNode():
"""
蒙特卡洛树节点
"""
def __init__(self, parent, color):
self.parent = parent
self.w = 0
self.n = 0
self.color = color
self.child = dict()
class SilentGame(object):
''' 重构游戏类,模拟下棋过程中,不实时打印棋盘 '''
def __init__(self, black_player, white_player, board=ReversiBoard(), current_player=None):
self.board = deepcopy(board) # 棋盘
# 定义棋盘上当前下棋棋手,先默认是 None
self.current_player = current_player
self.black_player = black_player # 黑棋一方
self.white_player = white_player # 白棋一方
self.black_player.color = "X"
self.white_player.color = "O"
def switch_player(self, black_player, white_player):
"""
游戏过程中切换玩家
:param black_player: 黑棋
:param white_player: 白棋
:return: 当前玩家
"""
# 如果当前玩家是 None 或者 白棋一方 white_player,则返回 黑棋一方 black_player;
if self.current_player is None:
return black_player
else:
# 如果当前玩家是黑棋一方 black_player 则返回 白棋一方 white_player
if self.current_player == self.black_player:
return white_player
else:
return black_player
def print_winner(self, winner):
"""
打印赢家
:param winner: [0,1,2] 分别代表黑棋获胜、白棋获胜、平局3种可能。
:return:
"""
print(['黑棋获胜!', '白棋获胜!', '平局'][winner])
def force_loss(self, is_timeout=False, is_board=False, is_legal=False):
"""
落子3个不合符规则和超时则结束游戏,修改棋盘也是输
:param is_timeout: 时间是否超时,默认不超时
:param is_board: 是否修改棋盘
:param is_legal: 落子是否合法
:return: 赢家(0,1),棋子差 0
"""
if self.current_player == self.black_player:
win_color = '白棋 - O'
loss_color = '黑棋 - X'
winner = 1
else:
win_color = '黑棋 - X'
loss_color = '白棋 - O'
winner = 0
if is_timeout:
print('\n{} 思考超过 60s, {} 胜'.format(loss_color, win_color))
if is_legal:
print('\n{} 落子 3 次不符合规则,故 {} 胜'.format(loss_color, win_color))
if is_board:
print('\n{} 擅自改动棋盘判输,故 {} 胜'.format(loss_color, win_color))
diff = 0
return winner, diff
def run(self):
"""
运行游戏
:return:
"""
# 定义统计双方下棋时间
total_time = {"X": 0, "O": 0}
# 定义双方每一步下棋时间
step_time = {"X": 0, "O": 0}
# 初始化胜负结果和棋子差
winner = None
diff = -1
# 游戏开始
while True:
# 切换当前玩家,如果当前玩家是 None 或者白棋 white_player,则返回黑棋 black_player;
# 否则返回 white_player。
self.current_player = self.switch_player(self.black_player, self.white_player)
start_time = datetime.datetime.now()
# 当前玩家对棋盘进行思考后,得到落子位置
# 判断当前下棋方
color = "X" if self.current_player == self.black_player else "O"
# 获取当前下棋方合法落子位置
legal_actions = list(self.board.get_legal_actions(color))
# print("%s合法落子坐标列表:"%color,legal_actions)
if len(legal_actions) == 0:
# 判断游戏是否结束
if self.game_over():
# 游戏结束,双方都没有合法位置
winner, diff = self.board.get_winner() # 得到赢家 0,1,2
break
else:
# 另一方有合法位置,切换下棋方
continue
action = self.current_player.get_move(self.board)
if action is None:
continue
else:
self.board._move(action, color)
if self.game_over():
winner, diff = self.board.get_winner() # 得到赢家 0,1,2
break
return winner, diff
def game_over(self):
"""
判断游戏是否结束
:return: True/False 游戏结束/游戏没有结束
"""
# 根据当前棋盘,判断棋局是否终止
# 如果当前选手没有合法下棋的位子,则切换选手;如果另外一个选手也没有合法的下棋位置,则比赛停止。
b_list = list(self.board.get_legal_actions('X'))
w_list = list(self.board.get_legal_actions('O'))
is_over = len(b_list) == 0 and len(w_list) == 0 # 返回值 True/False
return is_over
# 结合了多种策略,同时也结合了Mobility的特性,因为中间子的优先级较高,会提高自己的Mobility而限制对手的可走步数
class RoxannePlayer(object):
''' Roxanne 策略 详见 《Analysis of Monte Carlo Techniques in Othello》 '''
''' 提出者:Canosa, R. Roxanne canosa homepage. https://www.cs.rit.edu/~rlc/ '''
def __init__(self, color):
"""
Roxanne策略初始化
:param roxanne_table: 从上到下依次按落子优先级排序
:param color: 执棋方
"""
self.roxanne_table = [
['A1', 'H1', 'A8', 'H8'],
['C3', 'F3', 'C6', 'F6'],
['C4', 'F4', 'C5', 'F5', 'D3', 'E3', 'D6', 'E6'],
['A3', 'H3', 'A6', 'H6', 'C1', 'F1', 'C8', 'F8'],
['A4', 'H4', 'A5', 'H5', 'D1', 'E1', 'D8', 'E8'],
['B3', 'G3', 'B6', 'G6', 'C2', 'F2', 'C7', 'F7'],
['B4', 'G4', 'B5', 'G5', 'D2', 'E2', 'D7', 'E7'],
['B2', 'G2', 'B7', 'G7'],
['A2', 'H2', 'A7', 'H7', 'B1', 'G1', 'B8', 'G8']
]
self.color = color
def roxanne_select(self, board):
"""
采用Roxanne 策略选择落子策略
:return: 落子策略
"""
action_list = list(board.get_legal_actions(self.color))
if len(action_list) == 0:
return None
else:
for move_list in self.roxanne_table:
random.shuffle(move_list)
for move in move_list:
if move in action_list:
return move
def get_move(self, board):
"""
采用Roxanne 策略进行搜索
:return: 落子
"""
if self.color == 'X':
player_name = '黑棋'
else:
player_name = '白棋'
# print("请等一会,对方 {}-{} 正在思考中...".format(player_name, self.color))
action = self.roxanne_select(board)
return action
class AIPlayer(object):
''' 蒙特卡罗树搜索智能算法 '''
def __init__(self, color, time_limit=2):
"""
蒙特卡洛树搜索策略初始化
:param color: 执棋方
:param time_limit: 蒙特卡洛树搜索每步的搜索时间步长
:param tick:记录开始搜索的时间
:param sim_black, sim_white: 采用Roxanne策略代替随机策略搜索
"""
self.time_limit = time_limit
self.tick = 0
self.sim_black = RoxannePlayer('X')
self.sim_white = RoxannePlayer('O')
self.color = color
def mcts(self, board):
"""
蒙特卡洛树搜索,在时间限制范围内,拓展节点搜索结果
:return: 选择最佳拓展
"""
root = TreeNode(None, self.color)
# 设定一个时间停止计算,限定规模
while time() - self.tick < self.time_limit - 1:
sim_board = deepcopy(board)
choice = self.select(root, sim_board)
self.expand(choice, sim_board)
winner, diff = self.simulate(choice, sim_board)
back_score = [1, 0, 0.5][winner]
if choice.color == 'X':
back_score = 1 - back_score
self.back_prop(choice, back_score)
best_n = -1
best_move = None
for k in root.child.keys():
if root.child[k].n > best_n:
best_n = root.child[k].n
best_move = k
return best_move
def select(self, node, board):
"""
蒙特卡洛树搜索,节点选择
:return: 搜索树向下递归选择子节点
"""
if len(node.child) == 0:
return node
else:
best_score = -1
best_move = None
for k in node.child.keys():
if node.child[k].n == 0:
best_move = k
break
else:
N = node.n
n = node.child[k].n
w = node.child[k].w
# 随着访问次数的增加,加号后面的值越来越小,因此我们的选择会更加倾向于选择那些还没怎么被统计过的节点
# 避免了蒙特卡洛树搜索会碰到的陷阱——一开始走了歪路。
score = w / n + sqrt(2 * log(N) / n)
if score > best_score:
best_score = score
best_move = k
board._move(best_move, node.color)
return self.select(node.child[best_move], board)
def expand(self, node, board):
"""
蒙特卡洛树搜索,节点扩展
"""
for move in board.get_legal_actions(node.color):
node.child[move] = TreeNode(node, oppo(node.color))
def simulate(self, node, board):
"""
蒙特卡洛树搜索,采用Roxanne策略代替随机策略搜索,模拟扩展搜索树
"""
if node.color == 'O':
current_player = self.sim_black
else:
current_player = self.sim_white
sim_game = SilentGame(self.sim_black, self.sim_white, board, current_player)
return sim_game.run()
def back_prop(self, node, score):
"""
蒙特卡洛树搜索,反向传播,回溯更新模拟路径中的节点奖励
"""
node.n += 1
node.w += score
if node.parent is not None:
self.back_prop(node.parent, 1 - score)
def get_move(self, board):
"""
蒙特卡洛树搜索
:return: 采取最佳拓展落子策略
"""
self.tick = time()
if self.color == 'X':
player_name = '黑棋'
else:
player_name = '白棋'
# print("请等一会,对方 {}-{} 正在思考中...".format(player_name, self.color))
action = self.mcts(deepcopy(board))
return action
# 人类玩家黑棋初始化
black_player = HumanPlayer("X")
# AI 玩家 白棋初始化
white_player = AIPlayer("O")
# 游戏初始化,第一个玩家是黑棋,第二个玩家是白棋
game = Game(black_player, white_player)
# 开始下棋
game.run()