311 lines
9.8 KiB
Python
311 lines
9.8 KiB
Python
"""
|
|
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
|
|
"close_button": (1886, 27), # Close button for the miniprogram window
|
|
}
|
|
|
|
# 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
|
|
MAX_RETRY_ATTEMPTS = 5 # Maximum retry attempts
|
|
|
|
|
|
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.2) # 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 close_window_and_restart():
|
|
"""
|
|
Close the current mini-program window and wait a short period
|
|
"""
|
|
logging.info("Closing the mini-program window and preparing to restart")
|
|
click_at("close_button")
|
|
|
|
|
|
def reservation_process():
|
|
"""
|
|
Execute the complete reservation process
|
|
"""
|
|
# 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")
|
|
if not wait_for_pixel_color(**PIXEL_CHECKS["miniprogram_loaded"]):
|
|
return False
|
|
|
|
# Navigate through reservation interface
|
|
click_at("reservation_button")
|
|
if not wait_for_pixel_color(**PIXEL_CHECKS["reservation_page_loaded"]):
|
|
return False
|
|
|
|
click_at("venue_button")
|
|
if not wait_for_pixel_color(**PIXEL_CHECKS["venue_page_loaded"]):
|
|
return False
|
|
|
|
click_at("badminton_button")
|
|
click_at("reserve_button")
|
|
if not wait_for_pixel_color(**PIXEL_CHECKS["court_page_loaded"]):
|
|
return False
|
|
|
|
click_at("court_selection")
|
|
if not wait_for_pixel_color(**PIXEL_CHECKS["court_selected"]):
|
|
return False
|
|
|
|
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)
|
|
|
|
if captcha_attempts >= 10:
|
|
logging.warning(
|
|
"CAPTCHA recognition failed more than 10 times, need to restart"
|
|
)
|
|
return False
|
|
else:
|
|
logging.info("Reservation process completed")
|
|
return True
|
|
|
|
|
|
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()
|
|
|
|
retry_count = 0
|
|
success = False
|
|
|
|
while not success and retry_count < MAX_RETRY_ATTEMPTS:
|
|
try:
|
|
if retry_count > 0:
|
|
logging.info(f"Retry reservation attempt {retry_count}")
|
|
success = reservation_process()
|
|
|
|
if not success:
|
|
retry_count += 1
|
|
close_window_and_restart()
|
|
except Exception as e:
|
|
logging.error(f"Error occurred during reservation process: {str(e)}")
|
|
retry_count += 1
|
|
close_window_and_restart()
|
|
|
|
if success:
|
|
logging.info("Reservation successfully completed!")
|
|
else:
|
|
logging.error(
|
|
f"Failed to complete reservation after {MAX_RETRY_ATTEMPTS} attempts"
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"An error occurred: {str(e)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|