Skip to content

Commit c913b8a

Browse files
Initial Commit
1 parent 7ec7384 commit c913b8a

File tree

2 files changed

+397
-2
lines changed

2 files changed

+397
-2
lines changed

Game_Automation.py

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pyautogui
4+
import pytesseract
5+
from PIL import Image, ImageGrab, ImageOps, ImageChops
6+
import time
7+
import random
8+
import re
9+
import imagehash # for checking if images look different
10+
import math
11+
import traceback
12+
import os
13+
14+
# --- user needs to set these stuff ---
15+
# path to tesseract exe, pls set this
16+
try:
17+
pytesseract.get_tesseract_version()
18+
print("tesseract found yay")
19+
except pytesseract.TesseractNotFoundError:
20+
tesseract_path = r'' # <-- pls put ur full path here!!
21+
if not tesseract_path: print("\noops no tesseract path..."); exit()
22+
try:
23+
pytesseract.pytesseract.tesseract_cmd = tesseract_path
24+
pytesseract.get_tesseract_version()
25+
print(f"manual tesseract set to: {tesseract_path}")
26+
except Exception as e: print(f"\nerror with tesseract path prob: {tesseract_path}"); exit()
27+
except Exception as e: print(f"something broke checking tesseract: {e}"); exit()
28+
29+
# screen areas to watch (left, top, width, height)
30+
QUESTION_REGION = (880, 260, 300, 100) # where question shows up
31+
OPTIONS_REGIONS = [ # areas for answer choices
32+
(900, 370, 200, 50),
33+
(900, 440, 200, 50),
34+
(900, 520, 200, 50),
35+
(900, 600, 200, 50),
36+
]
37+
BACK_TO_GAME_REGION = (900, 700, 200, 50) # popup close button area
38+
39+
# behavior settings
40+
MIN_DELAY_BEFORE_ACTION = 0.8; MAX_DELAY_BEFORE_ACTION = 2.5 # random wait before doing stuff
41+
MIN_MOUSE_MOVE_DURATION = 0.2; MAX_MOUSE_MOVE_DURATION = 0.8 # how fast mouse moves
42+
ERROR_RATE = 0 # chance to click wrong answer lol
43+
MAX_ITERATIONS = 100000 # how many questions to try
44+
RETRY_LIMIT = 5 # how many times to retry if stuck
45+
ANIMATION_DELAY = 1.0 # wait for screen to update
46+
47+
# ocr settings
48+
OCR_CONFIG = '--psm 6 --oem 1 -c tessedit_char_whitelist=0123456789xX*' # for questions
49+
OCR_CONFIG_OPTIONS = '--psm 7 --oem 1 -c tessedit_char_whitelist=-0123456789.' # for answers
50+
OCR_CONFIG_POPUP = '--psm 7 --oem 1' # for popups
51+
POPUP_KEYWORDS = ["back", "game", "continue", "congrats"] # words to look for in popups
52+
53+
# debug stuff
54+
SAVE_DEBUG_IMAGES = False # set true to save processed images
55+
DEBUG_IMAGE_DIR = "ocr_debug_images" # where to dump images
56+
PREPROCESSING_THRESHOLD = 180 # image processing level
57+
58+
# some globals to track stuff
59+
last_click_target_region = None
60+
last_question_raw_text = None
61+
62+
def capture_screen_region(region, iteration_num=-1, region_name="unknown", save_debug=SAVE_DEBUG_IMAGES):
63+
"""grabs screen area and processes it for ocr"""
64+
try:
65+
left, top, width, height = region
66+
if width <= 0 or height <= 0:
67+
print(f"weird region size for {region_name}, skipping")
68+
return None
69+
# take screenshot and make it bw
70+
screenshot = pyautogui.screenshot(region=region)
71+
img_processed = ImageOps.grayscale(screenshot)
72+
img_processed = img_processed.point(lambda p: 0 if p < PREPROCESSING_THRESHOLD else 255)
73+
img_processed = img_processed.convert('1')
74+
75+
# save debug img if needed
76+
if save_debug and iteration_num != -1:
77+
if not os.path.exists(DEBUG_IMAGE_DIR):
78+
try: os.makedirs(DEBUG_IMAGE_DIR)
79+
except OSError as e: print(f"oops cant make debug dir: {e}")
80+
try:
81+
filename = os.path.join(DEBUG_IMAGE_DIR, f"iter_{iteration_num}_{region_name}.png")
82+
img_processed.save(filename)
83+
except Exception as save_err:
84+
print(f"couldnt save debug img: {save_err}")
85+
86+
return img_processed
87+
except Exception as e:
88+
print(f"failed to capture {region_name}: {e}")
89+
return None
90+
91+
def ocr_image(image, config=OCR_CONFIG):
92+
"""reads text from image using tesseract"""
93+
if image is None: return ""
94+
try:
95+
return pytesseract.image_to_string(image, config=config).strip()
96+
except Exception as e:
97+
print(f"ocr error lol: {e}")
98+
return ""
99+
100+
def clean_text_multiply(text):
101+
"""tidies up multiplication questions"""
102+
if not text: return None
103+
text = text.lower().replace(' ','').replace('x', '*')
104+
cleaned = re.sub(r'[^\d\*]', '', text)
105+
if not re.fullmatch(r'\d+\*\d+', cleaned):
106+
return None
107+
return cleaned
108+
109+
def parse_and_solve_multiply(question_text_raw):
110+
"""solves simple multiplication questions"""
111+
cleaned = clean_text_multiply(question_text_raw)
112+
if not cleaned: return None
113+
114+
match = re.match(r'^(\d+)\*(\d+)$', cleaned)
115+
if not match: return None
116+
117+
try:
118+
num1, num2 = map(int, match.groups())
119+
return num1 * num2
120+
except:
121+
return None
122+
123+
def clean_text_option(text):
124+
"""cleans answer options text"""
125+
if not text: return None
126+
cleaned = re.sub(r'[^\-\d\.]', '', text)
127+
if not cleaned or cleaned in ['.', '-']: return None
128+
if cleaned.count('.') > 1:
129+
cleaned = cleaned[:cleaned.find('.')+1] + cleaned[cleaned.find('.')+1:].replace('.', '')
130+
if not re.fullmatch(r'-?\d+(\.\d+)?', cleaned):
131+
return None
132+
return cleaned
133+
134+
def get_random_point_in_region(region):
135+
"""picks random spot in given area"""
136+
left, top, width, height = region
137+
return (
138+
random.randint(left, left + width - 1),
139+
random.randint(top, top + height - 1)
140+
)
141+
142+
def human_like_move_and_click(target_region, move_duration):
143+
"""moves mouse and clicks like a human-ish"""
144+
global last_click_target_region
145+
try:
146+
x, y = get_random_point_in_region(target_region)
147+
pyautogui.moveTo(x, y, duration=move_duration,
148+
tween=random.choice([pyautogui.easeInQuad, pyautogui.easeOutQuad]))
149+
time.sleep(random.uniform(0.05, 0.15))
150+
pyautogui.click()
151+
last_click_target_region = target_region
152+
except Exception as e:
153+
print(f"whoops click error: {e}")
154+
155+
def compare_images(img1, img2):
156+
"""checks if two images are different using hashing"""
157+
if img1 is None or img2 is None: return 999
158+
try:
159+
return imagehash.average_hash(img1) - imagehash.average_hash(img2)
160+
except Exception as e:
161+
print(f"cant compare images: {e}")
162+
return 999
163+
164+
def main():
165+
global last_click_target_region, last_question_raw_text
166+
print("starting...")
167+
print("make sure game window is visible!")
168+
for i in range(3, 0, -1):
169+
print(f"starting in {i}...")
170+
time.sleep(1)
171+
172+
last_question_image = None
173+
retries = 0
174+
iterations = 0
175+
176+
try:
177+
while iterations < MAX_ITERATIONS:
178+
current_iter = iterations + 1
179+
print(f"\n--- loop #{current_iter} ---")
180+
181+
if iterations > 0:
182+
time.sleep(ANIMATION_DELAY)
183+
184+
# grab question area
185+
question_img = capture_screen_region(QUESTION_REGION, current_iter, "q")
186+
if not question_img:
187+
print("no question img, waiting...")
188+
time.sleep(3)
189+
continue
190+
191+
# read question text
192+
question_text = ocr_image(question_img, OCR_CONFIG)
193+
print(f"ocr got: '{question_text}'")
194+
195+
# check if screen changed
196+
img_diff = compare_images(last_question_image, question_img)
197+
text_diff = question_text != last_question_raw_text if last_question_raw_text else True
198+
changed = img_diff >= 5 or text_diff
199+
200+
if not changed and last_question_image:
201+
retries += 1
202+
if retries > RETRY_LIMIT:
203+
print("prob stuck, checking for popup...")
204+
for _ in range(10):
205+
popup_img = capture_screen_region(BACK_TO_GAME_REGION, -1, "popup", False)
206+
popup_text = ocr_image(popup_img, OCR_CONFIG_POPUP).lower()
207+
if any(kw in popup_text for kw in POPUP_KEYWORDS):
208+
print("found popup, clicking...")
209+
human_like_move_and_click(BACK_TO_GAME_REGION, 0.3)
210+
time.sleep(1.5)
211+
break
212+
time.sleep(0.5)
213+
else:
214+
print("no popup found, exiting")
215+
break
216+
retries = 0
217+
continue
218+
else:
219+
print(f"screen same, retry #{retries}")
220+
if last_click_target_region:
221+
human_like_move_and_click(last_click_target_region, 0.3)
222+
time.sleep(1)
223+
continue
224+
else:
225+
retries = 0
226+
last_question_image = question_img
227+
last_question_raw_text = question_text
228+
229+
# solve question
230+
answer = parse_and_solve_multiply(question_text)
231+
if not answer:
232+
print("cant solve, clicking first option")
233+
if OPTIONS_REGIONS:
234+
human_like_move_and_click(OPTIONS_REGIONS[0], 0.5)
235+
iterations += 1
236+
time.sleep(2)
237+
continue
238+
239+
# read answer options
240+
options = []
241+
print("checking options...")
242+
for idx, region in enumerate(OPTIONS_REGIONS):
243+
opt_img = capture_screen_region(region, current_iter, f"opt{idx+1}")
244+
opt_text = clean_text_option(ocr_image(opt_img, OCR_CONFIG_OPTIONS))
245+
print(f"opt {idx+1}: {opt_text}")
246+
if opt_text: options.append({"text": opt_text, "region": region})
247+
248+
# find matching answer
249+
target = None
250+
try:
251+
target_num = float(answer)
252+
closest = None
253+
min_diff = float('inf')
254+
for opt in options:
255+
try:
256+
opt_num = float(opt['text'])
257+
diff = abs(opt_num - target_num)
258+
if diff < 0.001:
259+
target = opt['region']
260+
break
261+
if diff < min_diff:
262+
closest = opt['region']
263+
min_diff = diff
264+
except: pass
265+
if not target and closest and min_diff < 0.1:
266+
target = closest
267+
except: pass
268+
269+
# decide where to click
270+
if not target:
271+
print("no good option, clicking first")
272+
target = OPTIONS_REGIONS[0]
273+
elif random.random() < ERROR_RATE:
274+
print("intentional wrong click!")
275+
wrongs = [r for r in OPTIONS_REGIONS if r != target]
276+
if wrongs: target = random.choice(wrongs)
277+
278+
# do the click
279+
if target:
280+
human_like_move_and_click(target, random.uniform(0.2, 0.8))
281+
time.sleep(random.uniform(2, 3))
282+
283+
iterations += 1
284+
285+
except KeyboardInterrupt:
286+
print("\nstopped by user")
287+
except Exception as e:
288+
print(f"\nerror: {e}")
289+
traceback.print_exc()
290+
finally:
291+
print(f"\ndone, did {iterations} questions")
292+
293+
if __name__ == "__main__":
294+
main()

0 commit comments

Comments
 (0)