2
\$\begingroup\$

I have written some python code to play this scratch game. But it is to slow to function correctly because each frame if it clicks, it takes a screenshot of almost the same thing like this

 ____
_    _  |     ______
       _|______
        |

Click
Screenshot

 ____
_    _   |    ______
       __|_____
         |

And so it clicks again because it thinks that it is still down low enough to jump, and so it double jumps to its death.

Any ideas on how to improve the speed, to get it working? Here is the code:

#Circle on scratch destroyer
import numpy as np #Numpy
import screenUtils #My custom code that gets the screenshot (Not a problem the screenshots are lightning fast)
from time import sleep, time #Sleep for sleep and time for time checking
from pymouse import PyMouse #Mouse clicking
from pykeyboard import PyKeyboardEvent #Failsafe key is q
from threading import Thread #Threading for keyboard input and the screen shot loop
m = PyMouse() #Init mouse
jumpHeight = 40 #How high the player jumps each click
class safe(PyKeyboardEvent): #I named it safe because it has the failsafe
    def xtra(self):
        self.tog = False #Toggle
    def tap(self, keycode, character, press):
        """
        Subclass this method with your key event handler. It will receive
        the keycode associated with the key event, as well as string name for
        the key if one can be assigned (keyboard mask states will apply). The
        argument 'press' will be True if the key was depressed and False if the
        key was released.
        """
        if self.lookup_char_from_keycode(keycode) == "q" and press: #If the event is a press of the key "q"
            self.tog = not self.tog #Toggle the screenshot loop
        else:
            pass
click = (575, 450) #where on screen to click
e = safe() #Init keyboard event handler
e.xtra()
def checkClick(screen):
    lineY = [] #The Y axis of all the line pixels
    circP = [] #The pixels of the right side of the circle
    if e.tog: #Only execute if the safegaurd is off
        pX = 0 #Pos X
        pY = 0 #Pos Y
        width = len(screen[0]) #Width of the screenshot
        whiteFilter = [] #Init the white filter
        for w in range(width):
            whiteFilter.append(np.array([255,255,255])) #Fill the white filter
        for y in range(len(screen)): #For each Y layer
            if np.array_equal(whiteFilter, screen[y]): #If the layer is white, skip it
                #meh
                pass
            else:
                for x in range(len(screen[y])): #For each pixel on this layer
                    if screen[y][x][0] >= 30 and screen[y][x][0] <= 50 and screen[y][x][1] >= 50 and screen[y][x][1] <= 65 and screen[y][x][2] >= 130 and screen[y][x][2] <= 150:
                        lineY.append(pY) #Found a line pixel
                    if screen[y][x][0] >= 60 and screen[y][x][0] <= 70 and screen[y][x][1] >= 75 and screen[y][x][1] <= 85 and screen[y][x][2] >= 175 and screen[y][x][2] <= 185:
                        circP.append((pX, pY)) #Found a circle pixel
                    pX += 1 #Increment X pos
            pY += 1 #Increment Y pos
            pX = 0 #Reset X Pos
        pix = [] #Init pix array (this stores all circle pixels at X position 35 of the screenshot)
        for pos in circP:
            if pos[0] == 35:
                pix.append(pos)
        final = []
        found = False
        for p in pix: #This loop gets the two inner pixels of the circle at X position 35
            for P in pix:
                if found == False:
                    if abs(p[1] - P[1]) == 87:
                        final.append(p)
                        final.append(P)
                        found = True
        bottom = () #The bottom pixel
        if len(final) == 2: #Double check the length of final
            try: #Handle random exceptions if you turn off safeguard when not on the game
                if final[0][1] > final[1][1]: #Find the bottom pixel
                    bottom = final[1]
                else:
                    bottom = final[0]
                if max(lineY) - bottom[1] >= jumpHeight: #Detect if the program should click
                    print("click")
                    m.click(click[0], click[1])
            except:
                pass

def screenLoop():
    while True:
        if e.tog:
            screen = screenUtils.grab_screen(405,325,468,675) #Screenshot
            screena = np.array(screen)
            start = time()
            checkClick(screena)
            stop = time()
            print("Time: " + str(stop - start))

t = Thread(target=screenLoop)
t.daemon = True
t.start()
e.run()
\$\endgroup\$
2
\$\begingroup\$

You should change :

    found = False
    for p in pix: #This loop gets the two inner pixels of the circle at X position 35
        for P in pix:
            if found == False:
                if abs(p[1] - P[1]) == 87:
                    final.append(p)
                    final.append(P)
                    found = True

by :

    for p in pix: #This loop gets the two inner pixels of the circle at X position 35
        for P in pix:
            if abs(p[1] - P[1]) == 87:
                final.append(p)
                final.append(P)
                break
        else: continue
        break

This breaks the two loops instead of pointlessly running the loop after it's found.


Second thing, you use Threads, they aren't executed in parallel due to GIL, this can be a problem if you're looking for performance, consider multiprocessing if needed.


This code is inefficient, assign screen[y][x] to a variable to avoid lookups, and do you really need to check every pixel of every frame ? This will be difficult to run in real time without melting your CPU, for the game you linked, you probably only need one column of pixel. Also it seems that pixels are either white or not white, so these big conditions are a bit superfluous.

            for x in range(len(screen[y])): #For each pixel on this layer
                if screen[y][x][0] >= 30 and screen[y][x][0] <= 50 and screen[y][x][1] >= 50 and screen[y][x][1] <= 65 and screen[y][x][2] >= 130 and screen[y][x][2] <= 150:
                    lineY.append(pY) #Found a line pixel
                if screen[y][x][0] >= 60 and screen[y][x][0] <= 70 and screen[y][x][1] >= 75 and screen[y][x][1] <= 85 and screen[y][x][2] >= 175 and screen[y][x][2] <= 185:
                    circP.append((pX, pY)) #Found a circle pixel

Your code would be easier to read if it was broken down into multiple functions and if you used good variable names (not single/double letters, snake_case, PEP8 compliant).


Take time to research about for else notation, the GIL and parallel computing in python (to make it simple: Theads run on one Core, Processes run on multiple Cores). screen[y][x] calls multiple functions behind the scene that is why you should assign in this case. PEP8 gives good advices on how to write readable code (It is a question of habit, use a linter, it can help).

\$\endgroup\$
  • 1
    \$\begingroup\$ Wow thanks for this information, also yes I probably only need to check row 35 of my screenshot. \$\endgroup\$ – Menotdan Feb 17 at 22:56
  • \$\begingroup\$ I use VSCode with pylinter installed, so I already use a \$\endgroup\$ – Menotdan Feb 18 at 16:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.