Last active
February 17, 2020 15:55
-
-
Save Meshiest/b016ddc073cd1dfec3ca9a008c980f5a to your computer and use it in GitHub Desktop.
reheatedcake's Split or Steal game bot for splitting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| ## cake's splitbot | |
| You **will** get banned for using this bot. It may take a few hours but it will happen. | |
| This will probably need tuning if you actually want to use it. | |
| I wrote this to learn opencv and because I was too lazy to sit idly clicking a few buttons. | |
| Tested on Windows Python 3.8 64bit. I have a 1440p display running the game in 1080p windowed mode | |
| ### Installation | |
| pip install pyautogui imutils numpy | |
| Install Windows Sdk, follow the pywin32 install instructions | |
| Follow online instructions for python opencv | |
| ### What it does | |
| 1. screenshot the game | |
| 2. identify all the rectangle/oval buttons by their hues | |
| 3. determine which page we're on based on button size, count, and placement | |
| 4. click on the right button | |
| """ | |
| import psutil, imutils | |
| import math, random, time | |
| import win32gui, win32con, win32api, win32process | |
| import pyautogui | |
| import cv2, numpy | |
| from functools import reduce | |
| # Get window from a pid | |
| def getHWnds(pid): | |
| def callback(hwnd, hwnds): | |
| if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): | |
| _, found_pid = win32process.GetWindowThreadProcessId(hwnd) | |
| if found_pid == pid: | |
| hwnds.append(hwnd) | |
| return True | |
| hwnds = [] | |
| win32gui.EnumWindows(callback, hwnds) | |
| return hwnds | |
| # Find the split or steal process | |
| def getProcess(): | |
| # Iterate over all running process | |
| for proc in psutil.process_iter(): | |
| try: | |
| # Return process that is split or steal exe | |
| if proc.name() == 'Split Or Steal.exe': | |
| return proc | |
| except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): | |
| pass | |
| # image saving for debug purposes | |
| def saveImg(img): | |
| file = 'screenshot {}.bmp'.format(int(time.time())) | |
| cv2.imwrite(file, img) | |
| # cropped screenshot of a window | |
| def screenshot(hwnd): | |
| x, y, x1, y1 = win32gui.GetClientRect(hwnd) | |
| x, y = win32gui.ClientToScreen(hwnd, (x, y)) | |
| x1, y1 = win32gui.ClientToScreen(hwnd, (x1 - x, y1 - y)) | |
| im = pyautogui.screenshot(region=(x, y, x1, y1)) | |
| return cv2.cvtColor(numpy.array(im), cv2.COLOR_RGB2BGR) | |
| # left clicks | |
| def leftClick(): | |
| win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0) | |
| time.sleep(.1) | |
| win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0) | |
| print('Click.') | |
| # set relative mouse position | |
| def setMouse(cord): | |
| x, y, x1, y1 = win32gui.GetClientRect(hwnd) | |
| x, y = win32gui.ClientToScreen(hwnd, (x, y)) | |
| win32api.SetCursorPos((x + cord[0], y + cord[1])) | |
| # get relative mouse position | |
| def getMouse(): | |
| mx, my = win32api.GetCursorPos() | |
| x, y, x1, y1 = win32gui.GetClientRect(hwnd) | |
| x, y = win32gui.ClientToScreen(hwnd, (x, y)) | |
| return (mx - x, my - y) | |
| # distance between two points | |
| def dist(a, b): | |
| return math.hypot(b[1]-a[1], b[0]-a[0]) | |
| # move a point along a vector | |
| def translate(a, b, len, curve=0): | |
| theta = math.atan2(b[1]-a[1], b[0]-a[0]) | |
| theta += curve | |
| return ( | |
| int(a[0] + math.cos(theta) * len), | |
| int(a[1] + math.sin(theta) * len), | |
| ) | |
| # smoothly move the mouse | |
| def moveMouse(coord): | |
| # throw in a nice little curve | |
| curve = random.random() * 0.3 + 0.2 | |
| if random.random() < 0.5: curve = -curve | |
| # move the mouse towards the button until it is within 50px | |
| while dist(coord, getMouse()) > 50: | |
| pos = getMouse() | |
| setMouse(translate(pos, coord, 20, curve)) | |
| time.sleep(.005) | |
| setMouse(coord) | |
| # opencv finds the buttons based on color masks | |
| def findButtons(img, debug=False, save=False): | |
| # mask based on button colors | |
| hsv_img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) | |
| all_mask = cv2.inRange(hsv_img, (0, 110, 110), (360, 250, 250)) | |
| blue_mask = cv2.inRange(hsv_img, (10, 110, 110), (25, 250, 250)) | |
| green_mask = cv2.inRange(hsv_img, (50, 120, 120), (80, 220, 220)) | |
| orange_mask = cv2.inRange(hsv_img, (100, 155, 155), (115, 210, 210)) | |
| mask = cv2.bitwise_or(green_mask, blue_mask, mask=None) | |
| mask = cv2.bitwise_or(mask, orange_mask, mask=None) | |
| # join the mask | |
| img = cv2.bitwise_and(img, img, mask=mask) | |
| # extract shapes | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| blurred = cv2.GaussianBlur(gray, (5, 5), 0) | |
| thresh = cv2.threshold(blurred, 60, 255, cv2.THRESH_BINARY)[1] | |
| cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, | |
| cv2.CHAIN_APPROX_SIMPLE) | |
| cnts = imutils.grab_contours(cnts) | |
| buttons = [] | |
| if save: | |
| saveImg(img) | |
| for c in cnts: | |
| peri = cv2.arcLength(c, True) | |
| approx = cv2.approxPolyDP(c, 0.04 * peri, True) | |
| (x, y, w, h) = cv2.boundingRect(approx) | |
| # fit this button to a line | |
| [vx,vy] = cv2.fitLine(c, cv2.DIST_L2,0,0.01,0.01)[:2] | |
| # determine if this is a rectangle and it is sufficiently large | |
| if w > 130 and h > 50 and vx > 0.98: | |
| M = cv2.moments(c) | |
| cX = int(M['m10'] / M['m00']) | |
| cY = int(M['m01'] / M['m00']) | |
| buttons.append(((cX, cY), w/h, len(approx), len(c))) | |
| cv2.drawContours(img, [c], -1, (255, 255, 0), 2) | |
| cv2.rectangle(img, (x, y), (x + w,y + h),(0,255,0),2) | |
| cv2.putText(img, '{}, {}, {}'.format(len(approx), len(c), int(w/h*10)/10),(x + 10,y+h//2),1,1,(255,255,255)) | |
| if debug: | |
| print(buttons) | |
| cv2.imshow('img', img) | |
| cv2.waitKey(0) | |
| return buttons | |
| # deterine if the ratios of button sizes matches | |
| def ratioMatch(buttons, ratios): | |
| for r in ratios: | |
| if len(buttons) == 0: | |
| return False | |
| matches = [b for b in buttons if b[1] > r[0]-0.1 and b[1] < r[0]+0.1] | |
| if len(matches) != r[1]: | |
| return False | |
| return True | |
| def classify(buttons): | |
| # return menu button from taking too long | |
| if len(buttons) == 1 and ratioMatch(buttons, [(2.2, 1)]): | |
| return ('return', buttons[0][0], 1) | |
| # return menu button from winning/losing | |
| if len(buttons) == 1 and ratioMatch(buttons, [(9.4, 1)]): | |
| return ('return', buttons[0][0], 1) | |
| # two menus have two button menus | |
| if len(buttons) == 2: | |
| vdist = reduce(lambda a, b: abs(a[0][1] - b[0][1]), buttons) | |
| hdist = reduce(lambda a, b: abs(a[0][0] - b[0][0]), buttons) | |
| if vdist < hdist and ratioMatch(buttons, [(3.5, 2)]): | |
| # select left most accept button | |
| return ('accept', sorted(buttons, key = lambda p: p[0][0])[0][0], 5) | |
| if vdist < hdist and ratioMatch(buttons, [(4.1, 2)]): | |
| # select left most accept button | |
| return ('complete', sorted(buttons, key = lambda p: p[0][0])[0][0], 1) | |
| # select split or steal | |
| if len(buttons) == 3 and ratioMatch(buttons, [(3.4, 2), (2.9, 1)]): | |
| # select top button (split) | |
| # return ('select', sorted(buttons, key = lambda p: p[0][1])[0][0], 1) | |
| return ('select', (1703, 754), 1) | |
| # lock | |
| if len(buttons) == 4: | |
| # select lock button | |
| return ('lock', sorted(buttons, key = lambda p: -p[0][1])[1][0], 2) | |
| # main menu, 4 buttons on bottom | |
| if len(buttons) >= 7 and len(list(filter(lambda b: b[0][1] > 500, buttons))) == 4: | |
| # select second to last button vertically | |
| selected = sorted(buttons, key = lambda p: -p[0][1])[1] | |
| # is generally a rectangle | |
| if selected[2] == 4 and selected[3] < 30: | |
| return ('join', selected[0], 5) | |
| return None | |
| # tester function | |
| def test(file, expected, debug=False): | |
| shot = cv2.imread(file) | |
| buttons = findButtons(shot, debug=debug) | |
| choice = classify(buttons) | |
| if debug: print(choice) | |
| print('test {} = {} ({})'.format( | |
| file, | |
| choice and choice[0] == expected or | |
| not choice and not expected, | |
| choice and choice[0] | |
| )) | |
| # get the split or steal process and window | |
| proc = getProcess() | |
| if not proc: | |
| print('Failed to find process') | |
| exit(1) | |
| print('Process ID: ', proc.pid) | |
| hwnds = getHWnds(proc.pid) | |
| if len(hwnds) != 1: | |
| print('Failed to find window') | |
| exit(1) | |
| print('hWnd: ', hwnds) | |
| hwnd = hwnds[0] | |
| # if live is false, this will run the tests | |
| live = True | |
| # if looping is false, this will only run once | |
| looping = True | |
| if live: | |
| win32gui.SetForegroundWindow(hwnd) | |
| time.sleep(.1) | |
| while True: | |
| shot = screenshot(hwnd) | |
| buttons = findButtons(shot, save=False) | |
| choice = classify(buttons) | |
| if choice: | |
| (name, pos, duration) = choice | |
| print(name, 'sleeping', duration) | |
| moveMouse(pos) | |
| leftClick() | |
| time.sleep(duration) | |
| else: | |
| time.sleep(2) | |
| if not looping: | |
| break | |
| else: | |
| # I wrote some tests to make sure it clicked on the right thing | |
| # you might be able to do it too | |
| # set debug=True if you want to see what the computer sees | |
| test('t1join.bmp', 'join') | |
| test('accept.bmp', 'accept') | |
| test('send.bmp', None) | |
| test('debug1.bmp', 'lock') | |
| # test('select.bmp', 'select', debug=True) | |
| test('select2.bmp', 'select') | |
| test('lock.bmp', 'lock') | |
| test('done.bmp', 'return') | |
| test('complete.bmp', 'complete') | |
| # test('leave.bmp', None, debug=True) | |
| # test('dog.bmp', 'lock') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment