使用Selenium自动玩2048
2014年8月20日 01:24
前几天看summer师弟玩Selenium感觉挺有意思。便拿着时间风靡一时的游戏“2048”来练手,写了个简单的 AI。甚是欢乐!
Selenium
啥是selenimu,简单的说——Selenium也是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE、Mozilla Firefox、Mozilla Suite等(来自 http://www.51testing.com/zhuanti/selenium.html)。
selenimu这里就不多介绍了,细节请看 selenium 的 python api 文档
2048策略
2048这个游戏地球人都知道就不介绍了。主要介绍下我的AI。我的AI对于每一个方向的评估有三个方面
- 移动导致合并的得分 score:这个就是游戏本身定义的得分,如 4和4合并得8分,128和128合并的256分
- 移动后每一行每一列的单调性 monotone:对于每一行(每一列)如果 line[i] <= line[i+1]则mon+= line[i]+line[i+1]否则,mon-=line[i]+line[i+1],monotone=sum(abs(mon))
- 移动后相邻块值相同的情况 adjoin:任意两个相邻块的值相同,如cells[i][j]=cells[i+1][j],则 adjoin+=cells[i][j]
最后的估值 estimation = score + monotone * 0.3 + adjoin。对于上下左右四个方向取estimation最大的方向操作
这种方法还可以,运气好的话,可以得到2048

程序结构
程序的源代码如下:
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import os
import time
size = 4
class Estimator:
def estimate(self, precells, postcells, action, score):
for i in range(size):
score += self.__estimate_line([postcells[i][j] for j in range(size)])
score += self.__estimate_line([postcells[j][i] for j in range(size)])
return score
def __estimate_line(self, line):
monotone, adjoin = 0, 0
for i in range(size - 1):
if line[i + 1] > line[i]:
monotone += line[i + 1] + line[i]
else:
monotone -= line[i + 1] + line[i]
if line[i + 1] == line[i]:
adjoin += line[i]
return abs(monotone) * .3 + adjoin
class Auto2048:
def __init__(self, url, estimator):
self.browser = webdriver.Firefox()
self.browser.get(url)
self.estimator = estimator
def get_cells(self):
tiles = self.browser.find_elements_by_class_name('tile')
self.cells = [[0 for i in range(4)] for i in range(4)]
for tile in tiles:
attr = tile.get_attribute('class').split()
value = int(attr[1].split('-')[1])
x = int(attr[2].split('-')[3]) - 1
y = int(attr[2].split('-')[2]) - 1
self.cells[x][y] = value
def AI(self):
self.get_cells()
self.Print(self.cells)
action, actionname = '', ''
moveable = False
strategies = [ {'fun': self.try_up, 'action': Keys.UP, 'name': 'Up'},
{'fun': self.try_down, 'action': Keys.DOWN, 'name': 'Down'},
{'fun': self.try_left, 'action': Keys.LEFT, 'name': 'Left'},
{'fun': self.try_right, 'action': Keys.RIGHT, 'name': 'Right'}]
for strategy in strategies:
result = strategy['fun']()
estimation = self.estimator.estimate(self.cells, result['cells'], strategy['name'], result['score'])
if result['moveable'] and (moveable == False or max_estimation < estimation):
action = strategy['action']
max_estimation = estimation
moveable = True
actionname = strategy['name']
if not moveable:
return False
self.browser.find_element_by_class_name('grid-container').send_keys(action)
print 'Action: ', actionname
return True
def move_left(self, cells):
moveable = False
score = 0
for x in range(size):
pre = 0
for y in range(size):
if cells[x][y]:
cells[x][pre] = cells[x][y]
if y != pre:
moveable = True
cells[x][y] = 0
pre += 1
for y in range(size - 1):
if cells[x][y] and cells[x][y] == cells[x][y + 1]:
cells[x][y] += cells[x][y]
score += cells[x][y]
cells[x][y + 1] = 0
moveable = True
pre = 0
for y in range(size):
if cells[x][y]:
cells[x][pre] = cells[x][y]
if y != pre:
moveable = True
cells[x][y] = 0
pre += 1
return {'moveable': moveable, 'score': score, 'cells': cells}
def try_left(self):
cells = [[self.cells[i][j] for j in range(size)] for i in range(size)]
return self.move_left(cells)
def try_right(self):
cells = [[self.cells[i][size - 1 - j] for j in range(size)] for i in range(size)]
result = self.move_left(cells)
result['cells'] = [[result['cells'][i][size - 1 - j] for j in range(size)] for i in range(size)]
return result
def try_up(self):
cells = [[self.cells[j][i] for j in range(size)] for i in range(size)]
result = self.move_left(cells)
result['cells'] = [[result['cells'][j][i] for j in range(size)] for i in range(size)]
return result
def try_down(self):
cells = [[self.cells[size - 1 - j][i] for j in range(size)] for i in range(size)]
result = self.move_left(cells)
result['cells'] = [[result['cells'][j][size - 1 - i] for j in range(size)] for i in range(size)]
return result
def __del__(self):
self.browser.close()
def Print(self, cells):
print
for x in range(size):
for y in range(size):
print '%5d' % cells[x][y],
print
if __name__ == '__main__':
url = 'file://' + os.path.abspath('2048/index.html')
# url = "http://gabrielecirulli.github.io/2048/"
auto2048 = Auto2048(url, Estimator())
while auto2048.AI():
time.sleep(0.2)
time.sleep(10)
源代码中有两个类
主逻辑类 Auto2048
使用 2048的url和估值类(AI逻辑)构造。测试过程中,用 wget 将 "http://gabrielecirulli.github.io/2048/" 的所有页面抓到本地分析(由于不懂js在网页中的工作原理,使用find_elements_by_class_name找当前cells的信息找了好久)
每一次操作调一次AI(),AI() 先获取当前页面的状态保存在 self.cells 中,然后对上下左右四个方向枚举,取估值最大的方向并使用send_keys进行操作。如果能操作则AI()返回True,否则返回False
估值类 Estimator
估值类只要实现 def estimate(self, precells, postcells, action, score): 估值方法即可。其中,precells为操作前状态,postcells为操作后状态,atcion为操作['Left', 'Right', 'Up', 'Down'],score为操作得分。返回值为估值estimation,值越到越好。
虽然我的估值方法运气好的话可以得到2048,但还是很粗糙的。你要有兴趣的话,可以写一个更好的Estimator得到更高的分。