Tuesday, July 23, 2013

Android game automation - part 2

In the previous post I touched upon the fact that simulating hardware input events was very slow and not really suited for fast, repeated actions.

The second approach is based on MonkeyRunner, a free library included with the Android SDK. It is able to talk to the Android device using a Python-like language.

Part 1: http://hackcorellation.blogspot.de/2013/07/android-game-automation-part-1.html




First, we need to set up a batch file that deals with starting the script from time to time which has the added benefit of an extra layer of redundancy in case the ADB bridge fails or the Python script needs to be modified:

:start  
 @echo "Start"  
 @time /t  
 call monkeyrunner.bat %CD%\mr_touchpad.py  
 @time /t  
 timeout /t 250  
 goto start  
 pause  


We need to wake up the device, drag the ring to unlock it (it's more fun that way), kill the previous app instance if it was running and start it again.

 # Kill Application  
 device.shell("am force-stop " + package)  
 time.sleep(1)  
 # wake device  
 device.wake()  
 time.sleep(1)  
 # Runs the activity  
 device.startActivity(component=runComponent)  
 # unlock  
 device.drag((741, 374), (948, 374), duration, steps)  
 # wait for start  
 time.sleep(38) 

Swiping or touching too fast will result in erratic behavior and will cause the application to get noticed in the logs, so we need to fix that by providing some framework methods. The first one simulates a human touch (which is never instant), the and following two methods simulate a slow full-screen drag:

 def devicetouch(x,y,s):  
      device.touch(x, y, MonkeyDevice.DOWN)  
      time.sleep(0.1)  
      device.touch(x, y, MonkeyDevice.UP)  
      time.sleep(0.1)  
   
 def moveHorizontal(offset):  
      device.drag((500, 380), (500-offset, 380), duration/1.5, steps)  
      time.sleep(0.7)  
   
 def moveVertical(offset2):  
      device.drag((500, 155), (500, 155+offset2), duration/1.5, steps)  
      time.sleep(0.7)   


I chose a touch-drag-optimized playfield coverage approach. It will scroll the playfield upwards by one step until the vertical area is covered, scroll sideways right and then continue down.

After each swipe-scroll the current screen is "harvested" as detailed further below.



The implementation for this alternative swipe is:

 for i in range(1, largeXIterations):  
     for j in range(1, largeYIterations):  
         print("Doing tile" + str(i) + "," + str(j))  
         if ((i==1) and (j==1)) or ((i==2) and (j==largeYIterations)):  
           print("Skipping")  
         else:  
           harvest()  
         moveVertical(largeYStep * signY)  
     #end for large y  
     harvest()  
     moveHorizontal(largeXStep)  
     signY *= -1  
 #end for large x   

The largeNIterations variables describe how many screens the playfield spans on. After each complete vertical iteration a horizontal swipe is performed and the vertical direction is reversed.



Harvesting is done by doing a grid-based touch coverage, clicking first on the "rows" and iterating through each "column".

I avoid touching the areas that might contain buttons or some other type of program interaction (buying food, social stuff, options, ...)

To make the program run faster and avoid simple detection random pauses are inserted here and some of the spots are intentionally missed (randomly).

Also the touches are slightly to the left or right, this improves coverage of the screen. Anyway, after running for a few iteration the script is able to cover all the useful areas.

if (random.nextInt(2)==0):  
                 devicetouch(minX + x*smallXStep + (random.nextInt(5)-8), minY + y*smallYStep, "")   


It would be nice to also quit the program gracefully:
 #press back  
 devicetouch(385, 743, "DOWN_AND_UP")  
 time.sleep(1.5)  
 #confirm quit  
 devicetouch(380, 490, "DOWN_AND_UP")   


That's about it, the actual script is under 3 kbytes including comments and took about about 2-3 hours to write, most of which was spent looking through the Python documentation. Yes, this is actually my first Python script.
Considering it saves more than 20 minutes per day I would say it has reached its goal within a few days, leaving more time for procrastination and writing this blog.


Conclusion


There are quite a lot of problems with this approach:
- it's completely blind so it might activate some unwanted actions (including buying stuff with real money)
- sometimes the adb bridge just dies and has to be restarted manually (on device and/or computer)
- it's still slower than I would like it to be (around 2 minutes per iteration)
- it does not perform other time-consuming tasks like buying food or upgrading animals
- to use adb on my other devices I have to stop the script and disconnect

Points 1 and 4 could be fixed but it would require a vision-based system, making it even slower.
On the upside, it only took a couple of hours to develop and performs as expected.

It can be made virtually untraceable by combining different harvesting and swiping strategies, making it indistinguishable from a human. Obviously I don't care about this as it's not earning me any money.

2 comments:

  1. How do I adapt this to Tiny Monsters for Android?

    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete