• Creating An HTML5 Game Bot Using Python
  • Original author: Vesche
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: lsvih
  • Proofreader: Faintz, vuuihc

Build an H5 game robot in Python

** I wrote a bot for the game Stabby. IO, source code please refer to: GitHub repo

A few weeks ago, I found stabby. IO on a boring night. So MY IO addiction fell again (cured). After entering the game, you are sent to a small map with many players who look like your character, and you can kill anyone around you. Most of the characters around you are computer gamers, and you need to figure out which one is human. I was addicted to the game and played happily for hours.

Just as I was having a night of boredom and drudgery, Mr. Eric S. Raymond reminded me… I still remember a teacher of LiveOverflow in the video Shouting at me to STOP WASTING YOUR TIME AND LEARN MORE HACKING! More code, less sleep. So I’m going to turn my boredom and monotony into a fun programming project and start building a Python robot that plays stabby for me!

Before we get started, stabby’s cool developer: Soulfoam, who streams programming and game development on his Twitch channel. I got permission from him to create this robot and share it with everyone.

My initial idea was to use Autopy to capture the screen and send mouse movements based on image analysis (the author mourned the Runescape robot he had made). But I quickly abandoned this approach because the game had more direct interaction — WebSockets. Since Stabby is a multiplayer real-time HTML5 game, it uses WebSockets to establish a long connection between the client and server, allowing both sides to send data at any time.

So we just need to focus on the WebSocket communication between the client and the server. If we can understand the messages received from the server and then sent to the server, we can play the game directly through WebSocket communication. Now play stabby and open Wireshark to see the traffic.

** NOTE: ** I encoded the above stabby server IP to prevent it from being attacked. To avoid script boys abusing the bot, I won’t provide this IP in stabbybot, you need to get it yourself.

Back to the delicious WebSocket packet. Here’s the first sign that we’re on the right track! I started the game with a character name of chain and then saw 03chain in the data section of the second WebSocket packet sent to the server. That’s how the rest of the game learned my name!

Through further analysis of captured packets, I determined what the client would send to the server when establishing a connection. Here’s what we need to recreate in Python:

  • Connect to stabby’s WebSocket server
  • Send current game version (000.0.4.3)
  • WebSocket Ping/Pong
  • Send our role name
  • Listen for messages from the server

I’ll use the Websocket-client library to let Python connect to the Websocket server. Here’s the code for the previous overview:

# main.py

import websocket

Create a WebSocket object
ws = websocket.WebSocket()

Stabby. IO server
ws.connect('ws://%s:443' % server_ip, origin='http://stabby.io')

Send the current game version to the server
ws.send('000.0.4.3')

# force a websocket ping/pong
ws.pong(' ')

Send user name
ws.send('03%s' % 'stabbybot')

try:
    while True:
        Listen for messages sent by the server
        print(ws.recv())
except KeyboardInterrupt:
    pass

ws.close()
Copy the code

Fortunately, the above program did not disappoint us and received the server message!

Xx, 030, 15 day 60 | stabbybot, | 0 162, 0 5 + 36551186.7, 131.0, and there, left,23.1 | + 58036, 122.8, walking, right | _20986, 55.2, 71.7, idle, left | _47394, 70.9, 84.9, walking, ri GHT | _58354, 10.4, 16.2, walking, right | _81344, 61.0, 27.8, and there, left | + 77108107, 8.9, and there, left | _96763, 118.8, 71.7, and walking , left | _23992, 104.4, 24.1, walking, right | + 30650118 activity, 8.0, idle, left | + 11693186.7, 35.5, and there, left | + 34643186.7, 118.3, walki Ng, left | + 65406,83.9, 33.3, idle, right | + 24414186.7, 136.3, and there, left,75.2 | + 00863, 35.3, and there, left | _57248, 39.0, 51.3, walki Ng, right | _98132, 165.2, 10.0, walking, right | _45741, 179.2, 5.2, walking, right | + 57840186.7, 45.3, and there, left | + 70676186.7, 135. 7, walking, left,90.8 | + 39478, 63.3, and there, left | _51961, 166.7, 138.7, idle, right | + 85034148 activity, 7.7, idle, right | _72926, 62.4, 23.7, There, left | _25474, 9.6, 58.0, idle, left,4.0 | 0, 1.0, idle, left | _52426, 61.0, 128.4, and there, left | _00194, 67.5, 96.1, and there, left | + 12906170.7, 33.7, walking, right | _67508, 87.2, 93.3, and there, left | + 51085140.3, 34.2, idle, right | _67544, 170.1, 100.7, idle, right | _77761, 158.5, 127.6, idle, left | _25113, 38.4, 111.2, and there, left,20.5 | 08100, 227.68056, 227.68056, 0.0, 0.0 t, xx, 250 m or less .Copy the code

This is the message from the server to the client. We can log in and get information about the time in the game: 030,day. Then there will be some data continuously produce: 5 + 36551186.7, 131.0, and there, left,23.1 | + 58036, 122.8, walking, right |… The global stats should look like: player ID, coordinates, status, face orientation. Now you can try debugging and reverse-engineering your game’s communications to understand what is being sent between the client and server.

For example, what happens when you kill someone in the game?

This time I used Wireshark and specifically set the filter to capture only WebSocket traffic going to the (IP.dst) server. After killing someone, 10 and the player ID are passed to the server. In case you don’t quite understand, everything sent to the server starts with a two-digit number, which I call an event code. There are about 20 different event codes, and I don’t fully understand what they do. However, I can find some significant events:

EVENTS = {
    '03': 'login'.'05': 'Global health'.'07': 'mobile'.'09': 'Time in play'.'10': 'kill'.'13': 'killed'.'14': 'Kill information'.'15': 'state'.'18': 'target'
}
Copy the code

Create a very simple robot

With this information, we can build robots!

.├ ── main.py - Entry file for robot. In this file we will connect stabby's server, │ ├ ─ and define the main loop. ├─ comm.py - Handle all messages sent and received. ├─ state.py - Track the current state of the game. ├── brain. Py - Decide what the robot will do. ├ ─ log.py - Provides logging functions your robot might need.Copy the code

The main loop in main.py does several things:

  • Receive server messages.
  • Pass the server message tocomm.pyProcess.
  • The processed data is stored in the current game state (state.py).
  • Passes the current game state tobrain.py.
  • Implement decisions based on game state.

Let’s take a look at how to implement a very basic robot that will move itself to the spot where the last player was killed. When someone is killed in the game, everyone else gets a broadcast message like 14+12906,120.2,64.8, Seth. In this message, 14 is the event code, followed by the comma-separated player ID, x and Y coordinates, and finally the name of the killer. If we want to go to this location, we send the event code 07, followed by the x and y coordinates separated by commas.

First, we create a game state class that tracks homicide information:

# state.py

class GameState(a):
    """ Tracks the current game state of 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}
Copy the code

Next, we create the communication code to process the received kill message (and then pass it to the game state class) and send the move command:

# comm.py

def incoming(gs, raw_data):
    """ Processing received game data """

    event_code = raw_data[:2]
    data = raw_data[2:]

    if event_code == '14':
        gs.kill_info(data)

class Outgoing(object):
    """ Processing game data to be sent." ""

    def move(self, x, y):
        x = x.split('. ') [0]
        y = y.split('. ') [0]
        self.ws.send('%s%s,%s' % ('07', x, y))
Copy the code

Here is the decision part. The program will make decisions based on the current state of the game, and if someone is killed, it will move our character to that position:

# brain.py

class GenOne(object):
    """ The first stabbybot. It's still stupid. (Laughter """

    def __init__(self, outgoing):
        self.outgoing = outgoing
        self.kill_info = {'uid': None.'x': None.'y': None.'killer': None}

    def testA(self, game_state):
        """ Walk to the spot where the last player was killed." ""
        ifself.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'])
Copy the code

Finally update the main file, which connects to the server and executes the main loop outlined above:

# 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')

# instantiate the class
gs = state.GameState()
outgoing = comm.Outgoing(ws)
bot = brain.GenOne(outgoing)

while True:
    Receive server messages
    raw_data = ws.recv()

    # Process the received data
    comm.incoming(gs, raw_data)

    # Make a decision
    bot.testA(gs.game_state)

ws.close()
Copy the code

When the robot runs, it will run as expected. When someone dies, the robot will attack the place of death. It’s not exciting, but it’s a good start! Now we can send and receive game data and perform specific tasks in the game.

Create a decent robot

The next step is to expand the simple version of the robot created earlier and add more functions. The comm.py and state.py files are now full of all kinds of functionality, see Stabbybot’s GitHub repo for details.

Now we’re going to make a robot that can compete with the average human player. The easiest way to win a stabby is to be patient, keep walking until you see someone killed, and then kill the murderer.

So we need robots to do the following:

  • Walk around randomly.
  • Check to see if anyone has been killed (game_state['kill_info']).
  • If someone has been killed, check the current global data (game_state['perception']).
  • To determine if someone was close enough to the killing site to identify the killer.
  • Kill that murderer for points and glory!

Open brain.py and write a GenTwo class (meaning second generation). The first step is to do the easy part and let the robot walk around randomly.

class GenTwo(object):
    """ Second generation Stabbybot. Watch this little fellow move about!" ""

    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):
        # check whether lock is enabled
        if (self.walk_lock): # a lock
            return True
        return False

    def random_walk(self, game_state):
        # check whether lock is enabled
        if not self.is_locked():
            # get random x and y coordinates
            rand_x = random.randint(40.400)
            rand_y = random.randint(40.400)
            # Start moving to random x and y coordinates
            self.outgoing.move(str(rand_x), str(rand_y))
            # locked
            self.walk_lock = True

        Check that the move is complete
        if self.max_step_count < self.walk_count:
            # unlock
            self.walk_lock = False
            self.walk_count = 0

        # Increase the walk counter
        self.walk_count += 1
Copy the code

It does one very important thing: it creates a locking mechanism. Because robots do a lot of things, I don’t want to see robots get confused and kill people on their way around randomly. When our character starts a random walk, it waits for 600 “steps” (events received) before starting the random walk again. 600 is calculated as the maximum number of steps from one corner of the map to the other.

Next, meat for our puppy. Check the most recent killings and compare them to the current global health data.

import collections

class GenTwo(object):

    def __init__(self, outgoing):
        self.outgoing = outgoing

        # Keep track of recent killings
        self.kill_info = {'uid': None.'x': None.'y': None.'killer': None}

    def main(self, game_state):
        # Priority execution
        self.go_for_kill(game_state)
        self.random_walk(game_state)

    def go_for_kill(self, game_state):
        # Check for new killings
        ifself.kill_info ! = game_state['kill_info']:
            self.kill_info = game_state['kill_info']

            # The x and y coordinates of the killings
            kill_x = float(game_state['kill_info'] ['x'])
            kill_y = float(game_state['kill_info'] ['y'])

            Create an OrderedDict with the ids, x coordinates, and y coordinates of the surrounding roles
            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)
Copy the code

Now in go_for_kill, there is a kill_x, kill_y coordinate indicating where the last killing occurred. There is also an ordered dictionary of player ids and player X and y coordinates. Someone was killed when the game, the dictionary will be shown in the following order: OrderedDict ([(‘ + 56523 ‘(315.8, 197.5)), (‘ + 93735’ (497.4, 130.7)),… ). Now find the player closest to the murder site. If a player gets close enough to the kill coordinates, the robot will find them!

So now that the task is clear, we need to find the closest coordinate in a set of coordinates. This approach is known as nearest neighbor lookup and can be implemented using K-D trees. I use the SciPy Python library in this super handsome, with its SciPy. Spatial. KDTree. The query method to realize the function.

from scipy import spatial

    #...

    def go_for_kill(self, game_state):
        ifself.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)

            Find the player closest to the kill coordinates
            tree = spatial.KDTree(list(player_coords.values()))
            distance, index = tree.query([(kill_x, kill_y)])

            Kill when you are close enough to a player
            if distance < 10:
                kill_uid = list(player_coords.keys())[int(index)]
                self.outgoing.kill(kill_uid)
Copy the code

If you want to see the full strategy, here’s the complete code for brain.py in Stabbybot.

Now let’s run the robot and see how it performs:

$ 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
Copy the code

It worked out pretty well. The robot survived for about 10 minutes, which is pretty impressive. He scored 717 points, which was the second highest when he was killed!

That’s all for this article! If you’re looking for a fun programming project, build robots for HTML5 games. You’ll have a lot of fun and practice web analytics, reverse engineering, programming, algorithms, AI, and more. Hope to see your creations!


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.