赞
踩
最近看到一个小游戏,感觉很有意思,想着如何让电脑学会跟人下。于是做了一些简单的探索,完成了基本的AI模拟,这里的算法是直接使用特征来进行差别。搜索深度也只做了2步,还可以做更深度的搜索,也可以使用深度模型来训练,就算是抛砖引玉吧。
版本历史
1.0.3 优化WEB界面,增加人机比分展示;
1.0.2 完成HTTP服务端包装,可通过网页实现对弈;
1.0.1 完成AI算法,可进行人机对弈;
1.0.0 完成重力四子棋基本框架,可进行命令行对弈;
先汇总一下相关资源:
重力四子棋在线游戏 http://connect4.hexi5.com/
重力四子棋源码:https://github.com/xmxoxo/connect4
重力四子棋共有7行6列
每次只能选择在一个列中下一个棋子;
由于重力作用,所下的棋子会下落到该列的最下方;
一方棋子在任意直线方向上连成4个棋子即获胜;
当然,根据这个规则还可以把游戏变得更复杂:
棋盘也可以变成更大,四子也可以变成N子;
甚至可以把二维变成三维:最强大脑里的立体重力四子棋,有兴趣的可以自行百度
如果对规则不是很了解的话,可以到马上试玩一下: 重力四子棋
首先是实现重力四子棋的基本方法,可以下棋,换游戏者,判断胜负等。
这些过程相对比较简单,重点的就说几个地方吧:
一是下棋:重力作用会让棋落到最下面。
这个的实现是基于原来的代码结构,使用了一个column的类来完成。其实也可以简化成使用numpy的判断。
二是判断胜负: 搜索所有的棋判断有没有连成4个。
搜索的时候是从上到下,从左到右,找到每一个棋子,
然后沿着:右,右下,下,左下,这四个方向去查找。
为什么是这四个方向呢,因为另外四个方向其实是对称的,已经包含在上面的方向里了。
只要在方向上能找到连续的另外三颗同色的棋子,就说明获胜了。
基本的实现过程可以看源码中的 Connect_N.ipynb
整个AI模拟的思路过程也都写在了Connect_N.ipynb
文件中。
这里把思路重新理一下:
棋盘状态特征的目的是针对当前棋盘状态,提取出棋子的特征值。
棋子的特征值可以分为针对游戏者双方分别给出特征值,提取的方法是相同的。
最终的特征为已方棋子特征加上对方棋子的特征,即:
[[已方特征],[对方特征]]
棋子特征主要是要能够描述棋子的好坏,棋子的发展变化情况,棋子离目标状态的远近。
这里参考围棋的思路,主要引入几个核心要素:棋串,棋长,真气,长气,气度等。
棋串是指一个或者多个在一条直线上的棋子连成的串;
棋串包括横向,竖向和斜向(正反)四个方向。
每个棋子可以包含在不同的棋串中。
棋串用chess表示
○○○○○○
○○▲○○○
○●●▲○○
上图中●方共有子串三个:
● 位置:((3,2))
● 位置:((3,3))
●● 位置:((3,2),(3,3))
▲方共有子串三个:
▲ 位置:((3,2))
▲ 位置:((3,3))
▲▲ 位置:((2,3),(3,4)) 斜线
棋长是指棋串的最大长度,是一个整数;
棋长用maxlen表示;
可以很容易推断出,对于N子棋来说:
真气是指棋串周边可以下棋的空位位置,位置包括斜方向,与围棋的气的计算相同。
真气的数量越多,表示棋串未来的发展变化程度越大。
棋串的真气数量用gas_count表示;
对于真气为0的棋串,已经没有发展的空间,可以不需要参与计算;
棋串的每一个真气,有些真气是马上可以“落下棋”的位置,而有些是未来才能“落下棋”的位置;
气度就是要多少步才能“落下棋”到该位置,用一个整数来表示,0表示马上就可以落棋;
对于N子棋来说,气度的取值范围为0至N-1;
显然,气度为0的真气就是下一步要落子的位置。
长气是指棋串的真气中,可以让棋长变大的真气;
长气的个数最小值为0,最大值为2;
显然可见,对于4子棋来说,如果出现:一个棋串长度为3,长气个数为2,长气的气度均为0,则为必胜棋。
如下图:
○○○○○○
○○○○○○
○○○○○○
○○○○○○
○○○○○○
○○▲▲▲○
○○●●●○
对于●方:
最大棋串的长度为3,真气个数为4,左右两边分别有一个长气,即长气个数为2,并且这两个长气的气度均为0。
此时无论对方下哪个位置,这两个位置二者必得其一,所以是必胜的局面。
对于4子棋来说,如果最大棋串长度为3,长气个数为1,且长气的气度为0,则为必应局面,也就是必须抢占这个位置。
如下图:
○○○○○○
○○○○○○
○○○○○○
○○○○○○
○○○○○○
○○○▲▲▲
○○○●●●
对于●
方:
最大棋串长度为3,长气个数为1,气度为0,也就是再下到这里就获胜了,所以是双方抢占的位置。
对于已方来说,叫获胜位;对于对方来说,叫必应位。
棋盘状态使用各个棋串表示,搜索出当前棋盘下的所有棋串,去除掉包含的子串以及死串。
重点:使用棋串长度l,长气气度d1,长气气度d2 这三个值组成的特征数组表示一个棋串
例如:
[3,1,1] 必胜棋串
[3,0,1] 必应棋串
[3,2,2] 带禁入的棋串,
[2,0,2] 斜向有发展前途的棋串
使用以下公式来计算一个棋串的分值:
value = 10**(l+2) + 10**(l+1)/d1**3 + 10**(l+1)/d2**3 + 10**(l+1)/(d1*d2)**2 + 10**(l)/(d1+d2)**2
公式中,要排除掉d1,d2为0的情况,避免出现计算错误。
棋盘的总得分即为所有棋串的分值总和。
这样设计公式的原因是把棋串长度,气度1,气度2分别放在不同的位上进行区分;
当棋串长度从3变为4时,棋串得分立即从10万等级上长到100万等级,因此很容易判断是否获胜。
同时考虑了气度1、气度2为0;以及同时为1的情况,公式的设计可以实现同长度棋串的不同气度排序:
[x,1,1] > [x, 1, 2] > [x, 1, 3] > [x, 0,1] > [x, 0, 2]
# 棋串得分计算公式 def chessvalue(l,d1,d2): if l<4 and d1==d2==0: return 0 value = 10**(l+2) k1 = d1*d2 k2 = d1+d2 v1 = 0 if d1==0 else 10**(l+1)/d1**3 v2 = 0 if d2==0 else 10**(l+1)/d2**3 value += v1+v2 if k1>0: value += (10**(l+1) / k1**1.5) if k2>0: value += (10**(l) / k2) #value *= 100 return int(value) testv = [[5,0,0], [4,1,1], [4,0,0], [4,0,1], [4,0,2], [3,1,1], [3,1,2], [3,1,3], [3,1,4], [3,0,1], [3,1,0], [3,2,2], [3,2,3], [3,2,4], [3,0,2], [3,0,3], [3,0,0], [2,1,1], [2,0,1], [2,1,0], [2,1,2], [2,1,3], [2,0,2], [2,0,3], [2,1,4], [2,0,0], [1,1,1], [1,0,1], [1,0,0], ] for x in testv: print('{0} ==> {1:>10,d}'.format(str(x), chessvalue(*x)))
运行完的结果:
[5, 0, 0] ==> 10,000,000 [4, 1, 1] ==> 1,305,000 [4, 0, 0] ==> 1,000,000 [4, 0, 1] ==> 1,110,000 [4, 0, 2] ==> 1,017,500 [3, 1, 1] ==> 130,500 [3, 1, 2] ==> 114,083 [3, 1, 3] ==> 111,731 [3, 1, 4] ==> 110,981 [3, 0, 1] ==> 111,000 [3, 1, 0] ==> 111,000 [3, 2, 2] ==> 103,375 [3, 2, 3] ==> 102,098 [3, 2, 4] ==> 101,729 [3, 0, 2] ==> 101,750 [3, 0, 3] ==> 100,703 [3, 0, 0] ==> 0 [2, 1, 1] ==> 13,050 [2, 0, 1] ==> 11,100 [2, 1, 0] ==> 11,100 [2, 1, 2] ==> 11,408 [2, 1, 3] ==> 11,173 [2, 0, 2] ==> 10,175 [2, 0, 3] ==> 10,070 [2, 1, 4] ==> 11,098 [2, 0, 0] ==> 0 [1, 1, 1] ==> 1,305 [1, 0, 1] ==> 1,110 [1, 0, 0] ==> 0
有了棋串的得分,把一个局面中所有的棋串得分加起来,就是当前局面的得分了。
代码如下:
def all_chess(self, board=None): '''盘面分析,获取所有的棋串,并计算得分''' #判断子串重复 check_repeat = lambda m,n: all( map(lambda x:x in n, m)) ret = [[],[]] # 每个点沿右,右下,下,左下4个方向最多走3步,每一步都必须与当前点同色,如果能走到3步则表示获胜 dire = [[0,1],[1,1],[1,0],[1,-1]] if board is None: bd = self.board else: bd = board for x in range(bd.shape[0]): for y in range(bd.shape[1]): po = bd[x,y] if po>0: #ret[po-1].append([(x,y)]) for d in dire: step=0 chess = [] #chess.append(po) chess.append((x,y)) xn,yn = x,y #----- 计算气度1:d1 d1 = 0 px, py = x-d[0],y-d[1] if 0<=px<bd.shape[0] and 0<=py<bd.shape[1]: if bd[px,py]==0: k = bd[:, py][::-1][:bd.shape[0]-px] d1 = len(list(filter(lambda x:x==0,k))) #----- #最大深入3级 for s in range(3): # 计算下一个点的位置 xn,yn = xn+d[0],yn+d[1] # 判断点是否在区域内 if 0<=xn<bd.shape[0] and 0<=yn<bd.shape[1]: if bd[xn,yn]==po: chess.append((xn,yn)) step += 1 else: break else: break #----- 计算气度2:d2 d2 = 0 #xn,yn = xn+d[0],yn+d[1] if 0<=xn<bd.shape[0] and 0<=yn<bd.shape[1]: if bd[xn,yn]==0: k = bd[:, yn][::-1][:bd.shape[0]-xn] d2 = len(list(filter(lambda x:x==0,k))) #----- # 记录棋串 #if step>0: # 判断是重重复 if not any(map(lambda x:check_repeat(chess,x[0]), ret[po-1])): value = chessvalue(len(chess),d1,d2) ret[po-1].append([chess,[len(chess), d1, d2],value]) # 记录棋串长度,气度1,气度2 # 棋串排序,按棋长 #mysort = lambda r: sorted(r,key=lambda x:-len(x[0])) # 棋串排序,按棋串得分 mysort = lambda r: sorted(r,key=lambda x:-x[-1]) ret = list(map(mysort,ret)) return ret
有了局面的分值,就可以进行评估了,这里来进行模拟一下:
board = Board(4) board.drop_disk(6) board.drop_disk(5) board.drop_disk(5) board.drop_disk(4) board.drop_disk(5) board.drop_disk(4) board.drop_disk(4) board.drop_disk(3) board.drop_disk(3) board.drop_disk(3) #board.drop_disk(3) board.display() ai = AI(board) allchess = ai.all_chess() #v = map(lambda y: sum(map(lambda x:x[-1], y)), allchess) v1, v2 = ai.score() print('棋盘状态:') print('玩家1 得分:{0:,d}'.format(v1)) ret = list(map(print,allchess[0])) print('-'*30) print('玩家2 得分:{0:,d}'.format(v2)) ret = list(map(print,allchess[1]))
运行结果如下:
------------重力棋游戏------------- 玩家:● 电脑:▲ 1 2 3 4 5 6 ○○○○○○ ○○○○○○ ○○○○○○ ○○○○○○ ○○▲●●○ ○○●▲●○ ○○▲▲▲● 1 2 3 4 5 6 当前轮到: [●]玩家 棋盘状态: 玩家1 得分:145,325 [[(4, 3), (5, 4), (6, 5)], [3, 1, 0], 111000] [[(4, 3), (5, 2)], [2, 1, 1], 13050] [[(4, 4), (5, 4)], [2, 1, 0], 11100] [[(4, 3), (4, 4)], [2, 0, 2], 10175] ------------------------------ 玩家2 得分:212,413 [[(6, 2), (6, 3), (6, 4)], [3, 1, 0], 111000] [[(4, 2), (5, 3), (6, 4)], [3, 4, 0], 100406] [[(4, 2)], [1, 3, 0], 1007] [[(5, 3), (6, 3)], [2, 0, 0], 0] [[(5, 3), (6, 2)], [2, 0, 0], 0]
有了局面的分值,就可以计算出局面的分值变化了,最终的目标就是让下一步棋后,得到分值更大的局面。当然这里也可以判断分值超过百万就是获胜了。
AI就是去计算每一步棋下完后的局面得分,然后按分值的高低排序即可。
以下是下一步棋推荐的核心代码:
def nextstep(self, player): '''预测出下一步最好的棋应该下在哪里''' '''计算下一步可下棋的位置''' pos = np.where (self.board[0]==0)[0]+1 #print(pos) #模拟在某一列下一个棋 def dropstep(bd,col,player): bd = bd.copy() #print('-'*30) #print(bd) k = bd[:, col][::-1] row = bd.shape[0] - k.tolist().index(0) - 1 bd[row,col] = player #print('-'*30) #print('drop:',col) #print(bd) return bd # 当前得分 current_score = np.array(self.score()) #print('current_score:', current_score) #分别计算每一个可下位置下棋后的状态得分变化值 def nextscore(bd, current_score, player): nplayer = 1 if player==2 else 2 f_score = np.array([current_score] * bd.shape[1]) '''计算下一步可下棋的位置''' pos = np.where (self.board[0]==0)[0] for p in pos: #模拟下一步棋后的状态 nbd = dropstep(bd, p, player) # 计算状态及得分 v_chess = self.all_chess(board=nbd) v_score = list(map(lambda y: sum(map(lambda x:x[-1], y)), v_chess)) f_score[p] = v_score f_score -= current_score return f_score score_change = nextscore(self.board, current_score, player ) #print('score_change:', score_change) pp = [1,-2] if player == 2: pp = [-2, 1] score_change = score_change * pp # 得分变化计算方式:已方增加值值 - 对方增加值 t_score = score_change[:,0] - score_change[:,1] #print(t_score) # 排序 idx = np.array(t_score).argsort()[::-1] + 1 #print('sorted:', idx) nt = np.sort(t_score,axis=0)[::-1] #ns = np.sort(score_change,axis=0)[::-1] #nf = np.sort(f_score,axis=0)[::-1] rpos = list(zip(idx, nt.tolist())) #, ns.tolist(), nm.tolist() return rpos
针对上面的局面,让AI推荐一下:
recommand = ai.nextstep(1)
print(recommand)
print('-'*30)
recommand = ai.nextstep(2)
print(recommand)
运行结果如下;
[(3, 892909), (5, 198948), (4, 23813), (6, 23125), (1, 1110), (2, -123436)]
------------------------------
[(3, 210798), (5, 24983), (1, -1110), (6, -1850), (4, -10635), (2, -995407)]
可以看到,都是推荐下在第3列,后面的值是得分的变化值。
上周末跟儿子一起下这个重力四子棋,没想到连输了好几盘,答应他这周末搞出一个AI来跟他下,看来基本上是完成了。
整个过程中对于numpy、flask、session、ajax等的使用能力都有所提升,代码里也有一些小小的通用功能点实现,可以作为后续工程的参考。
例如:对array的各类旋转、切片操作;加载favicon.ico;记录人和AI的获胜得分比;
当然代码中还有很多可以优化的地方:
Column类其实并不需要,可以优化掉。
棋串的得分函数还存在优化的空间;
AI下一步棋推荐算法中,只计算了两步:自己下完,以及对手再下完后的局面。
这样其实并没有看到最后能否获胜。
下一步棋推荐算法中,使用的是循环,不方便指定“AI算力”。如果改成递归或者加上深度的判断,可以指定AI的智力程度。
对于这类游戏,还可以使用深度模型来训练,得到一个非常强大的对弈模型。
希望有兴趣的小伙伴们可以继续,做出更好的更强的AI。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。