基于决策树的五子棋
人工智能五子棋
背景介绍
五子棋是两名玩家使用黑白两种棋子轮流在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个时,也是如此,所以应该定义更细致的状态判断和得分规则。
代码
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()