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 ("\n oops 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"\n error 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 ("\n stopped by user" )
287+ except Exception as e :
288+ print (f"\n error: { e } " )
289+ traceback .print_exc ()
290+ finally :
291+ print (f"\n done, did { iterations } questions" )
292+
293+ if __name__ == "__main__" :
294+ main ()
0 commit comments