Tutorial Use custom PlayerSense keys in your bots to simulate different playstyles across your userbase

Discussion in 'Tutorials & Resources' started by Savior, Mar 30, 2017.

  1. Savior

    Savior Java Warlord

    Joined:
    Nov 17, 2014
    Messages:
    4,909
    Likes Received:
    2,752
    Introduction

    The problem: Patterns
    Even simple bots like bankstanding bots can generate detectable patterns very easily, for example your everyday bankstanding bot....
    1. Opens the bank
    2. Withdraws item A
    3. Withdraws item B
    4. Closes the bank
    5. Combines A with B
    6. Does nothing until the operation is complete
    That's a pattern which at a first glance doesn't seem to bad, because every legit player does it that way, right?
    Well that's correct, but one essential thing is missing in this construct: the playstyle of a legit player.

    How much attention are you paying when doing bankstanding tasks? Do you watch movies concurrently? How efficient are you? Do you use your keyboard a lot? Are you always hovering the next thing to click on, or are you just moving your mouse when needed? How much attention are you paying while waiting for something ingame?

    All those questions are questions about your personal playstyle, and if every user would answer them, you would be surprised of how different the set of answers can be from user to user.

    Now watch your bot play for a while and you will notice a certain playstyle the bot comes up with.
    The big problem? Every. Single. User. has that very playstyle when using your bot.

    And yes, you may be using random delay timeouts here and there, but no, since every user has the same intervals of possible random values, they still represent the same playstyle.


    The solution: PlayerSense
    PlayerSense is an API used to simulate different playstyles for every user and every RuneScape account, which ideally leads to a prevention of patterns across all RuneMate users.

    Technically, PlayerSense is a persistent, instanced key->value map, where an instance is an account you have added to RuneMate Spectre. That means you are feeding it a random value for each account the user runs it with, this way you end up with a playstyle in form of a map.

    For example, a colloquial mapping of keys (questions) to values (answers, different for each player) for your PlayerSense:
    • How likely is it for you to play at maximum efficiency? 82%
    • In what order do you usually drop your items? In a top-to-bottom order
    • How frequently are you using hotkeys? 32% of the time
    • How good are your reflexes in terms of delay? About 212 ms

    There already are existing PlayerSense keys you can use, which especially high-level methods of the API make frequent use of (JDocs).

    Now that we have cleared things up, it's time to get practical. In the second chapter of this thread I will show you an example implementation of custom PlayerSense keys you can use in your bots.


    Implementation
    A basic understanding of enums is recommended

    Preparing a set of keys
    We start of with an empty class, which will later contain our keys and a few methods used to manage them.
    Code (Text):
    1. public class CustomPlayerSense {
    2.  
    3. }
    In that class, we now create an enum for the keys, just as in the API.
    Code (Text):
    1. public class CustomPlayerSense {
    2.     public enum Key {
    3.         ACTIVENESS_FACTOR_WHILE_WAITING,
    4.         SPAM_CLICK_COUNT,
    5.         REACION_TIME,
    6.         SPAM_CLICK_TO_HEAL
    7.     }
    8. }
    Now that we have the keys we want to use in the bot later on, it's time to make them PlayerSense conform. When taking a look at the documentation, we can see that methods like put and get require a String value as key, and furthermore we need suppliers for each key to feed the PlayerSense database with.
    Code (Text):
    1. public class CustomPlayerSense {
    2.     public enum Key {
    3.         ACTIVENESS_FACTOR_WHILE_WAITING("prime_activeness_factor", () -> Random.nextDouble(0.2, 0.8)),
    4.         SPAM_CLICK_COUNT("prime_spam_click_count", () -> Random.nextInt(2, 6)),
    5.         REACION_TIME("prime_reaction_time", () -> Random.nextLong(160, 260)),
    6.         SPAM_CLICK_TO_HEAL("prime_spam_healing", () -> Random.nextBoolean());
    7.  
    8.         private final String name;
    9.         private final Supplier supplier;
    10.  
    11.         Key(String name, Supplier supplier) {
    12.             this.name = name;
    13.             this.supplier = supplier;
    14.         }
    15.  
    16.         public String getKey() {
    17.             return name;
    18.         }
    19.     }
    20. }
    As you can see, the names all have a certain prefix, you should do that in order to prevent collisions with other bot authors.

    We have our keys prepared, now it's time to feed them to PlayerSense. In order to do that, we will make a method called initializeKeys() in the class we created.
    To feed the values properly and don't overwrite them everytime, it's important to know that PlayerSense.get(String) will return null if the key has no associated value yet.
    Code (Text):
    1. public class CustomPlayerSense {
    2.     public static void initializeKeys() {
    3.         for (Key key : Key.values()) {
    4.             if (PlayerSense.get(key.name) == null) {
    5.                 PlayerSense.put(key.name, key.supplier.get());
    6.             }
    7.         }
    8.     }
    9.  
    10.     public enum Key {
    11.         ACTIVENESS_FACTOR_WHILE_WAITING("prime_activeness_factor", () -> Random.nextDouble(0.2, 0.8)),
    12.         SPAM_CLICK_COUNT("prime_spam_click_count", () -> Random.nextInt(2, 6)),
    13.         REACION_TIME("prime_reaction_time", () -> Random.nextLong(160, 260)),
    14.         SPAM_CLICK_TO_HEAL("prime_spam_healing", () -> Random.nextBoolean());
    15.  
    16.         private final String name;
    17.         private final Supplier supplier;
    18.  
    19.         Key(String name, Supplier supplier) {
    20.             this.name = name;
    21.             this.supplier = supplier;
    22.         }
    23.  
    24.         public String getKey() {
    25.             return name;
    26.         }
    27.     }
    28. }
    The added method will go over all the keys we have, and if the current key has not been initialized yet, it will do so by putting a random value generated by the key's value supplier into PlayerSense.

    We are essentially done with the CustomPlayerSense class. Accessing the values may be done with calling PlayerSense.getAsDouble(CustomPlayerSense.Key.ACTIVENESS_FACTOR_WHILE_WAITING.getKey()) for example, and as you may notice, that is pretty long and will waste a lot of space in your code.

    To make accessing the values a bit less ugly, we will add convenience methods to the Key enum.
    Code (Text):
    1. public class CustomPlayerSense {
    2.     public static void initializeKeys() {
    3.         for (Key key : Key.values()) {
    4.             if (PlayerSense.get(key.name) == null) {
    5.                 PlayerSense.put(key.name, key.supplier.get());
    6.             }
    7.         }
    8.     }
    9.  
    10.     public enum Key {
    11.         ACTIVENESS_FACTOR_WHILE_WAITING("prime_activeness_factor", () -> Random.nextDouble(0.2, 0.8)),
    12.         SPAM_CLICK_COUNT("prime_spam_click_count", () -> Random.nextInt(2, 6)),
    13.         REACION_TIME("prime_reaction_time", () -> Random.nextLong(160, 260)),
    14.         SPAM_CLICK_TO_HEAL("prime_spam_healing", () -> Random.nextBoolean());
    15.  
    16.         private final String name;
    17.         private final Supplier supplier;
    18.  
    19.         Key(String name, Supplier supplier) {
    20.             this.name = name;
    21.             this.supplier = supplier;
    22.         }
    23.  
    24.         public String getKey() {
    25.             return name;
    26.         }
    27.  
    28.         public Integer getAsInteger() {
    29.             return PlayerSense.getAsInteger(name);
    30.         }
    31.  
    32.         public Double getAsDouble() {
    33.             return PlayerSense.getAsDouble(name);
    34.         }
    35.  
    36.         public Long getAsLong() {
    37.             return PlayerSense.getAsLong(name);
    38.         }
    39.  
    40.         public Boolean getAsBoolean() {
    41.             return PlayerSense.getAsBoolean(name);
    42.         }
    43.     }
    44. }
    Now, accessing a value will be as short as
    CustomPlayerSense.Key.ACTIVENESS_FACTOR_WHILE_WAITING.getAsDouble(). That's still fairly long but as short as it gets.

    That's basically it for the class, but since the PlayerSense database will be different for each account in RuneMate Spectre, we need to call the initializeKeys() method whenever a bot of yours is started.
    Code (Text):
    1. public class YourMainClass extends TreeBot {
    2.     @Override
    3.     public void onStart(String... strings) {
    4.         CustomPlayerSense.initializeKeys();
    5.     }
    6. }
    Calling the method in the overridden onStart method will do the job.


    Using PlayerSense in your code
    Congratulations, you have prepared your bot to be capable of simulating tons of different playstyles, but the main aspect of using PlayerSense is actually utilizing the API in your bot logic.

    Code (Text):
    1. @Override
    2. public void execute() {
    3.     final SpriteItem food = Inventory.newQuery().actions("Eat").names("Shark").results().first();
    4.     if (food != null) {
    5.         if (isHealthDangerouslyLow() && CustomPlayerSense.Key.SPAM_CLICK_TO_HEAL.getAsBoolean()) {
    6.             final int clicks = CustomPlayerSense.Key.SPAM_CLICK_COUNT.getAsInteger();
    7.             for (int i = 0; i < clicks && food.isValid(); i++) {
    8.                 food.click();
    9.             }
    10.         } else {
    11.             food.interact("Eat");
    12.         }
    13.     }
    14. }
    This task will eat sharks in order to heal, although if the health of the player is extremely low (which can, and should, also be depending on PlayerSense), the bot will maybe spam click the food to simulate a very stressful player.



    Conclusion
    PlayerSense should be used wherever you can, the more places you use it the better. Make the ranges of possible random values large enough to allow a change in the bot's behavior.

    Also, you should have the courage to not only use it to deviate some values a little, use it for fairly large things as well, for example changing the entire way of prioritizing monsters to attack, or which transportation system to use.


    Thanks for reading this guide, which turned out larger than I thought. :)
     
    #1 Savior, Mar 30, 2017
    Last edited: Mar 30, 2017
  2. Wet Rag

    Wet Rag easily triggered ✌

    Joined:
    Dec 31, 2015
    Messages:
    4,458
    Likes Received:
    1,646
    first

    edit: i support this
     
  3. Party

    Party Client Developer

    Joined:
    Oct 12, 2015
    Messages:
    3,542
    Likes Received:
    1,572
    Well written and in-depth guide, stickied.

    Worth emphasising the point that each individual RuneScape account added to Spectre has it's own unique PlayerSense values, and these are persistent across all bot sessions on that account. This means that account's "play-style" is consistent and is actually really important in simulating human behaviour.

    PlayerSense is a massively under-utilised part of the API and I recommend all authors familiarise themselves with it.
     
    Effortless and Slex like this.
  4. Yuuki Asuna

    Joined:
    Aug 11, 2016
    Messages:
    789
    Likes Received:
    110
    remember tho, playersense is kinda dumb for using presets. You should ALWAYS use presets, even if all bots used presets it would be fine, as almost every legit player uses them too.
     
  5. Slex

    Joined:
    Jan 23, 2017
    Messages:
    118
    Likes Received:
    21
    you can leave it out whenever you want. Up to the author...
     
  6. Snufalufugus

    Joined:
    Aug 23, 2015
    Messages:
    1,922
    Likes Received:
    750
    Some extra information in this part of the Jdocs would help.

    For example:
    public static final PlayerSense.Key USE_NUMPAD

    That's all the info that's given. I have no idea how to work with that.
     
  7. Leipo1

    Joined:
    Dec 25, 2016
    Messages:
    106
    Likes Received:
    18
    Would like to see a list of released bots that actually use PlayerSense, just so that i can avoid those that don't.
     
  8. Savior

    Savior Java Warlord

    Joined:
    Nov 17, 2014
    Messages:
    4,909
    Likes Received:
    2,752
    I agree with @Snufalufugus, at least document the value range and the data type of the key

    Well for once, pretty much all Prime bots use quite a few playersense keys ;)
     
    Boot likes this.
  9. Party

    Party Client Developer

    Joined:
    Oct 12, 2015
    Messages:
    3,542
    Likes Received:
    1,572
    Prime, Alpha, Nano and (maybe) Maxi bots should use it extensively. I don't know of anyone else who does.
     
    Leipo1 likes this.
  10. dogetrix

    Joined:
    Feb 18, 2017
    Messages:
    178
    Likes Received:
    45
    How does this actually create different playstyles? I notice that PlayerSense's return value is based off of the previous values inputted. If you have a Supplier like
    Code (Text):
    1. () -> Random.nextDouble(0.2, 0.8)
    , won't the value converge on 0.6 for anyone who uses the bot a couple times, making long-term users have the same pattern?
     
  11. Savior

    Savior Java Warlord

    Joined:
    Nov 17, 2014
    Messages:
    4,909
    Likes Received:
    2,752
    The idea of payersense is to prevent patterns across a larger userbase. Not to prevent patterns for an individual user.
     
    Boot likes this.
  12. Aidden

    Aidden The better executive

    Joined:
    Dec 3, 2013
    Messages:
    5,108
    Likes Received:
    967
    You're only calling it once and storing it in PlayerSense for the account that is currently in use. That way each account has its values generated once. It's just so that each account will play a particular way. One account might always close the bank by pressing the close button, while another account might always use the escape key for example. If you look at the initializeKeys method, you can see that the key value pair is only added to PlayerSense if it doesn't already exist.
     
  13. RobinPC

    Joined:
    Jun 9, 2015
    Messages:
    3,062
    Likes Received:
    1,210
    Randomize the value slightly, so that everyone has a different playstyle (which is then slightly randomized)
     
  14. Exia

    Joined:
    Nov 3, 2013
    Messages:
    609
    Likes Received:
    259
    Has PlayersSense been moved to the cloud for each account for custom keys? As I remember it, the the PlayerSense map is empty at every start of the bot. So this is actually not the best way to generate a consistent "PlayStyle" for each account. This will lead to a random playstyle every time you start the bot.

    I got around this with my miner by generating a seed based on a hash of the user's forum username and the name of the account like so:
    Code (Text):
    1. public static void intialize() {
    2.         int seed = 0;
    3.         if(RuneScape.isLoggedIn()){
    4.             seed = (sumBytes(Environment.getForumName()) | sumBytes(Players.getLocal().getName())) * sumBytes(Players.getLocal().getName());
    5.             Random random = new Random(seed);
    6.             PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
    7.             PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
    8.             PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
    9.             PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
    10.          
    11.             playerSenseIntited = true;
    12.         }else{
    13.             seed = sumBytes(Environment.getForumName());
    14.             Random random = new Random(seed);
    15.             PlayerSense.put(Key.ACTION_BAR_SPAM.playerSenseKey, random.nextInt(100));
    16.             PlayerSense.put(Key.BANKER_PREFERENCE.playerSenseKey, random.nextInt(100));
    17.             PlayerSense.put(Key.DOUBLE_CLICK.playerSenseKey, random.nextInt(35) + 10);
    18.             PlayerSense.put(Key.VIEW_PORT_WALKING.playerSenseKey, random.nextInt(15));
    19.         }
    20.     }
    This would lead to a user's playstyle for the bot always being the same between runs while allowing for each user to have a unique playstyle not only based on the user but also which account they are botting on.

    This was my CustomPlayerSEnse class:
    Exia-Mining-All-In-One/CustomPlayerSense.java at master · JohnRThomas/Exia-Mining-All-In-One · GitHub
     
  15. Savior

    Savior Java Warlord

    Joined:
    Nov 17, 2014
    Messages:
    4,909
    Likes Received:
    2,752
    The PlayerSense dataset is consistent.
     
    Boot likes this.
  16. Exia

    Joined:
    Nov 3, 2013
    Messages:
    609
    Likes Received:
    259
    For custom keys too? Sorry, been away for a bit and this was definitely not the case when I wrote mine.
     
  17. Savior

    Savior Java Warlord

    Joined:
    Nov 17, 2014
    Messages:
    4,909
    Likes Received:
    2,752
    Yeah :p
     
    Boot likes this.
  18. Aidden

    Aidden The better executive

    Joined:
    Dec 3, 2013
    Messages:
    5,108
    Likes Received:
    967
    They're not stored in the cloud but they are stored locally. So PlayerSense profiles persist between sessions on a single machine.
     
    Exia likes this.
  19. dogetrix

    Joined:
    Feb 18, 2017
    Messages:
    178
    Likes Received:
    45
    How do I know that the value corresponds with the user's actual playstyle though? Or is that not necessary?
     
  20. Aidden

    Aidden The better executive

    Joined:
    Dec 3, 2013
    Messages:
    5,108
    Likes Received:
    967
    Lol... obviously it doesn't. It's randomly picked. But that account will always play that way when you run a bot
     
    sickness0666 likes this.

Share This Page

Loading...