我们在上节完成了围棋规则和棋盘状态监测功能,本节我们在基于上节的基础上,设计一个能自己下棋的围棋机器人。首先我们设计一个类叫Agent,它的初始化代码如下:
class Agent:
def __init__(self):
pass
def select_move(self, game_state):
raise NotImplementedError()
代码中的select_move用于机器人选择当前走法。该函数是整个机器人的核心所在,因为所有智能表现都集中在走法的评估和选择上,一开始我们只使用简单规则和推理来设定机器人的落子算法,因此机器人在实现初期会非常弱鸡,后面我们会在该函数中加入相应智能算法,让它变得像AlphGo一样强大。
现在我们实现方法非常简单,就是随便选择一个不违反规则的地方落子,只要机器人能避开棋眼以及防止出现ko情形。因此我们现在对select_move的逻辑是,遍历整个棋盘状态,找到一个不违背给定规则的位置就可以落子:
class RandomBot(Agent):
def select_move(self, game_state):
'''
遍历棋盘,只要看到一个不违反规则的位置就落子
'''
candidates = []
for r in range(1, game_state.board.num_rows + 1):
for c in range(1, game_state.board.cols + 1):
candidate = Point(row = r, col = c)
if game_state.is_valid_move(Move.play(candidate) and not
is_point_an_eye(game_state.board,
candidate,
game_state.next_player)):
candidates.append(candidate)
if not candidates:
return Move.pass_turn()
#在所有可选位置随便选一个
return Move.play(random.choice(candidates))
有了上面代码后,我们可以实现机器人的自我对弈,当然过程颇为简单和无聊,无法是两个机器人随机扫描棋盘,找到一个合理点后落子。接着我们要绘制棋盘,通常情况下,我们应该用贴图方式绘制出游戏那种美轮美奂的棋盘,但为了简便行事,我们使用简单的点和线来构造一个简易棋盘。
棋盘上可落子的空位,我们用’.'来代替,已经被棋子占据的位置,我们用’x’来表示黑子,用一个圆圈’o‘来表示白子。我们看看实现简易棋盘的代码:
#棋盘的列用字母表示
COLS = 'ABCDEFGHJKLMNOPQRST'
STONE_TO_CHAR = {
None: ' . ',
Player.black: 'x',
Player.white: 'o'
}
def print_move(player, move):
if move.is_pass:
move_str = 'passes'
elif move.is_resign:
move_str = 'resign'
else:
move_str = '%s%d' % (COLS[move.point.col - 1], move.point.row)
print('%s %s' % (player, move_str))
def print_board(board):
for row in range(board.num_rows, 0, -1):
bump = ' ' if row <= 9 else ''
line = []
for col in range(1, board.num_cols + 1):
stone = board.get(Point(row = row, col = col))
line.append(STONE_TO_CHAR[stone])
print('%s%d %s' % (bump, row, ''.join(line)))
print(' ' + ' '.join(COLS[:board.num_cols]))
在上面实现中,我们使用字母来表示列,用数字来表示行,这样在描述落子位置时容易定位,例如说白子落子A10等。有了棋盘,我们就可以初始化两个机器人相互对弈:
import time
def main():
#初始化9*9棋盘
board_size = 9
game = GameState.new_game(board_size)
bots = {
Player.black : RandomBot(),
Player.white : RandomBot()
}
while not game.is_over():
time.sleep(0.3)
print(chr(27) + "[2J")
print_board(game.board)
bot_move = bots[game.next_player].select_move(game)
print_move(game.next_player, bot_move)
game = game.apply_move(bot_move)
main()
上面代码运行时,你会看到变化的棋盘在控制台上绘制出来:
当上面代码运行的时候,程序运行速度比较慢,要等一会才能等到其结束。主要原因就在于以前实现的does_move_violate_ko,该函数会一直追溯回过去所有棋盘状况去比较,随着落子次数越多,过去的棋盘状况数量就越多,因此该函数的执行非常耗时,要想加快速度就必须改进该函数的比对算法。
一种常用算法是对每一步落子进行编码,然后把落子编码与棋盘编码做异或运算,具体过程如下,首先我们面对一个空白棋盘,给它的编码为0:
接着有棋子落在C3时,我们给它一个二进制编码0x1001001:
于是我们做一次异或运算 0x00 XOR 0x1001001 = 0x1001001,接着白棋落子在D3,我们对此进行的编码为0x101000:
于是我们再次与前面数值做异或运算: 0x1001001 XOR 0x101000 = 0x100001,如果此时我们把白子拿走,于是棋盘返回到上一个状态,由于位置D3上的变化被我们编码为0x101000,那么我们再次用该值与先前值做异或运算:0x100001 XOR 0x101000 = 0x1001001:
由此我们比对数值就可以发现棋盘状态是否产生了回滚,不需要像原来那样进行一次二维数组的扫描比对,如果棋盘对应的二维数组维度为n,一次扫描比对需要的时间是O(n^2),但是一次数值比对的时间是O(1),由此在效率上能够提高两个数量级!!
上面描述的编码其实很简单,对于一个19*19的棋盘而言,我们给每一个位置一个整数值,因此总共对应3*19*19个整数,其中3对应3种状态,也就是位置空着,位置落黑棋,位置落白棋,我们把对应整数转换为二进制数进行运算即可,实现代码如下:
def to_python(player_state):
if player_state is None:
return 'None'
if player_state == Player.black:
return Player.black
return Player.white
#用一个64位整形对应每个棋盘
MAX63 = 0x7fffffffffffffff
#发明这种编码算法的人叫zobrist
zobrist_HASH_CODE = {}
zobrist_EMPTY_BOARD = 0
for row in range(1, 20):
for col in range(1, 20):
for state in (None, Player.black, Player.white):
#随机选取一个整数对应当前位置,这里默认当前取随机值时不会与前面取值发生碰撞
code = random.randint(0, MAX63)
zobrist.HASH_CODE[Point(row, col), state] = code
print('HASH_CODE = {')
for (pt, state), hash_code in table.items():
print(' (%r, %s): %r,' % (pt, to_python(state), hash_code))
print('}')
print(' ')
print('EMPTY_BOARD = %d' % (empty_board,))
上面代码运行后,会把棋盘每个位置上的不同状态打印出来。接下来我们对原有代码做相应修改:
class GoString():
def __init__(self, color, stones, liberties):
self.color = color
#将两个集合修改为immutable类型
self.stones = frozenset(stones)
self.liberties = frozenset(liberties)
#替换掉原来的remove_liberty 和 add_liberty
def without_liberty(self, point):
new_liberties = self.liberties - set([point])
return GoString(self.color, self.stones, new_liberties)
......
class Board():
def __init__(self, num_rows, num_cols):
self.num_rows = num_rows
self.num_cols = num_cols
self._grid = {}
#添加hash
self._hash = zobrist_EMPTY_BOARD
def zobrist_hash(self):
return self._hash
def place_stone(self, player, point):
....
#从下面开始新的修改
for same_color_string in adjacent_same_color:
new_string = new_string.merged_with(same_color_string)
for new_string_point in new_string.stones:
#访问棋盘某个点时返回与该点棋子相邻的所有棋子集合
self._grid[new_string_point] = new_string
#增加落子的hash值记录
self._hash ^= zobrist_HASH_CODE[point, None]
self._hash ^= zobrist_HASH_CODE[point, player]
for other_color_string in adjacent_opposite_color:
#当该点被占据前,它属于反色棋子的自由点,占据后就不再属于反色棋子自由点
#修改成without_liberty
replacement = other_color_string.without_liberty(point)
if replacement.num_liberties:
self._replace_string(other_color_string.without_liberty(point))
else:
#如果落子后,相邻反色棋子的所有自由点都被堵住,对方棋子被吃掉
self._remove_string(other_color_string)
#增加一个新函数
def _replace_string(self, new_string):
for point in new_string.stones:
self._grid[point] = new_string
....
def _remove_string(self, string):
#从棋盘上删除一整片连接棋子
for point in string.stones:
for neighbor in point.neighbors():
neighbor_string = self._grid.get(neighbor)
if neighbor_string is None:
continue
if neighbor_string is not string:
#修改
self._replace_string(neighbor_string.with_liberty(point))
self._grid[point] = None
#由于棋子被拿掉后,对应位置状态发生变化,因此修改编码
self._hash ^= zobrist_HASH_CODE[point, string.color]
self._hash ^= zobrist_HASH_CODE[point, None]
class GameState():
def __init__(self, board, next_player, previous, move):
self.board = board
self.next_player = next_player
self.previous_state = previous
self.last_move = move
#添加新修改
if previous is None:
self.previous_states = frozenset()
else:
self.previous_states = frozenset(previous.previous_states | {(previous.next_player,
previous.board.zobrist_hash())})
....
def does_move_violate_ko(self, player, move):
if not move.is_play:
return False
next_board = copy.deepcopy(self.board)
next_board.place_stone(player, move.point)
next_situation = (player.other, next_board)
#判断Ko不仅仅看是否返回上一步的棋盘而是检测是否返回以前有过的棋盘状态
#修改,我们不用在循环检测,只要看当前数值与前面数值是否匹配即可
return next_situation in self.previous_states
修改完上面代码后,我们再次运行main函数,你会发现它的执行比原来快了很多。最后我们再添加人与机器人对弈的功能,要实现人机对弈,我们必须把人的落子位置告知程序,这一点不难,只要我们输入类似A3,D4这样的信息即可,由此我们增加一个辅助函数用于输入人类棋手的落子位置:
#该函数把A3,D3这样的输入转换成具体坐标
def point_from_coords(coords):
#获取表示列的字母
col = COLS.index(coords[0]) + 1
#获取表示行的数字
row = int(coords[1:])
return Point(row = row, col = col)
然后我们调用上面程序,启动人机对弈流程:
from six.moves import input
def main():
#构造一个9*9棋盘
board_size = 9
game = GameState.new_game(board_size)
bot = RandomBot()
while not game.is_over():
print(chr(27) + "[2J")
print_board(game.board)
#人类用黑棋
if game.next_player == Player.black:
human_move = input('--')
point = point_from_coords(human_move.strip())
move = Move.play(point)
else:
move = bot.select_move(game)
print_move(game.next_player, move)
game = game.apply_move(move)
main()
上面代码运行后情形如下:
它会显示出棋盘,然后底下有输入框,我们分别输入列对应的字符以及行号,那么程序就能在棋盘上显示对应的落子,在程序设定中,人类始终使用黑棋,因此上面输入完毕回车后,在给定的位置会显示出一个’x’。
至此,我们就有了棋盘,有了落子功能,有了棋盘状态检测,同时还有了机器人自动对弈,以及人机对弈功能,由此我们完成了实现AlphaGo所需要的基础设施。
更详细的讲解和代码调试演示过程,请点击链接
更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号: