使用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得到更高的分。