""" Badminton Court Reservation Bot ------------------------------- This script automates the process of reserving a badminton court at precisely the configured time. It navigates through the reservation interface, handles CAPTCHA challenges, and logs the entire process. """ import logging import time from datetime import datetime, timedelta import ddddocr import numpy as np import pyautogui from PIL import Image # Configure logging to both file and console logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("reservation.log"), logging.StreamHandler()], ) # Configuration constants RESERVATION_HOUR = 7 # Target hour for reservation (24-hour format) RESERVATION_MINUTE = 0 # Target minute for reservation COORDINATES = { "miniprogram_icon": (1093, 225), # Position of the mini-program icon "title_bar": (973, 21), # Title bar position "right_edge": (1919, 150), # Screen right edge position "reservation_button": (1557, 546), # Reservation button position "venue_button": (1260, 332), # Main venue category selection "badminton_button": (1101, 223), # Main badminton category selection "reserve_button": (1017, 355), # Reserve action button "court_selection": (1401, 516), # Specific court selection "confirm_button": (1439, 991), # Final confirmation button "captcha_input": (1851, 570), # CAPTCHA input field "captcha_region": (1628, 550, 1776, 592), # Screen region containing CAPTCHA image } # Pixel check points and expected colors for each UI stage PIXEL_CHECKS = { "miniprogram_loaded": { "position": (1505, 229), "color": (54, 61, 77), "timeout": 5, }, "reservation_page_loaded": { "position": (1019, 351), "color": (255, 161, 18), "timeout": 5, }, "venue_page_loaded": { "position": (1038, 133), "color": (255, 161, 18), "timeout": 5, }, "court_page_loaded": { "position": (1192, 988), "color": (149, 152, 159), "timeout": 3, }, "court_selected": {"position": (1138, 993), "color": (46, 49, 66), "timeout": 3}, } CAPTCHA_CHECK_PIXEL = (1109, 987) # Pixel to check for CAPTCHA presence CAPTCHA_CHECK_COLOR = (46, 49, 66) # Expected color when CAPTCHA is visible def wait_until_scheduled_time(): """ Suspends execution until the configured reservation time approaches. Sleeps until 10 seconds before the target time, then actively polls to ensure precise timing. """ logging.info( f"Waiting until {RESERVATION_HOUR}:{RESERVATION_MINUTE:02d} to start reservation process" ) target_time = datetime.now().replace( hour=RESERVATION_HOUR, minute=RESERVATION_MINUTE, second=0, microsecond=0 ) if target_time < datetime.now(): target_time += timedelta(days=1) time_to_sleep = (target_time - datetime.now()).total_seconds() - 10 if time_to_sleep > 0: logging.info(f"Sleeping for {time_to_sleep:.2f} seconds") time.sleep(time_to_sleep) while datetime.now() < target_time: time.sleep(0.01) logging.info("Starting reservation process") def wait_for_pixel_color(position, color, timeout=5, check_interval=0.01): """ Wait until the pixel at the given position changes to the expected color or timeout is reached. Args: position (tuple): (x, y) coordinates of the pixel to check color (tuple): Expected RGB color value (r, g, b) timeout (float): Maximum time to wait in seconds check_interval (float): Time between checks in seconds Returns: bool: True if the pixel color matched, False if timeout was reached """ x, y = position start_time = time.time() logging.info(f"Waiting for pixel at ({x}, {y}) to change to color {color}") while time.time() - start_time < timeout: pixel_color = Image.fromarray( np.array(pyautogui.screenshot(region=(x, y, 1, 1))) ).getpixel((0, 0)) # Check if colors are close enough (allow small variations) if pixel_color == color: logging.info(f"Pixel color matched: {pixel_color}") return True time.sleep(check_interval) logging.warning( f"Timeout waiting for pixel at ({x}, {y}) to change to {color}, current color: {pixel_color}" ) return False def click_at(position_name): """ Click at a predefined position on the screen. Args: position_name (str): Key name from COORDINATES dictionary """ x, y = COORDINATES[position_name] logging.info(f"Clicking {position_name} at ({x}, {y})") pyautogui.click(x, y) def hold_and_drag(start_position_name, end_position_name): """ Hold the mouse button down at the start position and drag to the end position. Args: start_position_name (str): Key name from COORDINATES for the start position end_position_name (str): Key name from COORDINATES for the end position """ start_x, start_y = COORDINATES[start_position_name] end_x, end_y = COORDINATES[end_position_name] logging.info( f"Holding mouse at {start_position_name} ({start_x}, {start_y}) and dragging to {end_position_name} ({end_x}, {end_y})" ) pyautogui.moveTo(start_x, start_y) pyautogui.mouseDown() pyautogui.moveTo(end_x, end_y, duration=0.25) # duration for a smoother drag pyautogui.mouseUp() def solve_captcha(): """ Capture, process and recognize the CAPTCHA text. Returns: str: Recognized text from the CAPTCHA image """ left, top, right, bottom = COORDINATES["captcha_region"] screenshot = pyautogui.screenshot() captcha_img = screenshot.crop((left, top, right, bottom)) captcha_img.save("captcha.png") # Save for debugging/analysis purposes # Initialize OCR engine and recognize CAPTCHA ocr = ddddocr.DdddOcr(show_ad=False) with open("captcha.png", "rb") as f: img_bytes = f.read() captcha_text = ocr.classification(img_bytes) logging.info(f"CAPTCHA recognized as: {captcha_text}") return captcha_text def input_captcha(text): """ Input the recognized CAPTCHA text into the appropriate field. Args: text (str): CAPTCHA text to input """ click_at("captcha_input") # Wait briefly for input focus time.sleep(0.01) pyautogui.hotkey("ctrl", "a") # Select all existing text pyautogui.typewrite(text) def is_captcha_showing(): """ Check if the CAPTCHA challenge is currently displayed. Returns: bool: True if CAPTCHA is displayed, False otherwise """ x, y = CAPTCHA_CHECK_PIXEL pixel_color = Image.fromarray( np.array(pyautogui.screenshot(region=(x, y, 1, 1))) ).getpixel((0, 0)) return pixel_color == CAPTCHA_CHECK_COLOR def main(): """ Main execution function that orchestrates the reservation process. Handles the sequence of UI interactions and CAPTCHA solving. """ try: pyautogui.FAILSAFE = False # Disable failsafe to prevent interruptions wait_until_scheduled_time() # Click on the mini-program icon to open the reservation interface click_at("miniprogram_icon") # Move window to the edge hold_and_drag("title_bar", "right_edge") wait_for_pixel_color(**PIXEL_CHECKS["miniprogram_loaded"]) # Navigate through reservation interface click_at("reservation_button") wait_for_pixel_color(**PIXEL_CHECKS["reservation_page_loaded"]) click_at("venue_button") wait_for_pixel_color(**PIXEL_CHECKS["venue_page_loaded"]) click_at("badminton_button") click_at("reserve_button") wait_for_pixel_color(**PIXEL_CHECKS["court_page_loaded"]) click_at("court_selection") wait_for_pixel_color(**PIXEL_CHECKS["court_selected"]) click_at("confirm_button") # Handle CAPTCHA if presented captcha_attempts = 0 while is_captcha_showing() and captcha_attempts < 10: captcha_attempts += 1 logging.info(f"CAPTCHA attempt {captcha_attempts}") captcha_text = solve_captcha() input_captcha(captcha_text) time.sleep(0.05) click_at("confirm_button") time.sleep(0.2) # Report final status if captcha_attempts >= 10: logging.warning("Maximum CAPTCHA attempts reached") else: logging.info("Reservation process completed") except Exception as e: logging.error(f"An error occurred: {str(e)}") if __name__ == "__main__": main()