How to Write a Runescape Autoclicker with Python, Part II

Now that we’ve reviewed the key points in avoiding bot detection (see Part I), we can apply those principles to the development of our iron ore mining auto clicker for the Dwarven Mines. However, we’re still not quite at the coding part. First we’ll need to 1) install the required tools and 2) design the bot that won’t get you caught (you know, unless you write a very public blog post about it).

Installing the required tools:

  1. First, install the latest version of python from . Pay close attention to whether you’re installing the 32 or 64 bit version (the default Python Windows installer is 32 bits, and that’s the version I installed/will reference). If you need the 64 bits version of Python, check the Looking for a specific release? section of the downloads page.
    • If python is already installed, check your version by opening Windows PowerShell and typing python and then enter.
  2. Next, download and install the Microsoft Visual C++ 2015 Redistributable. Be sure to select the version corresponding to your Python installer (32 or 64 bits).
  3. Now to install our packages:
    1. Download the Numpy version corresponding to your Python installation from here. In my case, I’ve used numpy-1.12.1+mkl-cp36-cp36m-win32.whl. Type  pip install numpy-1.12.0+mkl-cp36-cp36m-win32.whl on the command line (PowerShell) to install.
    2. Download the OpenCV version corresponding to your Python installation from here. In my case, I’ve used opencv_python-3.2.0-cp36-cp36m-win32.whl. Type  pip install opencv_python-3.2.0-cp36-cp36m-win32.whl on the command line (PowerShell) to install. This package is dependent on Numpy, and you will get an error if you haven’t installed Numpy first.
    3. Install pyautogui by typing  pip install pyautogui on the command line (PowerShell). This module is the crux of our program, and what we’ll be using to control mouse movement/clicks and to “press” keys.
    4. Optional: Download the pyHook or pynput for “listening” for keyboard or mouse inputs.


Let’s think about design. Let’s start by laying out out what we want our bot to be able to do. We want our bot to click on each available rock, mine the ore, and then either bank or drop everything once we have a full inventory. However, since we want to mimic a human player, we want to do these things less than perfectly (in a variable way). Some simple behaviors to mirror include ensuring the time intervals between activities isn’t always the same, that the same exact spot shouldn’t be clicked every time, and that our bot should be somewhat context aware (like when a scorpion gets in the way of a click, or when you’re lagging). First, let’s write some pseudo code of the main “mining” loop, our “banking” loop, and some code to call them appropriately “forever”:

From here, we can set up helper functions to complete some of the more code-intense tasks, or just ones that we might want to reuse later. We’ll also discover along the way where we need to build in some defenses against bot detection and unexpected in-game events getting in our way. If you want a mental exercise, you can pause here and attempt to write the actual code yourself before continuing. Or alternatively, take a break to think through the possible “events” that aren’t included in our initial design that could mess up our program later.

Now on to the coding! Before we go any further, I want to remind everyone that botting is a bannable and unappealable offense in Runescape, so please NEVER test these techniques on your main account (or even while using the same IP address or computer, if possible). With that, let’s start with the helper function code. First, finding a random acceptable location:

The code above takes advantage of the builtin random module and the pyautogui module we installed earlier. As inputs, it takes the upper left-most clickable area (represented by x_lower and y_lower) and calculates the bottom right-most clickable area (by adding x_range and y_range to their respective _lower values). It then finds a random coordinate within the range (a random x, labeled x; a random y, labeled y) and uses pyautogui’s moveTo method to move the cursor to that location over 0 seconds.

We’ll need to come back to the random_coordinate function later to make improvements, because right now we have the cursor teleporting problem indicative of a bot instead of a human (time is 0 seconds), but let’s put a pin in that. For now, now that we can find acceptable locations to click, we need to actually know where the rocks are:

This is where the code will depend on your screen resolution and game layout, but this worked for my setup. I’ve also added a couple of extra locations to represent locations next to the rocks, so we have locations for our character to move before making clicks (since rock 3 is separate from rocks 1-2). We will adjust function inputs for this location storage structure in a moment.

Going back to our mine_loop and banking_loop, it also seems like it’d be helpful to have a wait_for_trigger function, since it makes up a large part of what our program has to do (waiting for iron, for successfully mining a rock, etc.). But what exactly are we waiting for? How will we know when a rock is ready to mine? As a human, I usually notice this by the image of the rock changing slightly to have the color of the ore visible, but since we don’t know what could get in the way of the rock (blocking the color change), I chose to look for the change in the hover message (see images below). Successfully having mined a rock is represented by the rock being empty (i.e. we already got the ore), so the hover message will say just say “mine rock.” This methodology drives the need for another feature to our program: image matching. We’ll also need to make sure we have some data structure storing those various needed triggers.

The rock_triggers set up areas to search within for our “triggers,” which are the images of the hover messages. Those trigger areas and images are then fed to our wait_for_trigger function, which will wait until the image is found. Whether or not the image if sound is determined by image_match(), which utilizes OpenCV (cv2) for the image processing and numpy to determine a match with a certain degree of certainty (threshold = 0.80). The reason for not searching for an exact match has to do with the color matching of the image.  For an image to match “exactly” every single pixel has to be perfectly identical, and I found at times saving game screenshots (in various attempted image file formats) altered pixel colors very slightly (even in “lossless” types). The 0.80 threshold is the balance I found worked best.

In the last code block you’ll notice I left a comment for “wait.” In my final code, which I will post at the bottom of this post, I’ve chosen to define my own function to take inputs to make my life easier while writing this, but the underlying code is based on the builtin time module’s time.sleep() method, for a random number (in this case a float, which is why I use uniform instead of randint) from a range:   time.sleep(uniform(min, max))  . Don’t forget to add uniform to your from random import line from earlier.

The banking_loop follows a nearly identical process from a coding point of view, so I won’t belabor the points above, but the full code is provided below. What I want to spend the rest of the time talking about are the issues that would pop up with our basic design and how to deal with them. I’ll also discuss more advanced methods of avoiding bot detection that I chose not to implement, but would absolutely be worth it for regular users.

Unexpected events:

  • Scorpions. Scorpions in the Dwarven Mines are annoying, especially to low level players, but for a bot they can completely shut down a loop.  If instead of clicking a rock or the ground next to a rock, you accidenally attack a scorpion, the design of our bot (so far) will never trigger its next event, and your bot will stand until it logs out.  A simple solution is to check for yet another hover event, looking for an image of the “attack” message, and if it’s found, right clicking to move to the spot:
  • Mining gems instead of ore. Luckily our built in check for a full inventory at the beginning of each mine_loop will make this consideration irrelevant most of the time, however, if a gem is the last item added to an inventory it will shut the bot down. This is because “mining” a gem is a side effect of mining, and doesn’t actually cause the rock to become empty. Given that the odds of this happening are 1/7,980 (1/285 chance of mining a gem, 1/28 inventory slots where it matters) I chose to ignore this possibility for the simplicity’s sake.
  • Lag/disconnects. The beauty of an image-trigger-based design is that even if the game lags, causing the screen to freeze, the program will still run properly.  The program will simply wait until it can “see” the changed screen again.  That said, if RS disconnects to the lobby/logs out, another function could be added to automatically log in again. This could be triggered by searching for static images found on the lobby and main login pages, however I chose to leave this as a manual decision for my program.
  • Other players. What happens if other people are mining on the same rocks? Our bot will still do its thing! The same triggers will still get set off, but there may be some waiting if the other person is faster – just like if a human were playing. Imagine you and this other player mining on the rock, and they’re successful first. The hover will still change to reflect the ore being gone, and so our bot will move on to the next rock (just without having been “successful” as we called it).

Advanced topics:

  • Cursor speed. Remember I said we’d come back to this with regard to the “teleportation effect” of having speed set to 0. Since the time parameter in pyautogui reflects an absolute speed in seconds, simply changing the speed to, say, 1 wouldn’t be ideal. That’s because a 1 second move for 1000 pixels might make sense, but for a 50 pixel move would be incredibly slow. In order to address this, I added travel_time() to determine a time based on the distance between the start and end pixels and a more reasonable randomized rate at which to travel.
  • Cursor movement patterns. Fixing cursor speed still leaves us with the fact that our mouse is moving perfectly linearly. Ideally, we would use curves, jitter, and other patterns based on a sampling of real player mouse behavior, but that was beyond the scope of my planned implementation. Implementing these techniques is certainly complicated, but very much worth it, as it is a major component to bot detection (though I can’t speak for how Jagex does it). Implementing Bezier curves would be a great start to improving cursor path, and there are a couple of newer modules out there to help – I encourage you to look into them. If you’re not fixed on python like I was when I began this project, there are also a few already written solutions for human-like cursor movement out there on the internet written in pascal, Java, and C++ (just give it a google).
  • Humans get sick of the grind. As great as randomness is, unlike computer programs people get tired.  That usually means taking breaks and less efficient clicks. It might even mean switching activities for awhile. Ultimately the point of a bot is to relatively efficiently and hastle-freely train a specific skill, so I avoided mimicking this behavior on purpose. Just remember to build in some ineffeciency to “fly below the radar” so to speak.

The final project. Below is what my “final” product ended up looking like. For easier reading/copying, check it our on GitHub at


If you follow the philosophy from part I of this guide, and were able to follow along with the design/code in this second part, you are now well on your way to designing your own bots for anything/everything RS (or anything else)! Like this post? Have questions or comments? Please leave me feedback below!


      1. Any interest in coding a bot together?
        I mean, not a pixel based input one, i thought of cracking up the client and forging packets

        1. If Zax not … I am interested in 🙂
          Currently I am trying to implement some deep learning algorithms for object detection.

  1. Nice Guide, but how would you go about the standing type bots (alching, fletching). It seems these are harder to avoid detection on

  2. You’re totally wrong
    They’re the easiest type of bot to make
    Barelly three lines of code
    Take alch for example
    Just make it click at random intervals at the spell, which automatically switches to the inventory tab
    3 lines = undetectable auto high alch

  3. I was looking at this and I didn’t understand the drop_loop(). It didn’t look like it was clicking anything to drop. Is that just a place holder currently?

  4. Hi there, I know it’s been a few years since this was posted, but I’m having a hard time understanding how the trigger coordinates relate to the location coordinates. For example,’rock_locations = {‘rock1′: (1243, 569, 55, 62)….’ and ‘rock_triggers = {‘rock1iron’: (1243, 569, 350, 200, ‘triggers/mine_iron_ore_rocks.png’)….’. The first 2 values in each case are the same, I assume indicating the position of the rock on screen in x/y coordinates.
    However it is the second pair that is confusing me. In the first case (rock1) this looks like it follows the random_coordinate method however in the second case the values appear to be completely different. Do you remember what these values represent and why you chose them?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.