基于决策树的五子棋

人工智能五子棋#

背景介绍#

五子棋是两名玩家使用黑白两种棋子轮流在15*15的棋盘上下棋,只要白方或黑方的棋子在横,竖或斜三个方向上任意一个方向能够连成五子,则判定为胜利。
本次设计五子棋游戏为真人玩家与AI对战,真人持黑棋先下,AI持白棋后下。

程序设计思路及主要方法#

整个游戏框架采用pygame框架实现。

棋盘#

棋盘界面使用网上棋盘的图片,图片要相对规整,格子大小分布均匀。
此次使用棋盘图片大小为664*669,棋盘中每个小格长约为44,宽约为45,左右上下两侧余24,16。

棋子#

黑白棋子采用绘制黑白实心圆圈代替,每个棋子的半径为16。
棋子的位置:每次落子时,记录鼠标的位置,因为人点击的位置不可能正好落在某点上,将点击位置转化为15*15的坐标,如最中心的位置为(7,7),代码如下,通过减去两侧的多余位置,再除以格子的大小,最后四舍五入,化为整数坐标。(round((i - 16) / 45), (round((j - 24) / 44)) 最后绘制时,只要采用逆过程即可。

算法设计与实现#

算法采用基于博弈树的MAX-MIN算法。

生成博弈树#

针对每一种状态,以当前状态为根生成2步的博弈树,以下图为例,因为真人玩家先下,所以起始棋局上已经有落子。之后针对该棋局生成多个或树节点,每个节点就是叉号可能落子的位置。最后再向下进行生成,生成与树节点,每个节点是针对父节点的棋局,圆圈所有可能落子的位置。

计算代价函数值#

因为从智能一方考虑,代价函数值越大,对智能一方越有利。
本次设计的代价函数算法如下所示:
首先针对当前棋局计算所有落子的周边位置,如图,三角形所标位置为落子周边的所有位置,即每个落子的上下左右,左上,左下,右上,右下。

之后计算这些位置摆放对方棋子的得分fail和摆放自己棋子的得分win,每个位置得分计算规则为,以摆放棋子处位置为中心,如果能连成1个记一分,2个记两分,依次类推。
摆放对方棋子如图所示:

计算得分以上图中1号位置所示,它只能与下面棋子连成2个,所以记两分,其它位置同理,最后得出一个总分:

最后将摆自己棋子的得分win减去摆对方棋子的得分fail,得到每个叶子结点的得分。
以下图为例,假设第一个得分5,然后等等。父节点如果是与结点,那么它的得分为所有子子结点的最小值,假设为2,父节点如果为或结点,那么它的得分为所有子节点的最大值。因此最后得到根节点的值。

确定下一步#

最后选择使根节点得分最高的那一步,如下红箭头所示,即机器的下一步要下在叉号的位置。

不断循环#

之后,对方每下一步,算法就以当前棋局为根节点,进行如上的步骤,不断循环。

游戏输赢判定设计与实现#

每下一步棋,就是用上一节所述算法,以这步棋的位置为中心,向八个方向进行搜索,判定是否存在五个相同棋子相连的状况,黑棋成功则更新游戏状态为1,判定成功,输出获胜信息,白棋成功则更新游戏状态为2,判定成功,输出获胜信息。否则游戏状态为0,继续进行游戏。

实验结果展示及分析#

如下所示,为一局棋的对弈,能够较好的完成自动与人对弈的任务,但是多次运行后发现也存在着几个问题,可以进一步改进。
(1)程序运行速度慢。每一步棋计算都有明显的停顿,给人体验不好。我分析可能一是15*15的棋盘导致运算量过大。二是我在编程时在每一个节点都额外存储了一个当前的棋盘状况。解决这个问题可能采用α-β剪枝的方法,减少计算量,也可能需要进一步优化我的代码。
(2)自动下棋的第一步总是在左上角。我分析由于本次算法只采取了两步推理(多步推理却又造成程序运行过慢),所以在最开始计算时,白棋的的很多位置的得分都是一样的,所以按顺序挑选了一个位置,但是按照常识,越靠中间的位置才是得分越好的,这一步需要继续改进。
(3)白棋的行为总是在堵。每次下棋即使白棋能够胜利,它也要下堵黑棋的一步。我分析一方面是推理步数过少,一方面是代价函数,即得分规则,还是不够细致。当连成3个或4个时,往往会产生比1个,2个更大的威胁,所以响应的得分应该相差更高,同时能成5个的状态应该是一个最大的值。并且我查阅资料,五子棋的规则其实应该更加复杂和细致。比如连成4个时,一侧被堵,两侧被堵,和没有被堵的情况应该是完全不一样的,连成3个时,也是如此,所以应该定义更细致的状态判断和得分规则。

代码#

Copy
import sys import numpy as np import pygame import copy #树结构 class Tree: def __init__(self): self.weight = None#节点分值 self.child = []#孩子节点列表 self.father = None #父节点 self.manual = None #当前节点对应棋谱 self.flag = None #0为或树根节点,1为与树根节点 self.move = None # 当前节点对应的一步棋 #构建树结构 def build_tree(node, step=0): #step表示分析深度 if (step == 2): return #遍历15*15棋盘,穷举当前状态所有可能的下一步 for i in range(15): for j in range(15): if (node.manual[i][j] == 0): child_node = Tree() child_node.flag = (1 + node.flag) % 2 manual = copy.deepcopy(node.manual) manual[i][j] = child_node.flag + 1 child_node.move = (i, j) child_node.manual = manual child_node.father = node node.child.append(child_node) #深度迭代 build_tree(child_node, step + 1) # 八个方位的迭代搜索,输入当前棋盘state和下的一步棋(i,j),这步棋对应的颜色flag,得到以这步棋为中心在这个反向与这个棋颜色一样的个数number def find_up(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_up(state, i - 1, j, flag, number) def find_down(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_down(state, i + 1, j, flag, number) def find_left(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_left(state, i, j - 1, flag, number) def find_right(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_right(state, i, j + 1, flag, number) def find_l_u(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_l_u(state, i - 1, j - 1, flag, number) def find_r_u(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_r_u(state, i - 1, j + 1, flag, number) def find_l_d(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_l_d(state, i + 1, j - 1, flag, number) def find_r_d(state, i, j, flag, number=0): if (i < 0 or i >= 15): return number if (j < 0 or j >= 15): return number if (state[i][j] != flag): return number number = number + 1 return find_r_d(state, i + 1, j + 1, flag, number) #根据探测的位置,以(i,j)为中心打分,总分为连续个数的和,如组成2个三个,1个1个,则总分为3+3+1 def score(state, position, flag): sum = 0 for p in position: i, j = p if (i >= 0 and j >= 0 and i < 15 and j < 15 and state[i][j] == 0): a = find_up(state, i - 1, j, flag) b = find_down(state, i + 1, j, flag) c = find_left(state, i, j - 1, flag) d = find_right(state, i, j + 1, flag) e = find_l_u(state, i - 1, j - 1, flag) f = find_r_d(state, i + 1, j + 1, flag) g = find_l_d(state, i + 1, j - 1, flag) h = find_r_u(state, i - 1, j + 1, flag) sum = (a + b + 1) + (c + d + 1) + (e + f + 1) + (g + h + 1) + sum return sum # 根据当前棋盘探测位置,即在下过的棋子周围描边,记录这些位置 def probe(state): position = [] for i in range(15): for j in range(15): if (state[i][j] != 0): position.extend( [(i - 1, j - 1), (i - 1, j + 1), (i + 1, j - 1), (i + 1, j + 1), (i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)]) return set(position) #检查是否连成5个 def check(state, position, flag): i, j = position a = find_up(state, i - 1, j, flag) b = find_down(state, i + 1, j, flag) c = find_left(state, i, j - 1, flag) d = find_right(state, i, j + 1, flag) e = find_l_u(state, i - 1, j - 1, flag) f = find_r_d(state, i + 1, j + 1, flag) g = find_l_d(state, i + 1, j - 1, flag) h = find_r_u(state, i - 1, j + 1, flag) if (a + b + 1 == 5 or c + d + 1 == 5 or e + f + 1 == 5 or g + h + 1 == 5): return True else: return False pygame.init() #敞口大小 size = width, height = 664, 669 screen = pygame.display.set_mode(size) pygame.display.set_caption('五子棋') #设置背景棋盘 background = 'C:/Users/DELL/Desktop/2.png' chessboard = pygame.image.load(background) #记录棋位置,用于绘图 chess = [] #游戏状态,0进行中,1黑骑生,2白棋胜 game_state = 0 #逻辑棋盘 state=list(np.zeros((15,15))) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() #点击鼠标下棋 if (game_state == 0) and (event.type == pygame.MOUSEBUTTONDOWN): #黑棋下棋。得到鼠标点击位置,棋盘中每个小格长约为44,宽约为45,左右上下两侧余24,16 i, j = pygame.mouse.get_pos() chess.append((round((i - 16) / 45), (round((j - 24) / 44)), 1)) state[round((i - 16) / 45)][round((j - 24) / 44)]=1 #判断是否获胜 if(check(state,(round((i - 16) / 45), (round((j - 24) / 44))),1)): game_state=1 # 建树 node = Tree() node.manual = state node.flag = 0 node.move = (round((i - 16) / 45), round((j - 24) / 44)) node_list = [] best = 0 build_tree(node) #相当于对树层次遍历 for i in node.child: node_list.append(i) for j in i.child: node_list.append(j) for n in j.child: node_list.append(n) #从叶子结点计算 for m in reversed(node_list): #判断是否为叶子结点 if(m.child==[]): position = probe(m.manual) #对手为1,黑棋 fail = score(m.manual, position, 1) win = score(m.manual, position, 2) # 得分 m.weight = win-fail #更新父节点 if (m.father.flag == 0): if (m.father.weight == None): m.father.weight = m.weight elif (m.weight > m.father.weight): m.father.weight = m.weight if (m.father.flag == 1): if (m.father.weight == None): m.father.weight = m.weight elif (m.weight < m.father.weight): m.father.weight = m.weight #找出下一步棋 best = node.child[0].weight best_node = node.child[0] for i in node.child: if (i.weight > best): best = i.weight best_node = i i,j=best_node.move #白棋下棋 chess.append((i,j, 2)) state[i][j]=2 #判断白棋是否获胜 if(check(state,(round((i - 16) / 45), (round((j - 24) / 44))),2)): game_state=2 #绘制棋盘 screen.blit(chessboard, (0, 0)) for i, j, flag in chess: if (flag == 1): pygame.draw.circle(screen, (0, 0, 0), [i * 45 + 16, j * 44 + 24], 16, 0) else: pygame.draw.circle(screen, (255, 255, 255), [i * 45 + 16, j * 44 + 24], 16, 0) #宣布获胜方 if game_state == 1: font = pygame.font.Font(None, 60) text = font.render('black win', True, (0, 0, 0)) screen.blit(text, (250, 250)) if game_state == 2: font = pygame.font.Font(None, 60) text = font.render('white win', True, (255, 255, 255)) screen.blit(text, (250, 250)) pygame.display.update()
posted @   启林O_o  阅读(709)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示
CONTENTS