• 原文地址:Creating An HTML5 Game Bot Using Python
  • 原文作者:vesche
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:lsvih
  • 校对者:faintz, vuuihc

用 Python 做一个 H5 游戏机器人

**摘要:**我给游戏 stabby.io 写了一个机器人(bot),源码请参考: GitHub repo。

几周前,我在一个无聊的夜晚发现了一款游戏:stabby.io。于是乎我的 IO 游戏瘾又犯了(曾经治好过)。在进入游戏后,你会被送进一个小地图中,场景里有许多和你角色长得一样的玩家,你可以杀死你身边的任何一个人。你周围的角色大多数都是电脑玩家,你需要设法弄清哪个才是人类玩家。我沉迷游戏无法自拔,愉快地玩了几个小时。

正当我放纵一夜时,Eric S. Raymond 先生提醒我 boredom and drudgery are evil(无聊和单调都是罪恶)……我还记得 LiveOverflow 的一位老师在视频里冲我叫喊 STOP WASTING YOUR TIME AND LEARN MORE HACKING!(多码代码少睡觉)。因此,我打算把我的无聊与单调转变成为一个有趣的编程项目,开始做一个为我玩 stabby 的 Python 机器人!

在开始前,先介绍一下 stabby 超酷的开发者:soulfoam,他在自己的 Twitch 频道直播编程与游戏开发。我得到了他的授权,允许我创建这个机器人并与大家分享。

我最开始的想法是用 autopy 捕获屏幕,并根据图像分析发送鼠标的移动(作者在此悼念了曾经做过的 Runescape 机器人)。但很快我就放弃这种方式,因为这个游戏有着更直接的交互方式 - WebSockets。由于 stabby 是一款多人实时 HTML5 游戏,因此它使用了 WebSockets 在客户端与服务器之间建立了长连接,双方都能随时发送数据。

所以我们只需要关注客户端与服务器间的 WebSocket 通讯就行了。如果可以理解从服务器接收的消息以及之后发送给服务器的消息,那我们就能直接通过 WebSocket 通讯来玩游戏。现在开始玩 stabby 游戏,并打开 Wireshark 查看流量。

**注意:**我对上面 stabby 的服务器 IP 进行了打码处理,避免它被攻击。为了避免脚本小子滥用这个机器人,我不会在 stabbybot 中提供这个 IP,你需要自行获取。

接着说这美味的 WebSocket 数据包。在这儿看到了第一个表明我们正处于正确道路的标志!我在开始游戏时,将角色名设定为 chain,紧接着在发往服务器的第二个 WebSocket 包的数据部分看到了 03chain。游戏里的其他人就这样知道了我的名字!

通过对抓包进一步的分析,我确定了在建立连接时客户端要发送给服务端的东西。下面是我们需要在 Python 中重新复现的内容:

  • 连接至 stabby 的 WebSocket 服务器
  • 发送当前游戏版本(000.0.4.3)
  • WebSocket Ping/Pong
  • 发送我们的角色名
  • 监听服务器发来的消息

我将使用 websocket-client 库来让 Python 连接 WebSocket 服务器。下面编写前文概述内容的代码:

# main.pyimport websocket# 创建一个 websocket 对象
ws = websocket.WebSocket()# 连接到 stabby.io 服务器
ws.connect('ws://%s:443' % server_ip, origin='http://stabby.io')# 向服务器发送当前游戏版本
ws.send('000.0.4.3')# force a websocket ping/pong
ws.pong('')# 发送用户名
ws.send('03%s' % 'stabbybot')try:while True:# 监听服务器发送的消息print(ws.recv())
except KeyboardInterrupt:passws.close()
复制代码

幸运的是,上面的程序没有让我们失望,收到了服务器消息!

030,day
15xx,60|stabbybot,0|
162,2,0
05+36551,186.7,131.0,walking,left|+58036,23.1,122.8,walking,right|_20986,55.2,71.7,idle,left|_47394,70.9,84.9,walking,right|_58354,10.4,16.2,walking,right|_81344,61.0,27.8,walking,left|+77108,107.5,8.9,walking,left|_96763,118.8,71.7,walking,left|_23992,104.4,24.1,walking,right|+30650,118.4,8.0,idle,left|+11693,186.7,35.5,walking,left|+34643,186.7,118.3,walking,left|+65406,83.9,33.3,idle,right|+24414,186.7,136.3,walking,left|+00863,75.2,35.3,walking,left|_57248,39.0,51.3,walking,right|_98132,165.2,10.0,walking,right|_45741,179.2,5.2,walking,right|+57840,186.7,45.3,walking,left|+70676,186.7,135.7,walking,left|+39478,90.8,63.3,walking,left|_51961,166.7,138.7,idle,right|+85034,148.4,7.7,idle,right|_72926,62.4,23.7,walking,left|_25474,9.6,58.0,idle,left|0,4.0,1.0,idle,left|_52426,61.0,128.4,walking,left|_00194,67.5,96.1,walking,left|+12906,170.7,33.7,walking,right|_67508,87.2,93.3,walking,left|+51085,140.3,34.2,idle,right|_67544,170.1,100.7,idle,right|_77761,158.5,127.6,idle,left|_25113,38.4,111.2,walking,left|
08100,20.5,227.68056,227.68056,0.0,0.0
18t,xx,250m or less
...
复制代码

以上是由服务器传给客户端的消息。我们可以在登录后得到关于游戏中时间的信息:030,day。接着会有一些数据不断地产生: 05+36551,186.7,131.0,walking,left|+58036,23.1,122.8,walking,right|...,这些表达全局状况的数据看上去应该是:玩家 id、坐标、状态、脸对着的方向。现在可以试着调试并对游戏的通信进行逆向工程,以理解客户端、服务器之间发送的是什么了。

例如,当在游戏中杀人时会发生什么?

这次我使用了 Wireshark,特别设置了过滤器,仅抓取流向(ip.dst)服务器的 WebSocket 流量。在杀死某人后,10 与玩家 id 被传给服务器。可能你还不太明白,我解释一下:发送给服务器的一切东西都由两位数字开头,我将其称为事件代码。总共有差不多 20 个不同的事件代码,我还没完全弄清它们分别是做什么的。不过,我可以找到一些比较重要的事件:

EVENTS = {'03': '登录','05': '全局状况','07': '移动','09': '游戏中的时间','10': '杀','13': '被杀','14': '杀人信息','15': '状态','18': '目标'
}
复制代码

创造一个非常简单的机器人

有了这些信息,我们就能构建机器人啦!

.
├── main.py  - 机器人的入口文件。在此文件中会连接 stabby 的服务器,
│              并定义主循环(main loop)。
├── comm.py  - 处理所有消息的收发。
├── state.py - 跟踪游戏的当前状态。
├── brain.py - 决定机器人要做什么事。
└── log.py   - 提供机器人可能需要的日志功能。
复制代码

main.py 中的主循环会做以下几件事:

  • 接收服务器消息。
  • 将服务器消息传给 comm.py 进行处理。
  • 处理过的数据会储存在当前游戏状态(state.py)中。
  • 将当前游戏状态传给 brain.py
  • 执行基于游戏状态做出的决策。

下面让我们看看如何实现一个非常基本的会自己移动到上个玩家被杀的位置的机器人吧。当某人在游戏中被杀害时,其余的每个人都会受到一个类似 14+12906,120.2,64.8,seth 的广播消息。这个消息中,14 是事件代码,后面是用逗号分隔的玩家 id、x 坐标与 y 坐标,最后是杀手的名称。如果我们要走到这个位置区,要发送事件代码 07,后面跟着用逗号分隔的 x 与 y 坐标。

首先,我们创建一个跟踪杀人信息的游戏状态类:

# state.pyclass GameState():"""跟踪 stabbybot 的当前游戏状态。"""def __init__(self):self.game_state = {'kill_info': {'uid': None, 'x': None, 'y': None, 'killer': None},}def kill_info(self, data):uid, x, y, killer = data.split(',')self.game_state['kill_info'] = {'uid': uid, 'x': x, 'y': y, 'killer': killer}
复制代码

接下来,我们创建通信代码用以处理接收到的杀人信息(然后将其传给游戏状态类),以及将移动命令发送出去:

# comm.pydef incoming(gs, raw_data):"""处理收到的游戏数据"""event_code = raw_data[:2]data = raw_data[2:]if event_code == '14':gs.kill_info(data)class Outgoing(object):"""处理要发出的游戏数据。"""def move(self, x, y):x = x.split('.')[0]y = y.split('.')[0]self.ws.send('%s%s,%s' % ('07', x, y))
复制代码

下面为决策部分。程序将通过当前的游戏状态来进行决策,如果有人被杀了,它会将我们的角色移动到那个位置去:

# brain.pyclass GenOne(object):"""第一代 stabbybot。它现在还很蠢(笑"""def __init__(self, outgoing):self.outgoing = outgoingself.kill_info = {'uid': None, 'x': None, 'y': None, 'killer': None}def testA(self, game_state):"""走到上个玩家被杀的地点去。"""if self.kill_info != game_state['kill_info']:self.kill_info = game_state['kill_info']if self.kill_info['killer']:print('New kill by %s! On the way to (%s, %s)!'% (self.kill_info['killer'], self.kill_info['x'], self.kill_info['y']))self.outgoing.move(self.kill_info['x'], self.kill_info['y'])
复制代码

最后更新 main 文件,它将连接服务器,并执行上面概括的主循环:

# main.pyimport websocketimport state
import comm
import brainws = websocket.WebSocket()
ws.connect('ws://%s:443' % server_ip, origin='http://stabby.io')
ws.send('000.0.4.3')
ws.pong('')
ws.send('03%s' % 'stabbybot')# 将类实例化
gs = state.GameState()
outgoing = comm.Outgoing(ws)
bot = brain.GenOne(outgoing)while True:# 接收服务器消息raw_data = ws.recv()# 处理收到的数据comm.incoming(gs, raw_data)# 进行决策bot.testA(gs.game_state)ws.close()
复制代码

机器人运行时,将会如期运行。当有人死亡的时候,机器人会向那个死亡地点攻击。虽然不够刺激,但这是个不错的开头!现在,我们可以发送与接收游戏数据,并在游戏中完成一些特定的任务。

创造一个体面的机器人

接下来为前面创造的简单版机器人进行拓展,添加更多的功能。comm.pystate.py 文件现在充满了各种各样的功能,详情请查看 stabbybot 的 GitHub repo。

现在我们将做一个可以与普通人类玩家竞争的机器人。在 stabby 中最简单的获胜方式就是保持耐心,不断走动,直到看见某人被杀,然后去杀掉那个杀人凶手。

因此,我们需要机器人做下面的事:

  • 随机走动。
  • 检查是否有人被杀(game_state['kill_info'])。
  • 如果有人被杀了,就检查当前全局状况的数据(game_state['perception'])。
  • 确认是否某人是否离杀人地点够近,以确定杀人凶手。
  • 为了分数和荣耀去杀了那个凶手!

打开 brain.py 编写一个 GenTwo 类(意为第二代)。第一步实现最简单的部分,让机器人随机走动。

class GenTwo(object):"""第二代 stabbybot。看着这个小家伙到处走动吧!"""def __init__(self, outgoing):self.outgoing = outgoingself.walk_lock = Falseself.walk_count = 0self.max_step_count = 600def main(self, game_state):self.random_walk(game_state)def is_locked(self):# 检查是否加锁if (self.walk_lock): # 一个锁return Truereturn Falsedef random_walk(self, game_state):# 检查是否加锁if not self.is_locked():# 得到随机的 x、y 坐标rand_x = random.randint(40, 400)rand_y = random.randint(40, 400)# 开始向随机的 x、y 坐标移动self.outgoing.move(str(rand_x), str(rand_y))# 上锁self.walk_lock = True# 检查移动是否完成if self.max_step_count < self.walk_count:# 解锁self.walk_lock = Falseself.walk_count = 0# 增加走路计数器self.walk_count += 1
复制代码

上面做的是一件很重要的事情:创建了一个锁机制。由于机器人要进行许多的操作,我不希望看到机器人变得困惑,在随机走动的途中去杀人。当我们的角色开始随机行走时,会等待 600 个“步骤”(即收到的事件),然后才会再次开始随机行走。600 是通过计算得出的,从地图一角走到另一角的最大步数。

接下来为我们的小狗准备肉。检查最近的杀人事件,然后与当前的全局状况数据进行比较。

import collectionsclass GenTwo(object):def __init__(self, outgoing):self.outgoing = outgoing# 跟踪最近发生的杀人事件self.kill_info = {'uid': None, 'x': None, 'y': None, 'killer': None}def main(self, game_state):# 优先执行self.go_for_kill(game_state)self.random_walk(game_state)def go_for_kill(self, game_state):# 检查是否有新的杀人事件发生if self.kill_info != game_state['kill_info']:self.kill_info = game_state['kill_info']# 杀人事件发生的 x、y 坐标kill_x = float(game_state['kill_info']['x'])kill_y = float(game_state['kill_info']['y'])# 用周围角色的 id、x 坐标、y 坐标创建一个 OrderedDictplayer_coords = collections.OrderedDict()for i in game_state['perception']:player_x = float(i['x'])player_y = float(i['y'])player_uid = i['uid']player_coords[player_uid] = (player_x, player_y)
复制代码

现在在 go_for_kill 中,有一个 kill_xkill_y 坐标,表明了最近一次杀人时间的发生地点。另外还有一个由玩家 ID、玩家 x、y 坐标组成的有序字典。当游戏中有人被杀时,有序字典将会如下所示:OrderedDict([('+56523', (315.8, 197.5)), ('+93735', (497.4, 130.7)), ...])。下面找出离杀人地点最近的玩家就行了。如果有玩家离杀人坐标足够近,机器人将把他们找出来!

所以现在任务很清晰了,我们需要在一组坐标中找到最接近的坐标。这个方法被称为最邻近查找,我们可以用 k-d trees 实现。我使用了 SciPy 这个超帅的 Python 库,用它的 scipy.spatial.KDTree.query 方法实现了这个功能。

from scipy import spatial# ...def go_for_kill(self, game_state):if self.kill_info != game_state['kill_info']:self.kill_info = game_state['kill_info']self.kill_lock = Truekill_x = float(game_state['kill_info']['x'])kill_y = float(game_state['kill_info']['y'])player_coords = collections.OrderedDict()for i in game_state['perception']:player_x = float(i['x'])player_y = float(i['y'])player_uid = i['uid']player_coords[player_uid] = (player_x, player_y)# 找到距击杀坐标最近的玩家tree = spatial.KDTree(list(player_coords.values()))distance, index = tree.query([(kill_x, kill_y)])# 当距离某玩家足够近时进行击杀if distance < 10:kill_uid = list(player_coords.keys())[int(index)]self.outgoing.kill(kill_uid)
复制代码

如果你想看完整的策略,这儿是 stabbybot 中 brain.py 的完整代码.

现在让我们运行机器人,看看它表现如何:

$ python stabbybot/main.py -s <server_ip> -u stabbybot[+] MOVE: (228, 56)
[+] STAT: [('sam5', '2146'), ('jjkiller', '397'), ('QWERTY', '393'), ('N-chan', '240'), ('stabbybot', '0')]
[+] KILL: jjkiller (62.798412, 16.391998)
[+] STAT: [('sam5', '2146'), ('jjkiller', '407'), ('QWERTY', '393'), ('N-chan', '240'), ('stabbybot', '0')]
[+] KILL: N-chan (322.9627, 235.68994)
[+] STAT: [('sam5', '2146'), ('jjkiller', '407'), ('QWERTY', '393'), ('N-chan', '250'), ('stabbybot', '0')]
[+] KILL: jjkiller (79.39742, 11.73037)
[+] STAT: [('sam5', '2146'), ('jjkiller', '417'), ('QWERTY', '393'), ('N-chan', '250'), ('stabbybot', '0')]
[+] KILL: QWERTY (241.24649, 253.66882)
[+] STAT: [('sam5', '2146'), ('QWERTY', '505'), ('jjkiller', '417'), ('stabbybot', '0')]
[+] KILL: sam5 (91.02979, 41.00656)
[+] STAT: [('sam5', '2156'), ('QWERTY', '505'), ('jjkiller', '417'), ('stabbybot', '0')]
[+] MOVE: (287, 236)
[+] KILL: jjkiller (100.214806, 36.986927)
[+] STAT: [('jjkiller', '1006'), ('QWERTY', '505'), ('stabbybot', '0')]... snip (10 minutes later)[+] ASSA: _95181
[+] STAT: [('Mr.Stabb', '778'), ('QWERTY', '687'), ('stabbybot', '565'), ('fire', '408'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]
[+] KILL: stabbybot (159.09984, 218.41016)
[+] ASSA: 0
[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]
[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '306'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]
[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '306'), ('z', '37'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]
[+] MOVE: (245, 287)
[+] KILL: fire (194.04352, 68.50006)
[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '316'), ('z', '37'), ('ff', '0'), ('Guest72571', '0'), ('shako', '0')]
[+] TOD: night
[+] KILL: Guest72571 (212.10252, 150.89288)
[+] STAT: [('Mr.Stabb', '778'), ('stabbybot', '717'), ('QWERTY', '687'), ('fire', '316'), ('z', '37'), ('Guest72571', '10'), ('ff', '0'), ('shako', '0')]
[-] You have been killed.
close status: 12596
复制代码

结果还不错。机器人大约存活了 10 分钟,已经很了不起了。它得了 717 分,在被杀掉的时候排行第二!

以上就是本文的全部内容!如果你想找个有趣的编程项目,可以去做做 HTML5 游戏的机器人,你将获得无穷的乐趣,并能很好地练习网络分析、逆向工程、编程、算法、AI 等各种能力。希望能看到你的创作!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

用 Python 做一个 H5 游戏机器人相关推荐

  1. python做一个小游戏_利用python做个小游戏

    从本期开始,我们将利用几天的时间用python来做个小游戏,当然,在做小游戏之前,我们必须学会一个做小游戏的第三方库--pygame.可能有人会说,python不擅长或者说不适合用来做游戏,的确是这样 ...

  2. python做一个小游戏_12岁的少年教你用Python做小游戏

    你有没有想过电脑游戏是怎样制作出来的?其实它没有你想象的那样复杂!在这个教程里,你要学做一个叫<兔子和獾>的塔防游戏,兔子作为英雄,需要在城堡里抵御獾的进攻. 为了写这个游戏的代码,你将会 ...

  3. 利用python做一个小游戏_如何使用python做一个简单的猜数字的小游戏

    1 首先小编先打开IDLE,如下图: 2 然后这里点击菜单栏的'File',然后点击菜单"New File",如下图: 3 然后我们就在idle中新建了一个python文件,如下图 ...

  4. python做一个枪战游戏

    作为AI语言模型,我无法真正地为你创建一个完整的枪战游戏.但是,我可以提供一些帮助和指导,以便您开始构建自己的游戏. 首先,您需要决定游戏的基本设置,例如游戏场景和角色.你可以使用Python的Pyg ...

  5. python手机版做小游戏代码大全-Python大牛手把手教你做一个小游戏,萌新福利!...

    原标题:Python大牛手把手教你做一个小游戏,萌新福利! 引言 最近python语言大火,除了在科学计算领域python有用武之地之外,在游戏.后台等方面,python也大放异彩,本篇博文将按照正规 ...

  6. python怎么写游戏脚本_用PYTHON做一个简单的游戏脚本(基础,详细)

    引言 这段时间迷上了玩点点点的小游戏,但是某些重复的环节着实无聊,就想着能不能用PYTHON做一个游戏脚本,不过为了熟悉需要做脚本的各个模块,于是打算在4399上找一个比较像的游戏做个脚本练练手,后来 ...

  7. 用Python做一个猜数游戏(入门)

    用Python做一个猜数游戏(入门) 目录: 用Python做一个猜数游戏(入门) 第一阶段: 第二阶段: 前言: 各位大佬大家好,我是夕阳样桑,由于最近时间有点紧,所以先做一个小程序.感谢大家的支持 ...

  8. 用PYTHON做一个简单的游戏脚本(基础,详细)

    引言 这段时间迷上了玩点点点的小游戏,但是某些重复的环节着实无聊,就想着能不能用PYTHON做一个游戏脚本,不过为了熟悉需要做脚本的各个模块,于是打算在4399上找一个比较像的游戏做个脚本练练手,后来 ...

  9. 用python做一个简单的游戏,用python写一个小游戏

    大家好,本文将围绕如何用python做一个简单的小游戏展开说明,python编写的入门简单小游戏是一个很多人都想弄明白的事情,想搞清楚用python做一个简单的游戏需要先了解以下几个事情. 1.Pyt ...

最新文章

  1. AEAI WM v1.6.0 升级说明,开源工作管理系统
  2. Mesos:一个开源的分布式弹性资源管理系统
  3. OpenGL ES之GLSL实现仿抖音“分屏滤镜”效果
  4. linux c 进程策略 优先级,当两个线程拥有相同优先级时,linux c的线程调度策略问题...
  5. 开源joda-time使用demo
  6. GPGPU OpenCL 获取kernel函数编译信息
  7. win7安装SQL2005图文教程
  8. 联通微服务怎么还款_重磅!中国联通推出微信签约代扣新功能
  9. 计算机computer英语划分音节,英语基础知识1.computer有___个音节,按划分音节的规则,应为____.A.1;computer B.2;com...
  10. 简述网页部分知识点:空链接、锚链接、网页图标等
  11. python画最简单的折线图,推荐origin画图软件
  12. PDF文件的旋转和保存
  13. 我问了10个博客专家好友,原来他们都在用这些高效率软件
  14. 语义分割制作自己的数据集
  15. 2023河北工业大学计算机考研信息汇总
  16. 二级java pdf_全国计算机等级考试二级Java语言程序设计.PDF
  17. 2只继电器控制三相电机正反转
  18. 霍尔电流传感器在直流列头柜的应用
  19. 如何使用C语言绘制函数图像
  20. 牛客15108 道路建设

热门文章

  1. 释放内存软件_手机内存不够用?可能是这3个原因造成,教你一招,释放大量内存...
  2. 圆的面积(控制输出小数点位数)
  3. 如何修改git提交名字
  4. python subprocess pipe_Python的subprocess模块总结
  5. JAVA模拟自动售货机
  6. 计算机仿真的实质意义是什么,工程结构的计算机仿真有什么意义
  7. 微软 WSL 2 (Windows Subsystem for Linux) 1.0 正式版发布
  8. 关于 数据结构 与算法的复杂度
  9. python科学计算2 数据组织形式与numpy入门
  10. SpringBoot在线电子商城管理系统