Создайте игрового робота H5 с помощью Python

сервер Программа перевода самородков игра робот

Создайте игрового робота H5 с помощью Python

**Обзор:** Я даю игруstabby.ioНаписал робота (бота), пожалуйста, обратитесь к исходному коду:GitHub repo.

Несколько недель назад скучной ночью я обнаружил игру:stabby.io. Итак, моя игровая зависимость от IO снова была зафиксирована (ее вылечили). После входа в игру вас отправят на небольшую карту, на которой много игроков, похожих на вашего персонажа в сцене, и вы сможете убить любого вокруг себя. Большинство персонажей вокруг вас — компьютерные игроки, и вам нужно выяснить, кто является игроком-человеком. Я так увлекся игрой, что не мог удержаться и с удовольствием играл часами.

01-scrot

Когда я предаюсь ночи, мистер Эрик С. Рэймонд напоминает мнеboredom and drudgery are evil(скука и однообразие - грехи)... помнюLiveOverflowОдин из моих учителей наорал на меня в видеоSTOP WASTING YOUR TIME AND LEARN MORE HACKING!(Больше кода, меньше сна). Итак, я собираюсь превратить свою скуку и монотонность в забавный проект по программированию и начать делать робота Python, который будет колоть меня!

Прежде чем мы начнем, позвольте мне представить супер крутого разработчика Стабби: Soulfoam, который в своем собственномTwitch-каналЖивое программирование и разработка игр. Я получил его разрешение создать этого бота и поделиться им со всеми.

Моей первой мыслью было использоватьautopyЗахват экрана и отправка движений мыши на основе анализа изображения (автор здесь, чтобы оплакать робота Runescape, который когда-то это делал). Но вскоре я отказался от этого пути, потому что в игре есть более прямой способ взаимодействия —WebSockets. Поскольку stabby — это многопользовательская игра HTML5 в реальном времени, она использует WebSockets для установления постоянного соединения между клиентом и сервером, и обе стороны могут отправлять данные в любое время.

01-websockets

Поэтому нам нужно сосредоточиться только на связи WebSocket между клиентом и сервером. Если понятно с сервераперениматьновости и послеОтправитьсообщение на сервер, тогда мы можем играть в игру напрямую через соединение WebSocket. Начни играть в стабби сейчас и откройWiresharkПроверьте трафик.

01-wireshark

**Примечание.** Я закодировал IP-адрес сервера stabby выше, чтобы предотвратить его атаку. Во избежание злоупотребления этим ботом скриптовыми детишками, я не буду указывать этот IP в stabbybot, вам нужно получить его самостоятельно.

Затем перейдите к этому восхитительному пакету WebSocket. Увидел здесь первый признак того, что мы на правильном пути! Когда я запускаю игру, я устанавливаю имя персонажа наchain, а затем увидел в части данных второй WebSocket-пакет, отправленный на сервер03chain. Вот как все остальные в игре знают мое имя!

Путем дальнейшего анализа захвата пакетов я определил, что клиент отправляет на сервер при установлении соединения. Вот что нам нужно воспроизвести в Python:

  • Подключиться к серверу WebSocket stabby
  • Отправить текущую версию игры (000.0.4.3)
  • WebSocket Ping/Pong
  • пришлите имя нашего персонажа
  • Слушайте сообщения с сервера

я буду использоватьwebsocket-clientБиблиотека, позволяющая Python подключаться к серверу WebSocket. Давайте напишем код для контента, описанного выше:

# main.py

import 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:
    pass

ws.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|..., данные, выражающие глобальную ситуацию, должны выглядеть так: идентификатор игрока, координаты, состояние, направление лица. Теперь пришло время попробовать отладить и перепроектировать коммуникации игры, чтобы понять, что пересылается между клиентом и сервером.

Например, что происходит при убийстве кого-либо в игре?

01-kill

На этот раз я использовал Wireshark, специально настроив фильтр так, чтобы он захватывал только направление потока.(ip.dst)Трафик WebSocket для сервера. после убийства кого-либо,10и идентификатор игрока передается на сервер. Может вы до сих пор не поняли, поясню: все отправляемое на сервер начинается с двузначного числа, которое я называю事件代码. Всего существует около 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— это код события, за которым следует идентификатор игрока, координаты x и y, разделенные запятыми, и, наконец, имя убийцы. Если мы собираемся отправиться в эту локацию, мы отправим код события07, за которым следуют координаты x и y, разделенные запятыми.

Во-первых, мы создаем класс состояния игры, который отслеживает убийства:

# state.py

class 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.py

def 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.py

class GenOne(object):
    """第一代 stabbybot。它现在还很蠢(笑"""

    def __init__(self, outgoing):
        self.outgoing = outgoing
        self.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.py

import websocket

import state
import comm
import brain

ws = 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.pyа такжеstate.pyФайл теперь наполнен различными функциями, подробности см.Репозиторий GitHub для stabbybot.

Теперь мы сделаем бота, который сможет конкурировать с обычными людьми. Самый простой способ победить в stabby — набраться терпения и продолжать идти, пока не увидите, что кого-то убили, а затем пойти и убить убийцу.

Поэтому нам нужно, чтобы робот делал следующее:

  • Ходите хаотично.
  • Проверьте, не погиб ли кто-нибудь (game_state['kill_info']).
  • Если кого-то убили, проверьте текущие данные глобального статуса (game_state['perception']).
  • Убедитесь, что человек находится далеко от места убийства достаточно близко, чтобы опознать убийцу.
  • Убейте этого убийцу за очки и славу!

Открытьbrain.pyНапишиGenTwoКласс (имеется в виду второе поколение). Первый шаг — реализовать самую простую часть, заставив робота ходить случайным образом.

class GenTwo(object):
    """第二代 stabbybot。看着这个小家伙到处走动吧!"""

    def __init__(self, outgoing):
        self.outgoing = outgoing
        self.walk_lock = False
        self.walk_count = 0
        self.max_step_count = 600

    def main(self, game_state):
        self.random_walk(game_state)

    def is_locked(self):
        # 检查是否加锁
        if (self.walk_lock): # 一个锁
            return True
        return False

    def 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 = False
            self.walk_count = 0

        # 增加走路计数器
        self.walk_count += 1

Вышеупомянутое делает одну очень важную вещь: создается блокирующий механизм. Поскольку у робота много дел, я не хочу, чтобы робот запутался и беспорядочно ходил, чтобы убивать людей. Когда наш персонаж начинает случайное блуждание, он ждет 600 «шагов» (т. е. полученных событий), прежде чем снова начать случайное блуждание. 600 рассчитывается как максимальное количество шагов, чтобы пройти из одного угла карты в другой.

Далее подготавливаем мясо для нашего щенка. Изучите недавние убийства и сравните с текущими глобальными данными о состоянии.

import collections

class 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 坐标创建一个 OrderedDict
            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)

сейчас наgo_for_kill, есть одинkill_x,kill_yКоординаты, указывающие на место последнего убийства. Также есть упорядоченный словарь идентификатора игрока, координат игрока 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 = True

            kill_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)

Если вы хотите увидеть полную стратегию,Вот полный код brain.py в stabbybot.

Теперь давайте запустим бота и посмотрим, как он работает:

$ 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, вы получите бесконечное удовольствие и сможете хорошо практиковать сетевой анализ, обратное проектирование, программирование, алгоритмы, искусственный интеллект и другие способности. Надеюсь увидеть ваши творения!


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.