├── .gitignore
├── LICENSE.txt
├── README.md
├── requirements.txt
└── skill_check
├── __main__.py
├── centeringCurrent.py
├── detection.py
├── filterCreation.py
├── filtering.py
├── mouseNow_Color.py
├── resources
├── Current.png
├── Spacebar.png
├── debug
│ ├── cross.png
│ ├── red.png
│ ├── ring.png
│ └── white.png
├── filtered.png
├── red.png
├── ring.png
└── white.png
└── waitTime.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Editors
2 | .vscode/
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | pyenv.cfg
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 michaeljhenderson1
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Functionality
2 | The following program automates the skill checks for https://www.mistersyms.com/tools/gitgud/. Skill checks are quick time events that require fast processing and precision.
3 |
4 | **Disclaimer**:
5 | Please note that this program will **not** run on the game Dead by Daylight.
6 |
7 | [](https://gyazo.com/e88b33dcf4e6a0bcfd8094c5bcaa0a63)
8 |
9 | A skill check is broken down into several components:
10 | * Red tick: Shows the current position of a skill check. When the space key is pressed, the tick's position relative to the other elements will determine the result.
11 | * Great Skill Check: If a skill check ends in the tiny white box, extra points are rewarded, and the skill check is successful.
12 | * Good Skill Check: If a skill check ends in the hollow bar, the skill check is successful.
13 |
14 | The cost of a single failure is far greater than that of a great skill check; hence, the safest solution was selected. The following video shows the results of the skill check bot.
15 |
16 | [](https://youtu.be/JymZaQRRISM)
17 |
18 | # How to Run
19 | For web browsers, Chrome provides the best results. After [installing requirements.txt](https://stackoverflow.com/questions/7225900/how-can-i-install-packages-using-pip-according-to-the-requirements-txt-file-from), the code can be run off of "\_\_main\_\_.py". The program begins to run after an initial 5-second delay. Please have the appropriate web browser and tab selected. Otherwise, the program will be pressing the space key into the selected application. When ready to **stop the program**, press the q key.
20 |
21 | # How the program works
22 |
23 | The problem of a skill check is greatly simplified by breaking it down to a unit circle.
24 |
25 |
26 |

27 |
28 |
29 | By determining a relevant coordinate, a right triangle can be formed between the center and chosen coordinate.
30 | Given that ,  can be found by solving for .
31 | Using trigonometry, the coordinates can be converted to degrees on a unit circle. The difference, in degrees between the skill check and red tick, is used to determine the wait time before completing a skill check.
32 |
33 | ## Image Detection
34 |
35 | The image is initially filtered such that only pixels in the skill check remain.
36 | As a result, irrelevant pixels are far less likely to interfere with the problem.
37 |
38 |
43 |
44 | The filtering above is resolved by subtarcting the filter from the original image. Afterwards, the image is filtered for relevant pixels based on rgb values.
45 |
46 |
47 |

48 |

49 |
50 |
51 |
52 | Images are filtered separately according to red tick & the skill check portions.
53 | From each of these images, a median coordinate is selected. The previous math is then applied to find the degrees of each element on the unit circle.
54 |
55 | # Image capture & filter setup
56 |
57 | In order to apply a unit circle to the skill check, verification is used to ensure the best coordinates for image capture. Additionally, the filter applied to the image needs to cleanly remove unnecessary portions of the image.
58 |
59 | ### Centering image capture
60 |
61 | Under `detection.py`, the method findSpace takes the image used throughout the program: Current.png.
62 | When `centeringCurrent.py` is run, cross.png is generated under resources/debug. The resulting image is used to determine if Current.png is centered. For additional adjustments, the coordinates used in `detection.py` need to be adjusted under the `findSpace` method.
63 |
64 |
65 |

66 |
67 |
68 | ### Creation of the filter
69 |
70 | `filterCreation.py` offered a straightforward solution to developing a filter. When ran, a new ring.png is generated under resources/debug. The solution involves whiting out any pixels that are outside the exterior radius or inside the internal radius. The logic is applied once more in the reverse to create an additional ring on the inside.
71 |
72 |
73 |

74 |
75 |
76 | ### Filtering by colors
77 |
78 | `mouseNow_Color.py` is used to determine the rgb values of a pixel on the screen. The rgb values filtered, under `filtering.py`, are then adjusted accordingly. Also, `filtering.py` can be run to quickly iterate over the values that are being filtered.
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/requirements.txt
--------------------------------------------------------------------------------
/skill_check/__main__.py:
--------------------------------------------------------------------------------
1 | import pyautogui as au, time, os
2 | from detection import findSpace
3 | from filtering import filterExtraneous
4 | from waitTime import wait
5 |
6 | au.FAILSAFE = True
7 | time.sleep(5)
8 | print('Press \'q\' to quit.')
9 | try:
10 | while True:
11 | startTime = findSpace()
12 | filterExtraneous(os.path.join('resources','Current.png'))
13 | wait(startTime,debug=False)
14 | au.press('space')
15 | time.sleep(.2)
16 | except KeyboardInterrupt:
17 | print('\nDone: main.')
--------------------------------------------------------------------------------
/skill_check/centeringCurrent.py:
--------------------------------------------------------------------------------
1 | import pyautogui as au, cv2, numpy as np, os
2 |
3 | au.PAUSE = 1
4 | au.FAILSAFE = True
5 |
6 | #Editing a red cross to the image to try and improve centering
7 | #These values should NOT be adjusted to resolve centering unless the picture size changes.
8 | #Centering the image captured should be done under detection.py, with the findSpace method ran to capture a new "Current.png".
9 | size = 150 #size of the picture
10 | adj_x = 10 #determines the border (Keeps the cross in the circle)
11 | adj_y = 10 #determines the border (Keeps the cross in the circle)
12 | center = 74 # Center of the image, for size 150x150
13 |
14 | def addCross(raw):
15 | im = np.array(raw)
16 | i = 0
17 | for row in im:
18 | j = 0
19 | for col in row:
20 | j += 1
21 | if((i == center or j == center) and (j > adj_x and j < size - adj_x) and (i > adj_y and i < size - adj_y)):
22 | col[0] = 0
23 | col[1] = 0
24 | col[2] = 255
25 | i += 1
26 |
27 | cv2.imwrite(os.path.join('resources','debug','cross.png'),im)
28 |
29 | #Creates a cross.png from the existing Current.png
30 | if __name__ == '__main__':
31 | im=os.path.join('resources','Current.png')
32 | im=cv2.imread(im)
33 | addCross(im)
--------------------------------------------------------------------------------
/skill_check/detection.py:
--------------------------------------------------------------------------------
1 | import pyautogui as au, cv2, numpy as np, statistics as stat, os, time, math
2 | from python_imagesearch.imagesearch import imagesearch as imFind
3 | import keyboard
4 |
5 | #Constantly searches for the spacebar image, per Spacebar.png
6 | #When the Spacebar is found, a screenshot is taken, and the method ends.
7 | def findSpace():
8 | au.PAUSE = 1
9 | au.FAILSAFE = True
10 |
11 | im = os.path.join('resources','Spacebar.png')
12 | y_center = 67 # used to center the image
13 | x_center = 45 # used to center the image
14 | size = 150 #size of the picture. Should require NO additional changes.
15 |
16 | while True:
17 | if keyboard.is_pressed('q'):
18 | raise KeyboardInterrupt()
19 | coords = imFind(im)
20 | status = coords != [-1,-1]
21 | if(status): #Takes a picture of the screen when the image is found, and ends the program.
22 | im = au.screenshot(os.path.join('resources','Current.png'),(coords[0] - x_center, coords[1] - y_center, size, size))
23 | return time.time()
24 |
25 | #Finds the median coordinates of the non-black pixels in an image.
26 | def findCoords(imagePath,debug=False):
27 | im = cv2.imread(imagePath)
28 | arr = np.array(im)
29 | indices = np.where(arr != [0])
30 |
31 | y_coords = indices[0]
32 | x_coords = indices[1]
33 |
34 | if len(y_coords) == 0:
35 | raise Exception("findCoords: No valid coordinates were found.")
36 |
37 | y_med = stat.median_high(y_coords)
38 | x_med = stat.median_high(x_coords)
39 |
40 |
41 | #Based on the median coordiante's degree, we will either favor the median x or y coordiante.
42 | #y will be favored if the degree is in (45,135) or (225,315), else x.
43 | deg = findDeg((x_med,y_med))
44 | if (deg > 45 and deg < 135) or (deg > 225 and deg < 315):
45 | #favor y
46 | valid_indices = np.where(indices[0] == y_med)
47 | x_med = stat.median_high(indices[1][valid_indices])
48 | else:
49 | #favor x
50 | valid_indices = np.where(indices[1] == x_med)
51 | y_med = stat.median_high(indices[0][valid_indices])
52 |
53 | #Under the debug folder, an image is created with the selected coordinate being changed to green.
54 | if(debug):
55 | print("Median coord for " + imagePath)
56 | print("(" + str(x_med) + "," + str(y_med) + ")")
57 | arr[y_med][x_med] = (0,255,0)
58 | cv2.imwrite(os.path.join('resources','debug',os.path.basename(imagePath)),arr)
59 |
60 | return (x_med,y_med)
61 |
62 | #Returns the value in degrees with respect to the circle centered at: (75,75)
63 | def findDeg(coords):
64 | x_center = 75
65 | y_center = 75
66 |
67 | adj = abs(x_center - coords[0])
68 | opp = abs(y_center - coords[1])
69 |
70 | #Edge case where x is directly at the center
71 | if adj == 0:
72 | return 180 #degrees
73 |
74 | radians = math.atan(opp/adj)
75 | degrees = math.degrees(radians)
76 |
77 | #Determines which section of the unit circle a given value is in.
78 | #Please note that as y increases, it goes down to the bottom of the image.
79 | #As x increases, it goes to the right of the image.
80 | if(coords[0] > x_center):
81 | if(coords[1] > y_center):
82 | degrees += 90
83 | else:
84 | degrees = 90 - degrees
85 | elif(coords[1] > y_center):
86 | degrees = 270 - degrees
87 | else:
88 | degrees = 270 + degrees
89 | return degrees
90 |
91 | if __name__ == '__main__':
92 | findCoords(os.path.join('resources','red.png'),debug=True)
93 | findCoords(os.path.join('resources','white.png'),debug=True)
--------------------------------------------------------------------------------
/skill_check/filterCreation.py:
--------------------------------------------------------------------------------
1 | import math, cv2, os, numpy as np
2 |
3 | #Adjust these values to adjust the filter being created
4 | ###################################
5 | length = 150 #image size. Should NOT be changed.
6 | center = 75 #center of the image. Should NOT be changed.
7 | outer_border = 6 # Determines the size of the outer ring. Typicall forms the non-skill check portion of the image.
8 | inner_border = 2 # Determines the size of the inner ring that's removed. Typically forms the skill check ring.
9 | radius = 65 #Should reach the skill check ring. Should NOT be changed.
10 | ###################################
11 |
12 | outer_min_dist = radius - outer_border
13 | outer_max_dist = radius + outer_border
14 | inner_min_dist = radius - inner_border
15 | inner_max_dist = radius + inner_border
16 | im = np.zeros((length,length,3), np.uint8)
17 |
18 | for x_coord in range(length):
19 | for y_coord in range(length):
20 | a = abs(x_coord - center)
21 | b = abs(y_coord - center)
22 | c = math.sqrt(math.pow(a,2) + math.pow(b,2))
23 | #c is the distance from the center to the current pixel
24 | if( c < outer_min_dist or c > outer_max_dist):
25 | im[x_coord, y_coord] = (255,255,255)
26 | if( c > inner_min_dist and c < inner_max_dist):
27 | im[x_coord, y_coord] = (255,255,255)
28 | cv2.imwrite(os.path.join('resources','debug','ring.png'),im)
--------------------------------------------------------------------------------
/skill_check/filtering.py:
--------------------------------------------------------------------------------
1 | import cv2, numpy as np, os
2 |
3 | #Uses color filter to specify only the pixels within the specific range.
4 | def _filterRed(raw):
5 | im = np.array(raw)
6 | for row in im:
7 | for col in row:
8 | if(col[0] < 45 or col[0] > 60 or col[1] > 120 or col[1] < 45 or col[2] > 240 or col[2] < 200):
9 | col[0] = 0
10 | col[1] = 0
11 | col[2] = 0
12 | cv2.imwrite(os.path.join('resources','red.png'),im)
13 |
14 | #Uses color filter to specify only the pixels within the specific range.
15 | def _filterWhite(raw):
16 | im = np.array(raw)
17 | for row in im:
18 | for col in row:
19 | if(col[0] > 195 or col[0] < 180 or col[1] > 190 or col[1] < 180 or col[2] > 190 or col[2] < 180):
20 | col[0] = 0
21 | col[1] = 0
22 | col[2] = 0
23 | cv2.imwrite(os.path.join('resources','white.png'),im)
24 |
25 | #Blacks out area in interior and exterior as edited in outline.png
26 | def filterExtraneous(imPath):
27 | im = cv2.imread(imPath)
28 | im = cv2.subtract(im,cv2.imread(os.path.join('resources','ring.png')))
29 | cv2.imwrite(os.path.join('resources','filtered.png'),im)
30 | _filterRed(im)
31 | _filterWhite(im)
32 | return im
33 |
34 | if __name__ == '__main__':
35 | filterExtraneous(os.path.join('resources','Current.png'))
--------------------------------------------------------------------------------
/skill_check/mouseNow_Color.py:
--------------------------------------------------------------------------------
1 | import pyautogui as au
2 | au.PAUSE = 1
3 | au.FAILSAFE = True
4 |
5 | print('Press Ctrl-C to quit.')
6 | #TODO: Get and print the mouse coordinates, and RGB color of the pixel
7 | try:
8 | while True:
9 | x,y=au.position()
10 | positionStr = 'X: ' + str(x).rjust(4) + ' Y: ' + str(y).rjust(4)
11 | pixelColor = au.screenshot().getpixel((x,y))
12 | positionStr += ' RGB: (' + str(pixelColor[0]).rjust(3)
13 | positionStr += ', ' + str(pixelColor[1]).rjust(3)
14 | positionStr += ', ' + str(pixelColor[2]).rjust(3) + ')'
15 | print(positionStr,end='')
16 | print('\b' * len(positionStr), end='', flush=True)
17 | except KeyboardInterrupt:
18 | print('\nDone.')
19 |
--------------------------------------------------------------------------------
/skill_check/resources/Current.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/Current.png
--------------------------------------------------------------------------------
/skill_check/resources/Spacebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/Spacebar.png
--------------------------------------------------------------------------------
/skill_check/resources/debug/cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/debug/cross.png
--------------------------------------------------------------------------------
/skill_check/resources/debug/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/debug/red.png
--------------------------------------------------------------------------------
/skill_check/resources/debug/ring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/debug/ring.png
--------------------------------------------------------------------------------
/skill_check/resources/debug/white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/debug/white.png
--------------------------------------------------------------------------------
/skill_check/resources/filtered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/filtered.png
--------------------------------------------------------------------------------
/skill_check/resources/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/red.png
--------------------------------------------------------------------------------
/skill_check/resources/ring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/ring.png
--------------------------------------------------------------------------------
/skill_check/resources/white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeljhenderson1/dbd_skillCheckBot/64e79bc0757ff3c2a7b0049ce350d33155fbdf3f/skill_check/resources/white.png
--------------------------------------------------------------------------------
/skill_check/waitTime.py:
--------------------------------------------------------------------------------
1 | from detection import findCoords, findDeg
2 | import time,os
3 |
4 | #For a given startTime, the program will determine how much longer the program needs to wait before pressing space. The difference is slept through before ending the method.
5 | def wait(startTime,debug=False):
6 | r_coords = findCoords(os.path.join('resources','red.png'))
7 | w_coords = findCoords(os.path.join('resources','white.png'))
8 | r_deg = findDeg(r_coords)
9 | w_deg = findDeg(w_coords)
10 |
11 | if(debug):
12 | print('r_deg:' + str(r_deg))
13 | print('w_deg:' + str(w_deg))
14 |
15 | cycleTime = 1.15
16 |
17 | if(r_deg > w_deg):
18 | print("Error: r_deg > w_deg in wait method")
19 | else:
20 | degrees = w_deg - r_deg
21 | percent = degrees / 360
22 |
23 | delay = time.time() - startTime
24 | waitTime = (cycleTime * percent) - delay
25 | if(waitTime > 0):
26 | time.sleep(waitTime)
27 | else:
28 | print('Negative wait time: ' + str(waitTime))
29 |
30 | if __name__ == '__main__':
31 | wait(time.time(),True)
--------------------------------------------------------------------------------