I’ve been playing Inflation RPG for a while now – it’s a fairly casual game, although there are times I take it a little too seriously. Anyway, the game has gem items that reward the number of times you have played through the game, as a bonus to all of your stats for each time you have played through: 3x, 17x, 41x. You can buy the 3x Play Gems, and farm the 17x and 41x gems. So, of course these are theoretically the most powerful items in the game, provided you play the game often enough. Not really practical to play through thousands of times? Probably not for a human…
At work we make extensive use of a program called AHK, or Auto Hot Key. AHK allows us to interact with the computer as if a user were typing input and using the mouse. The scripting language leaves a lot to be desired, but it pretty much allows you to code what you will. AHK also has image/pixel recognition support, along with COM and ODBC. At work, for example, a common application of AHK is for complex data entry, where a program would work off of a database, pulling down information, then interacting with another program, entering data, committing changes, then querying back out the data as a validation step, and finally marking an entry as complete. The most common complication is in achieving optimal speed of input while maintaining 100% accuracy. That is where AHK’s image recognition comes in – it allows us to wait for a confirmation by waiting for an image to appear on the screen.
So, we have this wonderful tool that allows us to interact with the computer, but Android is running on a phone, so to bridge the gap, we need to somehow control the phone from a computer, and also see what the phone is showing on the computer. Enter adbcontrol. Marian Schedenig’s little tool will allow you to capture and display screenshots on the screen, and also allow you to interact with your phone using taps, swipes, and the back and home buttons, among others.
All the pieces seem to be in place. The program should run through a gameplay as fast as possible, which, in this case, will be to start the game, and fight an enemy that is much stronger than I am (so I die quickly), 10 times, to deplete my battle points to 0.
Adbcontrol allowed me to take coordinates of all the buttons and areas I would need to use to interact with Inflation RPG in order to harvest gameplays. Starting a game would be the easy part – simply clicking buttons one after another to begin the game, with an appropriate delay between each click. The sequence of starting a game is to click Game Start, Try Again*, Confirm OK*, select a character, and finally selecting Normal difficulty. Note that the Try Again/Confirm OK dialogs come up when restarting in the middle of a run, so I click on those buttons, even if they’re not there.
I ensure the character is equipped with the weakest armor and weapons prior to starting. I also equip gems that make a random encounter less likely to happen, as the game starts on the 1st level field, where you always win the fight. I must move the character up to the 710 level field, where he’d be guaranteed to lose a fight.
Adbcontrol registers swipes and taps, but no click-holds, so I need to use MouseClickDrag a number of times to move the character up.
Once the character is in the correct field (note the red danger border radiating in from the edges of the screen), I can click on the Encounter button (make sure to equip the Instant Encounter gem), start the fight by clicking the screen after an appropriate delay, then returning to the field by clicking again after an appropriate delay. Yes, there is unfortunately a lot of waiting you have to do in Inflation RPG.
After 10 iterations of this (you start with a total of 30 Battle Points and lose 3 Battle Points for every fight you lose), the scene transitions to the ending screen, which means just some more button clicking with some delays, then you end up at the start screen again.
That’s fine and dandy, but I want to go more in-depth into the difficulties I experienced during the development, and how I made this fast and robust.
The End Game Advertisement
This one perplexed me at the time. At the end of every game, prior to getting back to the start menu, Inflation RPG displayed one full-screen ad, possibly different every time. Not only that, but there were two distinct types of ads, one completely full screen, with an X on the top left, which could also be closed using the back button, and another that was 1/3rd of the screen, centered in the middle of the screen, with a larger X about 1/3rd of the way down on the top left. This smaller ad could not be closed with the back button – clicking the back button at this point would close the game.
How could I know which X I had to press, or whether I could use the back button, or had to click on the large X? Not only that, but the screenshots were JPG, the placement of the X would differ slightly depending on the ad, etc. I could not rely on a direct image comparison. If you examine the code, I wrote some functions that would sample pixels and roughly compare their colors against what I expected. It could find the X if it looked around enough, and was reliable, but it was slow, mainly because of the lag – adbcontrol has a 4+ second lag between screenshots.
It turned out that this was unnecessary. If I click on the expected position of each X button, starting with the larger ad, then the smaller ad, it clears whichever ad had appeared nicely.
Increasing Reliability
Every once in a while the automation would fail for unknown reasons. So I introduced a loop to kill and restart Inflation RPG every hour or so. To kill Inflation RPG, I used adb:
> adb shell am force-stop air.infurerpgkuesuto
I found the package name using
> adb shell ps
Teamviewer Detour
Prior to finding adbcontrol, I tried using Teamviewer. This is fantastic software, but unsatisfactory for my purposes – the lag grew longer the longer I was connected to the phone, and it wouldn’t stay open for more than one or two hours.
Farming Settings
I recommend doing the following when farming:
- Turn off WIFI on your PC to avoid unwanted restarts/updates
- Turn on blocking mode on your phone
- Disable sync or other power-draining services on your phone
- Turn brightness down to 0
- Buy a y-cable to help keep your phone charged – mine will drain down to 0% after roughly 10 hours
Results
I’m able to farm gameplays roughly at a rate of 30 per hour. I have the software installed on an old but terribly reliable netbook, and have stopped after farming 9000 gameplays, equivalent to 153000 points to each stat with a single 17x crystal. 🙂 Having this helped me farm a number of items quickly, but so far a 41x play crystal eludes me. Oh well. I had fun writing the script and improving it to the point it is at now.
Source
Below is the AHK source code for the gameplay miner. It’s specific to a 1366×768 resolution, with adbcontrol occupying half the screen. (Windows 7, drag to right of screen/auto maximize). Getting it to work on another screen size will likely mean changing the vertical click values.
Sorry for the lack of comments, this one was quick & dirty. 🙂
Des
Here’s the script:
#SingleInstance force RestartInflationRPG() { ; Restart Inflation RPG - need to do a few things: ; 1. The Home key will wake up the phone, in case it's asleep ; 2. Additional presses on the Home key will navigate to the ; home screen ; 3. If the home key is pressed too often, then we may get into ; the "multi-home" screen, which we will bypass by pressing ; Escape ; Before doing anything, kill Inflation RPG on the phone: Run, kill_irpg.bat Sleep 1000 MyClick( 1, 1 ) Send {Home} Sleep, 2000 ; In case the phone needs to wake up Send {Home} Sleep, 200 ; Allow the phone to respond Send {Home} Sleep, 200 Send {Escape} ; Run Inflation RPG: MyClick( 52, 603 ) Sleep, 4000 ; In case it has closed, give Inflation RPG time to open } MyClick(X,Y) { EnsureWinActive() Click %X%,%Y% } EnsureWinActive() { global IfWinNotActive, %WinName% { WinActivate, %WinName% } } MyClickS(X,Y) { global MyClick(X,Y) Sleep, %ClickDelay% } MyClickSF(X,Y) { global MyClick(X,Y) Sleep, %FastClickDelay% } ClickBack() { EnsureWinActive() send {Escape} } ClickGameStart() { MyClickSF(268,410) } ClickTryAgain() { MyClickSF(279,387) } ClickConfirmOk() { MyClickSF(215,341) } ClickCharacter() { ; Pick the character least likely to combo: MyClickSF(360,271) } ClickNormal() { MyClickS(219,241) } ClickEncounter() { MyClick(162,111) Sleep, 50 MyClick(162,111) } ClickConfirmRegisterNo() { MyClickSF(211,345) } ClickPostTitle() { MyClickSF(323,684) } MyMouseClickDrag( Button, X1,Y1, X2,Y2, Speed ) { EnsureWinActive() ;MouseMove, X1,Y1,0 MouseClickDrag, %Button%, X1, Y1, X2, Y2, Speed } InitialMoveUp() { global MyMouseClickDrag( Left, 211, 282, 209,135, 1 ) MyMouseClickDrag( Left, 211, 282, 209,135, 5 ) MyMouseClickDrag( Left, 211, 282, 209,135, 5 ) MyMouseClickDrag( Left, 211, 282, 209,135, 5 ) ; MyMouseClickDrag, Left, 211, 282, 209,135, 15 Sleep, 200 Sleep, %ClickDelay% } Fight() { sleep, 2000 X:=210 Y:=425 MyClick(%X%,%Y%) ; MyClick(%X%,%Y%) ; MyClick(%X%,%Y%) sleep, 5500 MyClick(%X%,%Y%) MyClick(%X%,%Y%) Sleep, 1500 } ColorCompare(C1, C2, precision) { global C1B:=C1>>16 C1R:=(C1>>8) & 0xff C1G:=C1 & 0xff C2B:=C2>>16 C2R:=(C2>>8) & 0xff C2G:=C2 & 0xff ; Calculate differences: DB:=abs(C1B-C2B) DR:=abs(C1R-C2R) DG:=abs(C1G-C2G) ;MsgBox %DB%,%DR%,%DG% if( DB > precision or DR > precision or DG > precision ) { ;MsgBox Returning false return false } ;MsgBox Returning true return true } ClickAd1X() { ; Clicks the top-most X MyClickSF(22,44) } ClickAd2X() { ; This one clicks off the alternate ad format, which requires clicking the X to dismiss MyClickSF(45,153) } ClickBackOrX() { ClickAd1X() Sleep, 50 ClickAd2X() return ; This function needs to do some image reccognition to determine if the back ; button should be pressed, or the X button should be pressed Loop 40 ; 40 * 0.5 seconds = 20 seconds of checking { if IsType1Ad() { ClickAd1X() return } if IsType2Ad() { ClickAd2X() return } sleep, 500 ; Sleep for half a second } ; If we don't find anything, assume it's a google ad: ClickBack() return } DBGMouseMove(X,Y) { global ;MsgBox %Debugging%, %X%, %Y%, X, Y If %Debugging% { MouseMove %X%, %Y% Sleep, 500 } } DBGMsgBox(Words) { global If %Debugging% { MsgBox %Words% } } IsType1Ad() { ; Type 1 ads can use the back button to exit, or ; button press on the X, which is at the top left corner ; Possible Y positions: PossibleY1:=40 PossibleY2:=43 PossibleY3:=46 Delta:=10 PossibleX1:=23 PossibleX2:=26 PossibleX3:=29 Loop 3 ; Iterate over PossibleY { PY:=a_index Y:=PossibleY%PY% Loop 1 ; Iterate over PossibleX { PX:=a_index X:=PossibleX%PX%-Delta EnsureWinActive() DBGMouseMove( X, Y ) PixelGetColor, xbox1, %X%, %Y%, ALT DBGMsgBox( xbox1 ) X:=X+Delta DBGMouseMove( X, Y ) PixelGetColor, xbox2, %X%, %Y%, ALT DBGMsgBox( xbox2 ) X:=X+Delta DBGMouseMove( X, Y ) PixelGetColor, xbox3, %X%, %Y%, ALT DBGMsgBox( xbox3 ) if( ColorCompare( xbox1, 0x7e7e7e, 25 ) and ColorCompare( xbox2, 0x000000, 25 ) and ColorCompare( xbox3, 0x7e7e7e, 25 ) ) { DBGMsgBox( "Found X on top left" ) return True } } } DBGMsgBox( "Did not find X on top left" ) return False } IsType2Ad() { ; Type 2 ads are the 'internal' ads that have the large X box more ; towards the middle of the screen. These ads cannot be bypassed ; with the back button, and the X must be pressed PossibleY1:=153 PossibleY2:=156 PossibleY3:=159 PossibleY4:=162 Delta:=8 PossibleX1:=44 PossibleX2:=41 Loop 4 ; Iterate over PossibleY { PY:=a_index Y:=PossibleY%PY% Loop 2 ; Iterate over PossibleX { PX:=a_index X:=PossibleX%PX%-Delta EnsureWinActive() DBGMouseMove( X, Y ) PixelGetColor, xbox1, %X%, %Y%, SLOW DBGMsgBox( xbox1 ) X:=X+Delta DBGMouseMove( X, Y ) PixelGetColor, xbox2, %X%, %Y%, SLOW DBGMsgBox( xbox2 ) X:=X+Delta DBGMouseMove( X, Y ) PixelGetColor, xbox3, %X%, %Y%, SLOW DBGMsgBox( xbox3 ) if( ColorCompare( xbox1, 0x404040, 12 ) and ColorCompare( xbox2, 0xc7c7c7, 12 ) and ColorCompare( xbox3, 0x404040, 12 ) ) { return True } } } ; Otherwise, the X is not found: return False } MainLoop() { global ; Begin: Sleep, 500 ClickGameStart() ClickTryAgain() ClickConfirmOK() ClickCharacter() ClickNormal() Sleep, 1000 InitialMoveUp() ; Do the encounters: loop 10 { ClickEncounter() Fight() } ; Post-battles Sleep, 3500 ClickConfirmRegisterNo() ClickPostTitle() Sleep, 1500 ; 19000 ClickBackOrX() } ;;; Start of program: ; Absolute screen coordinates: CoordMode Mouse, Relative CoordMode Pixel, Relative ;;; Variables: BaseDelta:=748-1231 BaseDelta:=1350-1525 ClickDelay=1000 FastClickDelay=250 TVXImage=TVX.PNG InitialMoveUpTime=2200 WinName=ADB Control Debugging:=False ;True ;Debugging=True ;;; Start: WinActivate %WinName% ;RestartInflationRPG() ;exit ;InitialMoveUp() ;return ;ClickBackOrX() ;exit ;X:=CalcBase(1000) ;MsgBox %X% ;return ;DBGMsgBox( "Testing" ) ;IsType1Ad() ;return loop { ; roughly every 30 runs (1 hour) restart inflation RPG loop 60 { MainLoop() } RestartInflationRPG() } #p::Pause #Escape:: ExitApp Return
Here’s my kill_irpg.bat batch file:
:: Batch file to kill inflation RPG using ADB!!! C:\adt-bundle-windows-x86-20140702\sdk\platform-tools\adb shell am force-stop air.infurerpgkuesuto