0


Python实现蒙特卡洛树黑白棋完整代码

Python实现的基于蒙特卡洛树搜索的完整代码

最终效果:在控制台输入输出,实现3种玩家(AI或者人类或者随机)的对弈

前言:

关于代码:黑白棋部分直接来源为浙江大学Mo平台,仅AI模块为原创

由于水平所限,可能会出现一些错误,还请大佬们指正

本文仅做简要的介绍和实现,不涉及数学原理(因为我也不会QAQ)


一、黑白棋简介

黑白棋 (Reversi),也叫苹果棋,翻转棋,是一个经典的策略性游戏

游戏规则

棋局开始时黑棋位于 E4 和 D5 ,白棋位于 D4 和 E5,如图所示。

  1. 黑方先行,双方交替下棋。

  2. 一步合法的棋步包括:

(1)在一个空格处落下一个棋子,并且翻转对手一个或多个棋子;

(2)新落下的棋子必须落在可夹住对方棋子的位置上,对方被夹住的所有棋子都要翻转过来,可

以是横着夹,竖着夹,或是斜着夹。夹住的位置上必须全部是对手的棋子,不能有空格;

(3)一步棋可以在数个(横向,纵向,对角线)方向上翻棋,任何被夹住的棋子都必须被翻转过来,棋手无权选择不去翻某个棋子。

  1. 如果一方没有合法棋步,也就是说不管他下到哪里,都不能至少翻转对手的一个棋子,那他这

轮只能弃权,而由他的对手继续落子直到他有合法棋步可下。

  1. 如果一方至少有一步合法棋步可下,他就必须落子,不得弃权。

  2. 棋局持续下去,直到棋盘填满或者双方都无合法棋步可下。

  3. 如果某一方落子时间超过 1 分钟 或者 连续落子 3 次不合法,则判该方失败。


二、蒙特卡洛树搜索简介

1.蒙特卡洛树搜索Monte Carlo Tree Search, MCTS

简介:

蒙特卡洛树搜索大概可以被分成四步。选择,扩展,模拟,反向传播。

在开始阶段,搜索树只有一个节点,也就是当前需要做出选择的局面。

搜索树中的每一个节点至少包含:当前局面,访问次数,累计奖励。

1、选择(selection)∶指算法从搜索树的根结点开始,向下递归选择子结点,直至到达叶子结点或者到达具有还未被扩展过的子结点。这个向下递归选择过程可由UCB1算法(公式在后面)来实现。

2、扩展(expansion)∶如果结点L不是一个终止结点(或对抗搜索的终局结点),则随机扩展它的一个未被扩展过的后继边缘结点M。

3、模拟(simulation)∶从结点M出发,模拟扩展搜索树,直到找到一个终止结点。模拟过程使用的策略和采用UCB1 算法实现的选择过程并不相同,前者通常会使用比较简单的策略,例如使用随机策略。

4、反向传播(Back Propagation)∶用模拟所得结果(终止结点的代价或游戏终局分数)回溯更新模拟路径中结点的奖励均值和被访问次数。

2.上限置信区间UCB1算法

具体原理可以查阅其他文章,此处不做赘述。直接给出公式和含义

其中argmax x表示这个节点的平均奖励值,也就是奖励总和(reward)除以访问次数(visits)

C为UCB1的超参数,自定义

t为该节点的父节点的访问次数

T为该节点的访问次数

3.通俗算法思路

(仅限本文黑白棋例子)

选择:从根节点开始,递归选择UCB值最大的一个节点(我们认为没有被扩展的节点UCB值无限大)

扩展:(1)如果当目前节点下的所有节点都已经被访问过了,并且这些节点都不是终止节点,则需要选择一个UCB值最大的节点进行扩展(添加它的子节点并初始化),返回扩展的节点。(2)如果还有节点没有被访问过,就不进行扩展,返回这个没被访问的节点

模拟:从上面已经选择的节点开始,进行一次对局模拟,直到分出胜负或者达到步数限制,返回所得的分值

反向传播:由模拟得到的奖励值,进行由叶节点到根节点的反向路径上的传播,依次更新节点值:原来的节点值加上或者减去新的奖励值(取决于所选的颜色),并且路径上所有节点访问次数+1

最后到达步数上限后,选择搜索树的第一级子节点(根节点的孩子)中UCB值最大的节点,作为下一步行棋

4.图示

假设我们为黑方,图中数值:分值/访问次数

选择:假设初始如下,经过UCB值计算,最终选择了1/1的叶节点(UCB值最大)

扩展:叶节点都被访问过了,需要扩展新的节点,设为0/0

模拟:在新扩展的节点上模拟一次对局,结果白棋胜,记为0分

反向传播:所有路径上的节点分值+0,访问次数+1

三、代码实现

分为Game类,Board类,和三种Player

Game类

  1. # !/usr/bin/Anaconda3/python
  2. # -*- coding: utf-8 -*-
  3. from func_timeout import func_timeout, FunctionTimedOut
  4. import datetime
  5. from board import Board
  6. from copy import deepcopy
  7. class Game(object):
  8. def __init__(self, black_player, white_player):
  9. self.board = Board() # 棋盘
  10. # 定义棋盘上当前下棋棋手,先默认是 None
  11. self.current_player = None
  12. self.black_player = black_player # 黑棋一方
  13. self.white_player = white_player # 白棋一方
  14. self.black_player.color = "X"
  15. self.white_player.color = "O"
  16. def switch_player(self, black_player, white_player):
  17. """
  18. 游戏过程中切换玩家
  19. :param black_player: 黑棋
  20. :param white_player: 白棋
  21. :return: 当前玩家
  22. """
  23. # 如果当前玩家是 None 或者 白棋一方 white_player,则返回 黑棋一方 black_player;
  24. if self.current_player is None:
  25. return black_player
  26. else:
  27. # 如果当前玩家是黑棋一方 black_player 则返回 白棋一方 white_player
  28. if self.current_player == self.black_player:
  29. return white_player
  30. else:
  31. return black_player
  32. def print_winner(self, winner):
  33. """
  34. 打印赢家
  35. :param winner: [0,1,2] 分别代表黑棋获胜、白棋获胜、平局3种可能。
  36. :return:
  37. """
  38. print(['黑棋获胜!', '白棋获胜!', '平局'][winner])
  39. def force_loss(self, is_timeout=False, is_board=False, is_legal=False):
  40. """
  41. 落子3个不合符规则和超时则结束游戏,修改棋盘也是输
  42. :param is_timeout: 时间是否超时,默认不超时
  43. :param is_board: 是否修改棋盘
  44. :param is_legal: 落子是否合法
  45. :return: 赢家(0,1),棋子差 0
  46. """
  47. if self.current_player == self.black_player:
  48. win_color = '白棋 - O'
  49. loss_color = '黑棋 - X'
  50. winner = 1
  51. else:
  52. win_color = '黑棋 - X'
  53. loss_color = '白棋 - O'
  54. winner = 0
  55. if is_timeout:
  56. print('\n{} 思考超过 60s, {} 胜'.format(loss_color, win_color))
  57. if is_legal:
  58. print('\n{} 落子 3 次不符合规则,故 {} 胜'.format(loss_color, win_color))
  59. if is_board:
  60. print('\n{} 擅自改动棋盘判输,故 {} 胜'.format(loss_color, win_color))
  61. diff = 0
  62. return winner, diff
  63. def run(self):
  64. """
  65. 运行游戏
  66. :return:
  67. """
  68. # 定义统计双方下棋时间
  69. total_time = {"X": 0, "O": 0}
  70. # 定义双方每一步下棋时间
  71. step_time = {"X": 0, "O": 0}
  72. # 初始化胜负结果和棋子差
  73. winner = None
  74. diff = -1
  75. # 游戏开始
  76. print('\n=====开始游戏!=====\n')
  77. # 棋盘初始化
  78. self.board.display(step_time, total_time)
  79. while True:
  80. # 切换当前玩家,如果当前玩家是 None 或者白棋 white_player,则返回黑棋 black_player;
  81. # 否则返回 white_player。
  82. self.current_player = self.switch_player(self.black_player, self.white_player)
  83. start_time = datetime.datetime.now()
  84. # 当前玩家对棋盘进行思考后,得到落子位置
  85. # 判断当前下棋方
  86. color = "X" if self.current_player == self.black_player else "O"
  87. # 获取当前下棋方合法落子位置
  88. legal_actions = list(self.board.get_legal_actions(color))
  89. # print("%s合法落子坐标列表:"%color,legal_actions)
  90. if len(legal_actions) == 0:
  91. # 判断游戏是否结束
  92. if self.game_over():
  93. # 游戏结束,双方都没有合法位置
  94. winner, diff = self.board.get_winner() # 得到赢家 0,1,2
  95. break
  96. else:
  97. # 另一方有合法位置,切换下棋方
  98. continue
  99. board = deepcopy(self.board._board)
  100. # legal_actions 不等于 0 则表示当前下棋方有合法落子位置
  101. try:
  102. for i in range(0, 3):
  103. # 获取落子位置
  104. action = func_timeout(60, self.current_player.get_move,
  105. kwargs={'board': self.board})
  106. # 如果 action 是 Q 则说明人类想结束比赛
  107. if action == "Q":
  108. # 说明人类想结束游戏,即根据棋子个数定输赢。
  109. break
  110. if action not in legal_actions:
  111. # 判断当前下棋方落子是否符合合法落子,如果不合法,则需要对方重新输入
  112. print("你落子不符合规则,请重新落子!")
  113. continue
  114. else:
  115. # 落子合法则直接 break
  116. break
  117. else:
  118. # 落子3次不合法,结束游戏!
  119. winner, diff = self.force_loss(is_legal=True)
  120. break
  121. except FunctionTimedOut:
  122. # 落子超时,结束游戏
  123. winner, diff = self.force_loss(is_timeout=True)
  124. break
  125. # 结束时间
  126. end_time = datetime.datetime.now()
  127. if board != self.board._board:
  128. # 修改棋盘,结束游戏!
  129. winner, diff = self.force_loss(is_board=True)
  130. break
  131. if action == "Q":
  132. # 说明人类想结束游戏,即根据棋子个数定输赢。
  133. winner, diff = self.board.get_winner() # 得到赢家 0,1,2
  134. break
  135. if action is None:
  136. continue
  137. else:
  138. # 统计一步所用的时间
  139. es_time = (end_time - start_time).seconds
  140. if es_time > 60:
  141. # 该步超过60秒则结束比赛。
  142. print('\n{} 思考超过 60s'.format(self.current_player))
  143. winner, diff = self.force_loss(is_timeout=True)
  144. break
  145. # 当前玩家颜色,更新棋局
  146. self.board._move(action, color)
  147. # 统计每种棋子下棋所用总时间
  148. if self.current_player == self.black_player:
  149. # 当前选手是黑棋一方
  150. step_time["X"] = es_time
  151. total_time["X"] += es_time
  152. else:
  153. step_time["O"] = es_time
  154. total_time["O"] += es_time
  155. # 显示当前棋盘
  156. self.board.display(step_time, total_time)
  157. # 判断游戏是否结束
  158. if self.game_over():
  159. # 游戏结束
  160. winner, diff = self.board.get_winner() # 得到赢家 0,1,2
  161. break
  162. print('\n=====游戏结束!=====\n')
  163. self.board.display(step_time, total_time)
  164. self.print_winner(winner)
  165. # 返回'black_win','white_win','draw',棋子数差
  166. if winner is not None and diff > -1:
  167. result = {0: 'black_win', 1: 'white_win', 2: 'draw'}[winner]
  168. return result, diff
  169. def game_over(self):
  170. """
  171. 判断游戏是否结束
  172. :return: True/False 游戏结束/游戏没有结束
  173. """
  174. # 根据当前棋盘,判断棋局是否终止
  175. # 如果当前选手没有合法下棋的位子,则切换选手;如果另外一个选手也没有合法的下棋位置,则比赛停止。
  176. b_list = list(self.board.get_legal_actions('X'))
  177. w_list = list(self.board.get_legal_actions('O'))
  178. is_over = len(b_list) == 0 and len(w_list) == 0 # 返回值 True/False
  179. return is_over

Board类

  1. #!/usr/bin/Anaconda3/python
  2. # -*- coding: utf-8 -*-
  3. class Board(object):
  4. """
  5. Board 黑白棋棋盘,规格是8*8,黑棋用 X 表示,白棋用 O 表示,未落子时用 . 表示。
  6. """
  7. def __init__(self):
  8. """
  9. 初始化棋盘状态
  10. """
  11. self.empty = '.' # 未落子状态
  12. self._board = [[self.empty for _ in range(8)] for _ in range(8)] # 规格:8*8
  13. self._board[3][4] = 'X' # 黑棋棋子
  14. self._board[4][3] = 'X' # 黑棋棋子
  15. self._board[3][3], self._board[4][4] = 'O', 'O' # 白棋棋子
  16. def __getitem__(self, index):
  17. """
  18. 添加Board[][] 索引语法
  19. :param index: 下标索引
  20. :return:
  21. """
  22. return self._board[index]
  23. def display(self, step_time=None, total_time=None):
  24. """
  25. 打印棋盘
  26. :param step_time: 每一步的耗时, 比如:{"X":1,"O":0},默认值是None
  27. :param total_time: 总耗时, 比如:{"X":1,"O":0},默认值是None
  28. :return:
  29. """
  30. board = self._board
  31. # print(step_time,total_time)
  32. # 打印列名
  33. print(' ', ' '.join(list('ABCDEFGH')))
  34. # 打印行名和棋盘
  35. for i in range(8):
  36. # print(board)
  37. print(str(i + 1), ' '.join(board[i]))
  38. if (not step_time) or (not total_time):
  39. # 棋盘初始化时展示的时间
  40. step_time = {"X": 0, "O": 0}
  41. total_time = {"X": 0, "O": 0}
  42. print("统计棋局: 棋子总数 / 每一步耗时 / 总时间 ")
  43. print("黑 棋: " + str(self.count('X')) + ' / ' + str(step_time['X']) + ' / ' + str(
  44. total_time['X']))
  45. print("白 棋: " + str(self.count('O')) + ' / ' + str(step_time['O']) + ' / ' + str(
  46. total_time['O']) + '\n')
  47. else:
  48. # 比赛时展示时间
  49. print("统计棋局: 棋子总数 / 每一步耗时 / 总时间 ")
  50. print("黑 棋: " + str(self.count('X')) + ' / ' + str(step_time['X']) + ' / ' + str(
  51. total_time['X']))
  52. print("白 棋: " + str(self.count('O')) + ' / ' + str(step_time['O']) + ' / ' + str(
  53. total_time['O']) + '\n')
  54. def count(self, color):
  55. """
  56. 统计 color 一方棋子的数量。(O:白棋, X:黑棋, .:未落子状态)
  57. :param color: [O,X,.] 表示棋盘上不同的棋子
  58. :return: 返回 color 棋子在棋盘上的总数
  59. """
  60. count = 0
  61. for y in range(8):
  62. for x in range(8):
  63. if self._board[x][y] == color:
  64. count += 1
  65. return count
  66. def get_winner(self):
  67. """
  68. 判断黑棋和白旗的输赢,通过棋子的个数进行判断
  69. :return: 0-黑棋赢,1-白旗赢,2-表示平局,黑棋个数和白旗个数相等
  70. """
  71. # 定义黑白棋子初始的个数
  72. black_count, white_count = 0, 0
  73. for i in range(8):
  74. for j in range(8):
  75. # 统计黑棋棋子的个数
  76. if self._board[i][j] == 'X':
  77. black_count += 1
  78. # 统计白旗棋子的个数
  79. if self._board[i][j] == 'O':
  80. white_count += 1
  81. if black_count > white_count:
  82. # 黑棋胜
  83. return 0, black_count - white_count
  84. elif black_count < white_count:
  85. # 白棋胜
  86. return 1, white_count - black_count
  87. elif black_count == white_count:
  88. # 表示平局,黑棋个数和白旗个数相等
  89. return 2, 0
  90. def _move(self, action, color):
  91. """
  92. 落子并获取反转棋子的坐标
  93. :param action: 落子的坐标 可以是 D3 也可以是(2,3)
  94. :param color: [O,X,.] 表示棋盘上不同的棋子
  95. :return: 返回反转棋子的坐标列表,落子失败则返回False
  96. """
  97. # 判断action 是不是字符串,如果是则转化为数字坐标
  98. if isinstance(action, str):
  99. action = self.board_num(action)
  100. fliped = self._can_fliped(action, color)
  101. if fliped:
  102. # 有就反转对方棋子坐标
  103. for flip in fliped:
  104. x, y = self.board_num(flip)
  105. self._board[x][y] = color
  106. # 落子坐标
  107. x, y = action
  108. # 更改棋盘上 action 坐标处的状态,修改之后该位置属于 color[X,O,.]等三状态
  109. self._board[x][y] = color
  110. return fliped
  111. else:
  112. # 没有反转子则落子失败
  113. return False
  114. def backpropagation(self, action, flipped_pos, color):
  115. """
  116. 回溯
  117. :param action: 落子点的坐标
  118. :param flipped_pos: 反转棋子坐标列表
  119. :param color: 棋子的属性,[X,0,.]三种情况
  120. :return:
  121. """
  122. # 判断action 是不是字符串,如果是则转化为数字坐标
  123. if isinstance(action, str):
  124. action = self.board_num(action)
  125. self._board[action[0]][action[1]] = self.empty
  126. # 如果 color == 'X',则 op_color = 'O';否则 op_color = 'X'
  127. op_color = "O" if color == "X" else "X"
  128. for p in flipped_pos:
  129. # 判断action 是不是字符串,如果是则转化为数字坐标
  130. if isinstance(p, str):
  131. p = self.board_num(p)
  132. self._board[p[0]][p[1]] = op_color
  133. def is_on_board(self, x, y):
  134. """
  135. 判断坐标是否出界
  136. :param x: row 行坐标
  137. :param y: col 列坐标
  138. :return: True or False
  139. """
  140. return x >= 0 and x <= 7 and y >= 0 and y <= 7
  141. def _can_fliped(self, action, color):
  142. """
  143. 检测落子是否合法,如果不合法,返回 False,否则返回反转子的坐标列表
  144. :param action: 下子位置
  145. :param color: [X,0,.] 棋子状态
  146. :return: False or 反转对方棋子的坐标列表
  147. """
  148. # 判断action 是不是字符串,如果是则转化为数字坐标
  149. if isinstance(action, str):
  150. action = self.board_num(action)
  151. xstart, ystart = action
  152. # 如果该位置已经有棋子或者出界,返回 False
  153. if not self.is_on_board(xstart, ystart) or self._board[xstart][ystart] != self.empty:
  154. return False
  155. # 临时将color放到指定位置
  156. self._board[xstart][ystart] = color
  157. # 棋手
  158. op_color = "O" if color == "X" else "X"
  159. # 要被翻转的棋子
  160. flipped_pos = []
  161. flipped_pos_board = []
  162. for xdirection, ydirection in [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0],
  163. [-1, 1]]:
  164. x, y = xstart, ystart
  165. x += xdirection
  166. y += ydirection
  167. # 如果(x,y)在棋盘上,而且为对方棋子,则在这个方向上继续前进,否则循环下一个角度。
  168. if self.is_on_board(x, y) and self._board[x][y] == op_color:
  169. x += xdirection
  170. y += ydirection
  171. # 进一步判断点(x,y)是否在棋盘上,如果不在棋盘上,继续循环下一个角度,如果在棋盘上,则进行while循环。
  172. if not self.is_on_board(x, y):
  173. continue
  174. # 一直走到出界或不是对方棋子的位置
  175. while self._board[x][y] == op_color:
  176. # 如果一直是对方的棋子,则点(x,y)一直循环,直至点(x,y)出界或者不是对方的棋子。
  177. x += xdirection
  178. y += ydirection
  179. # 点(x,y)出界了和不是对方棋子
  180. if not self.is_on_board(x, y):
  181. break
  182. # 出界了,则没有棋子要翻转OXXXXX
  183. if not self.is_on_board(x, y):
  184. continue
  185. # 是自己的棋子OXXXXXXO
  186. if self._board[x][y] == color:
  187. while True:
  188. x -= xdirection
  189. y -= ydirection
  190. # 回到了起点则结束
  191. if x == xstart and y == ystart:
  192. break
  193. # 需要翻转的棋子
  194. flipped_pos.append([x, y])
  195. # 将前面临时放上的棋子去掉,即还原棋盘
  196. self._board[xstart][ystart] = self.empty # restore the empty space
  197. # 没有要被翻转的棋子,则走法非法。返回 False
  198. if len(flipped_pos) == 0:
  199. return False
  200. for fp in flipped_pos:
  201. flipped_pos_board.append(self.num_board(fp))
  202. # 走法正常,返回翻转棋子的棋盘坐标
  203. return flipped_pos_board
  204. def get_legal_actions(self, color):
  205. """
  206. 按照黑白棋的规则获取棋子的合法走法
  207. :param color: 不同颜色的棋子,X-黑棋,O-白棋
  208. :return: 生成合法的落子坐标,用list()方法可以获取所有的合法坐标
  209. """
  210. # 表示棋盘坐标点的8个不同方向坐标,比如方向坐标[0][1]则表示坐标点的正上方。
  211. direction = [(-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1)]
  212. op_color = "O" if color == "X" else "X"
  213. # 统计 op_color 一方邻近的未落子状态的位置
  214. op_color_near_points = []
  215. board = self._board
  216. for i in range(8):
  217. # i 是行数,从0开始,j是列数,也是从0开始
  218. for j in range(8):
  219. # 判断棋盘[i][j]位子棋子的属性,如果是op_color,则继续进行下一步操作,
  220. # 否则继续循环获取下一个坐标棋子的属性
  221. if board[i][j] == op_color:
  222. # dx,dy 分别表示[i][j]坐标在行、列方向上的步长,direction 表示方向坐标
  223. for dx, dy in direction:
  224. x, y = i + dx, j + dy
  225. # 表示x、y坐标值在合理范围,棋盘坐标点board[x][y]为未落子状态,
  226. # 而且(x,y)不在op_color_near_points 中,统计对方未落子状态位置的列表才可以添加该坐标点
  227. if 0 <= x <= 7 and 0 <= y <= 7 and board[x][y] == self.empty and (
  228. x, y) not in op_color_near_points:
  229. op_color_near_points.append((x, y))
  230. l = [0, 1, 2, 3, 4, 5, 6, 7]
  231. for p in op_color_near_points:
  232. if self._can_fliped(p, color):
  233. # 判断p是不是数字坐标,如果是则返回棋盘坐标
  234. # p = self.board_num(p)
  235. if p[0] in l and p[1] in l:
  236. p = self.num_board(p)
  237. yield p
  238. def board_num(self, action):
  239. """
  240. 棋盘坐标转化为数字坐标
  241. :param action:棋盘坐标,比如A1
  242. :return:数字坐标,比如 A1 --->(0,0)
  243. """
  244. row, col = str(action[1]).upper(), str(action[0]).upper()
  245. if row in '12345678' and col in 'ABCDEFGH':
  246. # 坐标正确
  247. x, y = '12345678'.index(row), 'ABCDEFGH'.index(col)
  248. return x, y
  249. def num_board(self, action):
  250. """
  251. 数字坐标转化为棋盘坐标
  252. :param action:数字坐标 ,比如(0,0)
  253. :return:棋盘坐标,比如 (0,0)---> A1
  254. """
  255. row, col = action
  256. l = [0, 1, 2, 3, 4, 5, 6, 7]
  257. if col in l and row in l:
  258. return chr(ord('A') + col) + str(row + 1)

三个Player(随机玩家,人类玩家,AI玩家)

  1. import math
  2. import random
  3. import sys
  4. from game import Game # 导入黑白棋文件
  5. from copy import deepcopy
  6. class RandomPlayer:
  7. """
  8. 随机玩家, 随机返回一个合法落子位置
  9. """
  10. def __init__(self, color):
  11. """
  12. 玩家初始化
  13. :param color: 下棋方,'X' - 黑棋,'O' - 白棋
  14. """
  15. self.color = color
  16. def random_choice(self, board):
  17. """
  18. 从合法落子位置中随机选一个落子位置
  19. :param board: 棋盘
  20. :return: 随机合法落子位置, e.g. 'A1'
  21. """
  22. # 用 list() 方法获取所有合法落子位置坐标列表
  23. action_list = list(board.get_legal_actions(self.color))
  24. # 如果 action_list 为空,则返回 None,否则从中选取一个随机元素,即合法落子坐标
  25. if len(action_list) == 0:
  26. return None
  27. else:
  28. return random.choice(action_list)
  29. def get_move(self, board):
  30. """
  31. 根据当前棋盘状态获取最佳落子位置
  32. :param board: 棋盘
  33. :return: action 最佳落子位置, e.g. 'A1'
  34. """
  35. if self.color == 'X':
  36. player_name = '黑棋'
  37. else:
  38. player_name = '白棋'
  39. print("请等一会,对方 {}-{} 正在思考中...".format(player_name, self.color))
  40. action = self.random_choice(board)
  41. return action
  42. class HumanPlayer:
  43. """
  44. 人类玩家
  45. """
  46. def __init__(self, color):
  47. """
  48. 玩家初始化
  49. :param color: 下棋方,'X' - 黑棋,'O' - 白棋
  50. """
  51. self.color = color
  52. def get_move(self, board):
  53. """
  54. 根据当前棋盘输入人类合法落子位置
  55. :param board: 棋盘
  56. :return: 人类下棋落子位置
  57. """
  58. # 如果 self.color 是黑棋 "X",则 player 是 "黑棋",否则是 "白棋"
  59. if self.color == "X":
  60. player = "黑棋"
  61. else:
  62. player = "白棋"
  63. # 人类玩家输入落子位置,如果输入 'Q', 则返回 'Q'并结束比赛。
  64. # 如果人类玩家输入棋盘位置,e.g. 'A1',
  65. # 首先判断输入是否正确,然后再判断是否符合黑白棋规则的落子位置
  66. while True:
  67. action = input(
  68. "请'{}-{}'方输入一个合法的坐标(e.g. 'D3',若不想进行,请务必输入'Q'结束游戏。): ".format(player,
  69. self.color))
  70. # 如果人类玩家输入 Q 则表示想结束比赛
  71. if action == "Q" or action == 'q':
  72. return "Q"
  73. else:
  74. row, col = action[1].upper(), action[0].upper()
  75. # 检查人类输入是否正确
  76. if row in '12345678' and col in 'ABCDEFGH':
  77. # 检查人类输入是否为符合规则的可落子位置
  78. if action in board.get_legal_actions(self.color):
  79. return action
  80. else:
  81. print("你的输入不合法,请重新输入!")
  82. class Node:
  83. def __init__(self, now_board, parent=None, action=None, color=""):
  84. self.visits = 0 # 访问次数
  85. self.reward = 0.0 # 期望值
  86. self.now_board = now_board # 棋盘状态
  87. self.children = [] # 孩子节点
  88. self.parent = parent # 父节点
  89. self.action = action # 对应动作
  90. self.color = color # 该节点玩家颜色
  91. def get_ucb(self, ucb_param):
  92. if self.visits == 0:
  93. return sys.maxsize # 未访问的节点ucb为无穷大
  94. # UCB公式
  95. explore = math.sqrt(2.0 * math.log(self.parent.visits) / float(self.visits))
  96. now_ucb = self.reward/self.visits + ucb_param * explore
  97. return now_ucb
  98. # 生个孩子
  99. def add_child(self, child_now_board, action, color):
  100. child_node = Node(child_now_board, parent=self, action=action, color=color)
  101. self.children.append(child_node)
  102. # 判断是否完全扩展
  103. def full_expanded(self):
  104. # 有孩子并且所有孩子都访问过了就是完全扩展
  105. if len(self.children) == 0:
  106. return False
  107. for kid in self.children:
  108. if kid.visits == 0:
  109. return False
  110. return True
  111. class AIPlayer:
  112. """
  113. AI 玩家
  114. """
  115. def __init__(self, color):
  116. """
  117. 玩家初始化
  118. :param color: 下棋方,'X' - 黑棋,'O' - 白棋
  119. """
  120. self.max_times = 50 # 最大迭代次数
  121. self.ucb_param = 1 # ucb的参数C
  122. self.color = color
  123. def uct(self, max_times, root):
  124. """
  125. 根据当前棋盘状态获取最佳落子位置
  126. :param max_times: 最大搜索次数
  127. :param root: 根节点
  128. :return: action 最佳落子位置
  129. """
  130. for i in range(max_times): # 最多模拟max次
  131. selected_node = self.select(root)
  132. leaf_node = self.extend(selected_node)
  133. reward = self.stimulate(leaf_node)
  134. self.backup(leaf_node, reward)
  135. max_node = None # 搜索完成,然后找出最适合的下一步
  136. max_ucb = -sys.maxsize
  137. for child in root.children:
  138. child_ucb = child.get_ucb(self.ucb_param)
  139. if max_ucb < child_ucb:
  140. max_ucb = child_ucb
  141. max_node = child # max_node指向ucb最大的孩子
  142. return max_node.action
  143. def select(self, node):
  144. """
  145. :param node:某个节点
  146. :return: ucb值最大的叶子
  147. """
  148. # print(len(node.children))
  149. if len(node.children) == 0: # 叶子,需要扩展
  150. return node
  151. if node.full_expanded(): # 完全扩展,递归选择ucb最大的孩子
  152. max_node = None
  153. max_ucb = -sys.maxsize
  154. for child in node.children:
  155. child_ucb = child.get_ucb(self.ucb_param)
  156. if max_ucb < child_ucb:
  157. max_ucb = child_ucb
  158. max_node = child # max_node指向ucb最大的孩子
  159. return self.select(max_node)
  160. else: # 没有完全扩展就选访问次数为0的孩子
  161. for kid in node.children: # 从左开始遍历
  162. if kid.visits == 0:
  163. return kid
  164. def extend(self, node):
  165. if node.visits == 0: # 自身还没有被访问过,不扩展,直接模拟
  166. return node
  167. else: # 需要扩展,先确定颜色
  168. if node.color == 'X':
  169. new_color = 'O'
  170. else:
  171. new_color = 'X'
  172. for action in list(node.now_board.get_legal_actions(node.color)): # 把所有可行节点加入孩子列表,并初始化
  173. new_board = deepcopy(node.now_board)
  174. new_board._move(action, node.color)
  175. # 新建节点
  176. node.add_child(new_board, action=action, color=new_color)
  177. if len(node.children) == 0:
  178. return node
  179. return node.children[0] # 返回新的孩子列表的第一个,以供下一步模拟
  180. def stimulate(self, node):
  181. """
  182. :param node:模拟起始点
  183. :return: 模拟结果reward
  184. board.get_winner()会返回胜负关系和获胜子数
  185. 考虑胜负关系和获胜的子数,定义获胜积10分,每多赢一个棋子多1分
  186. """
  187. board = deepcopy(node.now_board)
  188. color = node.color
  189. count = 0
  190. while (not self.game_over(board)) and count < 50: # 游戏没有结束,就模拟下棋
  191. action_list = list(node.now_board.get_legal_actions(color))
  192. if not len(action_list) == 0: # 可以下,就随机下棋
  193. action = random.choice(action_list)
  194. board._move(action, color)
  195. if color == 'X':
  196. color = 'O'
  197. else:
  198. color = 'X'
  199. else: # 不能下,就交换选手
  200. if color == 'X':
  201. color = 'O'
  202. else:
  203. color = 'X'
  204. action_list = list(node.now_board.get_legal_actions(color))
  205. action = random.choice(action_list)
  206. board._move(action, color)
  207. if color == 'X':
  208. color = 'O'
  209. else:
  210. color = 'X'
  211. count = count + 1
  212. # winner:0-黑棋赢,1-白旗赢,2-表示平局
  213. # diff:赢家领先棋子数
  214. winner, diff = board.get_winner()
  215. if winner == 2:
  216. reward = 0
  217. elif winner == 0:
  218. # 这里逻辑是反的,写出了bug...应该是其他地方逻辑也反了一次,负负得正了...实在不想找bug了对不住
  219. reward = 10 + diff
  220. else:
  221. reward = -(10 + diff)
  222. if self.color == 'X':
  223. reward = - reward
  224. return reward
  225. def backup(self, node, reward):
  226. """
  227. 反向传播函数
  228. """
  229. while node is not None:
  230. node.visits += 1
  231. if node.color == self.color:
  232. node.reward += reward
  233. else:
  234. node.reward -= reward
  235. node = node.parent
  236. return 0
  237. def game_over(self, board):
  238. """
  239. 判断游戏是否结束
  240. :return: True/False 游戏结束/游戏没有结束
  241. """
  242. # 根据当前棋盘,双方都无处可落子,则终止
  243. b_list = list(board.get_legal_actions('X'))
  244. w_list = list(board.get_legal_actions('O'))
  245. is_over = (len(b_list) == 0 and len(w_list) == 0) # 返回值 True/False
  246. return is_over
  247. def get_move(self, board):
  248. """
  249. 根据当前棋盘状态获取最佳落子位置
  250. :param board: 棋盘
  251. :return: action 最佳落子位置, e.g. 'A1'
  252. """
  253. if self.color == 'X':
  254. player_name = '黑棋'
  255. else:
  256. player_name = '白棋'
  257. print("请等一会,对方 {}-{} 正在思考中...".format(player_name, self.color))
  258. root = Node(now_board=deepcopy(board), color=self.color)
  259. action = self.uct(self.max_times, root)
  260. return action
  261. # 黑棋初始化
  262. black_player = AIPlayer("X")
  263. # 白棋初始化
  264. white_player = AIPlayer("O")
  265. # 游戏初始化,第一个玩家是黑棋,第二个玩家是白棋
  266. game = Game(black_player, white_player)
  267. # 开始下棋
  268. game.run()

效果图:

算法较为简陋,不过是完成作业罢了,还请大佬们指正。

标签: python 人工智能

本文转载自: https://blog.csdn.net/weixin_58691194/article/details/124067745
版权归原作者 永不秃头的屑 所有, 如有侵权,请联系我们删除。

“Python实现蒙特卡洛树黑白棋完整代码”的评论:

还没有评论