Initial project structure: reusable isometric bot engine with D2R implementation
This commit is contained in:
commit
e0282a7111
44 changed files with 3433 additions and 0 deletions
23
engine/input/__init__.py
Normal file
23
engine/input/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""Human-like input simulation for mouse and keyboard interactions.
|
||||
|
||||
This module provides tools for generating realistic input patterns that mimic
|
||||
human behavior, including natural mouse movement curves and timing variations.
|
||||
|
||||
Components:
|
||||
- mouse: Human-like mouse movement with Bézier curves
|
||||
- keyboard: Keyboard input with realistic timing patterns
|
||||
- humanize: Central controller for randomized, human-like interactions
|
||||
"""
|
||||
|
||||
from .mouse import MouseController, MousePath
|
||||
from .keyboard import KeyboardController, KeySequence
|
||||
from .humanize import HumanInput, InputConfig
|
||||
|
||||
__all__ = [
|
||||
"MouseController",
|
||||
"MousePath",
|
||||
"KeyboardController",
|
||||
"KeySequence",
|
||||
"HumanInput",
|
||||
"InputConfig",
|
||||
]
|
||||
112
engine/input/humanize.py
Normal file
112
engine/input/humanize.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"""Human-like behavior patterns for input simulation.
|
||||
|
||||
Provides randomization utilities to make bot inputs appear natural,
|
||||
including variable delays, mouse jitter, and activity scheduling.
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HumanProfile:
|
||||
"""Defines a human behavior profile for input randomization."""
|
||||
|
||||
# Reaction time range in seconds
|
||||
reaction_min: float = 0.15
|
||||
reaction_max: float = 0.45
|
||||
|
||||
# Mouse movement speed range (pixels per second)
|
||||
mouse_speed_min: float = 400.0
|
||||
mouse_speed_max: float = 1200.0
|
||||
|
||||
# Click position jitter in pixels
|
||||
click_jitter: int = 3
|
||||
|
||||
# Chance of double-reading (hesitation before action)
|
||||
hesitation_chance: float = 0.1
|
||||
hesitation_duration: Tuple[float, float] = (0.3, 1.2)
|
||||
|
||||
# Break scheduling
|
||||
micro_break_interval: Tuple[int, int] = (120, 300) # seconds
|
||||
micro_break_duration: Tuple[int, int] = (2, 8) # seconds
|
||||
long_break_interval: Tuple[int, int] = (1800, 3600) # seconds
|
||||
long_break_duration: Tuple[int, int] = (60, 300) # seconds
|
||||
|
||||
|
||||
class Humanizer:
|
||||
"""Applies human-like randomization to bot actions."""
|
||||
|
||||
def __init__(self, profile: Optional[HumanProfile] = None):
|
||||
self.profile = profile or HumanProfile()
|
||||
self._last_micro_break = time.time()
|
||||
self._last_long_break = time.time()
|
||||
self._next_micro_break = self._schedule_break(self.profile.micro_break_interval)
|
||||
self._next_long_break = self._schedule_break(self.profile.long_break_interval)
|
||||
self._action_count = 0
|
||||
|
||||
def reaction_delay(self) -> float:
|
||||
"""Generate a human-like reaction delay."""
|
||||
base = random.uniform(self.profile.reaction_min, self.profile.reaction_max)
|
||||
|
||||
# Occasionally add hesitation
|
||||
if random.random() < self.profile.hesitation_chance:
|
||||
base += random.uniform(*self.profile.hesitation_duration)
|
||||
|
||||
# Slight fatigue factor based on actions performed
|
||||
fatigue = min(self._action_count / 1000, 0.3)
|
||||
base *= (1 + fatigue * random.random())
|
||||
|
||||
return base
|
||||
|
||||
def jitter_position(self, x: int, y: int) -> Tuple[int, int]:
|
||||
"""Add small random offset to click position."""
|
||||
jitter = self.profile.click_jitter
|
||||
return (
|
||||
x + random.randint(-jitter, jitter),
|
||||
y + random.randint(-jitter, jitter),
|
||||
)
|
||||
|
||||
def mouse_speed(self) -> float:
|
||||
"""Get randomized mouse movement speed."""
|
||||
return random.uniform(
|
||||
self.profile.mouse_speed_min,
|
||||
self.profile.mouse_speed_max,
|
||||
)
|
||||
|
||||
def should_take_break(self) -> Optional[float]:
|
||||
"""Check if it's time for a break. Returns break duration or None."""
|
||||
now = time.time()
|
||||
|
||||
if now >= self._next_long_break:
|
||||
duration = random.uniform(*self.profile.long_break_duration)
|
||||
self._next_long_break = now + duration + self._schedule_break(
|
||||
self.profile.long_break_interval
|
||||
)
|
||||
logger.info(f"Long break: {duration:.0f}s")
|
||||
return duration
|
||||
|
||||
if now >= self._next_micro_break:
|
||||
duration = random.uniform(*self.profile.micro_break_duration)
|
||||
self._next_micro_break = now + duration + self._schedule_break(
|
||||
self.profile.micro_break_interval
|
||||
)
|
||||
logger.debug(f"Micro break: {duration:.1f}s")
|
||||
return duration
|
||||
|
||||
return None
|
||||
|
||||
def wait(self) -> None:
|
||||
"""Wait for a human-like reaction delay."""
|
||||
delay = self.reaction_delay()
|
||||
time.sleep(delay)
|
||||
self._action_count += 1
|
||||
|
||||
def _schedule_break(self, interval: Tuple[int, int]) -> float:
|
||||
"""Schedule next break with randomized interval."""
|
||||
return time.time() + random.uniform(*interval)
|
||||
368
engine/input/keyboard.py
Normal file
368
engine/input/keyboard.py
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
"""Human-like keyboard input simulation with realistic timing.
|
||||
|
||||
Provides keyboard input with natural typing patterns, including
|
||||
varied keystroke timing and realistic human typing characteristics.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
|
||||
import pyautogui
|
||||
from pynput import keyboard
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class KeySequence:
|
||||
"""Represents a sequence of keys with timing information."""
|
||||
keys: List[str]
|
||||
delays: List[float]
|
||||
total_duration: float
|
||||
|
||||
|
||||
class TypingProfile:
|
||||
"""Defines typing characteristics for human-like input."""
|
||||
|
||||
def __init__(self, wpm: int = 60, accuracy: float = 0.95):
|
||||
"""Initialize typing profile.
|
||||
|
||||
Args:
|
||||
wpm: Words per minute typing speed
|
||||
accuracy: Typing accuracy (0.0 to 1.0)
|
||||
"""
|
||||
self.wpm = wpm
|
||||
self.accuracy = accuracy
|
||||
|
||||
# Calculate base timing from WPM (assuming 5 characters per word)
|
||||
chars_per_minute = wpm * 5
|
||||
self.base_char_delay = 60.0 / chars_per_minute
|
||||
|
||||
# Timing variations
|
||||
self.min_delay = 0.02 # Minimum delay between keys
|
||||
self.max_delay = 0.5 # Maximum delay between keys
|
||||
self.word_pause = 0.1 # Additional pause after spaces
|
||||
|
||||
# Common typing patterns
|
||||
self.difficult_sequences = {
|
||||
'th', 'ch', 'sh', 'qu', 'tion', 'ing', 'er', 'ed'
|
||||
}
|
||||
|
||||
# Keys that typically take longer to find
|
||||
self.slow_keys = {
|
||||
'q', 'z', 'x', 'j', 'k', 'shift', 'ctrl', 'alt'
|
||||
}
|
||||
|
||||
|
||||
class KeyboardController:
|
||||
"""Controller for human-like keyboard input."""
|
||||
|
||||
def __init__(self, typing_profile: Optional[TypingProfile] = None):
|
||||
"""Initialize keyboard controller.
|
||||
|
||||
Args:
|
||||
typing_profile: Typing characteristics, or None for default
|
||||
"""
|
||||
self.profile = typing_profile or TypingProfile()
|
||||
self.shift_held = False
|
||||
self.ctrl_held = False
|
||||
self.alt_held = False
|
||||
|
||||
# Key mapping for special keys
|
||||
self.key_mapping = {
|
||||
'enter': '\n',
|
||||
'return': '\n',
|
||||
'tab': '\t',
|
||||
'space': ' ',
|
||||
'backspace': 'backspace',
|
||||
'delete': 'delete',
|
||||
'escape': 'esc',
|
||||
'esc': 'esc',
|
||||
}
|
||||
|
||||
def calculate_key_delay(self, key: str, previous_key: Optional[str] = None) -> float:
|
||||
"""Calculate realistic delay for typing a key.
|
||||
|
||||
Args:
|
||||
key: Key to type
|
||||
previous_key: Previously typed key for sequence analysis
|
||||
|
||||
Returns:
|
||||
Delay in seconds before typing this key
|
||||
"""
|
||||
# Base delay from typing speed
|
||||
delay = self.profile.base_char_delay
|
||||
|
||||
# Adjust for key difficulty
|
||||
if key.lower() in self.profile.slow_keys:
|
||||
delay *= random.uniform(1.2, 1.8)
|
||||
|
||||
# Adjust for difficult sequences
|
||||
if previous_key:
|
||||
sequence = previous_key.lower() + key.lower()
|
||||
if any(seq in sequence for seq in self.profile.difficult_sequences):
|
||||
delay *= random.uniform(1.1, 1.5)
|
||||
|
||||
# Add natural variation
|
||||
delay *= random.uniform(0.7, 1.4)
|
||||
|
||||
# Extra pause after spaces (word boundaries)
|
||||
if previous_key and previous_key == ' ':
|
||||
delay += self.profile.word_pause * random.uniform(0.5, 1.5)
|
||||
|
||||
# Clamp to reasonable bounds
|
||||
return max(self.profile.min_delay, min(self.profile.max_delay, delay))
|
||||
|
||||
def type_text(self, text: str, include_errors: bool = False) -> None:
|
||||
"""Type text with human-like timing and optional errors.
|
||||
|
||||
Args:
|
||||
text: Text to type
|
||||
include_errors: Whether to include typing errors and corrections
|
||||
"""
|
||||
if not text:
|
||||
return
|
||||
|
||||
logger.debug(f"Typing text: '{text[:50]}{'...' if len(text) > 50 else ''}'")
|
||||
|
||||
previous_key = None
|
||||
|
||||
for i, char in enumerate(text):
|
||||
# Calculate delay before this character
|
||||
delay = self.calculate_key_delay(char, previous_key)
|
||||
|
||||
# Sleep before typing
|
||||
time.sleep(delay)
|
||||
|
||||
# Occasionally make typing errors if enabled
|
||||
if include_errors and self.should_make_error():
|
||||
self.make_typing_error(char)
|
||||
else:
|
||||
self.type_key(char)
|
||||
|
||||
previous_key = char
|
||||
|
||||
def should_make_error(self) -> bool:
|
||||
"""Determine if a typing error should be made.
|
||||
|
||||
Returns:
|
||||
True if an error should be made
|
||||
"""
|
||||
return random.random() > self.profile.accuracy
|
||||
|
||||
def make_typing_error(self, intended_key: str) -> None:
|
||||
"""Make a typing error and correct it.
|
||||
|
||||
Args:
|
||||
intended_key: The key that was supposed to be typed
|
||||
"""
|
||||
# Type wrong key (usually adjacent on keyboard)
|
||||
wrong_key = self.get_adjacent_key(intended_key)
|
||||
self.type_key(wrong_key)
|
||||
|
||||
# Pause as human realizes mistake
|
||||
time.sleep(random.uniform(0.1, 0.4))
|
||||
|
||||
# Backspace to correct
|
||||
self.type_key('backspace')
|
||||
time.sleep(random.uniform(0.05, 0.15))
|
||||
|
||||
# Type correct key
|
||||
self.type_key(intended_key)
|
||||
|
||||
def get_adjacent_key(self, key: str) -> str:
|
||||
"""Get an adjacent key for typing errors.
|
||||
|
||||
Args:
|
||||
key: Original key
|
||||
|
||||
Returns:
|
||||
Adjacent key that could be mistyped
|
||||
"""
|
||||
# Simplified adjacent key mapping
|
||||
adjacent_map = {
|
||||
'a': 'sq', 'b': 'vgn', 'c': 'xvd', 'd': 'sfe', 'e': 'wrd',
|
||||
'f': 'dgr', 'g': 'fht', 'h': 'gyu', 'i': 'uko', 'j': 'hnu',
|
||||
'k': 'jmo', 'l': 'kpo', 'm': 'njk', 'n': 'bhm', 'o': 'ilp',
|
||||
'p': 'olo', 'q': 'wa', 'r': 'etf', 's': 'adw', 't': 'rgy',
|
||||
'u': 'yhi', 'v': 'cfg', 'w': 'qse', 'x': 'zdc', 'y': 'tgu',
|
||||
'z': 'xas'
|
||||
}
|
||||
|
||||
adjacent_keys = adjacent_map.get(key.lower(), 'abcd')
|
||||
return random.choice(adjacent_keys)
|
||||
|
||||
def type_key(self, key: str) -> None:
|
||||
"""Type a single key.
|
||||
|
||||
Args:
|
||||
key: Key to type
|
||||
"""
|
||||
# Handle special keys
|
||||
if key.lower() in self.key_mapping:
|
||||
mapped_key = self.key_mapping[key.lower()]
|
||||
if mapped_key in ['backspace', 'delete', 'esc']:
|
||||
pyautogui.press(mapped_key)
|
||||
else:
|
||||
pyautogui.write(mapped_key)
|
||||
else:
|
||||
pyautogui.write(key)
|
||||
|
||||
def press_key_combination(self, *keys: str) -> None:
|
||||
"""Press a combination of keys (e.g., Ctrl+C).
|
||||
|
||||
Args:
|
||||
keys: Keys to press together
|
||||
"""
|
||||
logger.debug(f"Pressing key combination: {'+'.join(keys)}")
|
||||
|
||||
# Press all keys down
|
||||
for key in keys:
|
||||
pyautogui.keyDown(key)
|
||||
time.sleep(random.uniform(0.01, 0.03))
|
||||
|
||||
# Hold briefly
|
||||
time.sleep(random.uniform(0.05, 0.1))
|
||||
|
||||
# Release all keys (in reverse order)
|
||||
for key in reversed(keys):
|
||||
pyautogui.keyUp(key)
|
||||
time.sleep(random.uniform(0.01, 0.03))
|
||||
|
||||
def press_key(self, key: str, duration: Optional[float] = None) -> None:
|
||||
"""Press and release a key.
|
||||
|
||||
Args:
|
||||
key: Key to press
|
||||
duration: How long to hold key, or None for quick press
|
||||
"""
|
||||
if duration is None:
|
||||
pyautogui.press(key)
|
||||
else:
|
||||
pyautogui.keyDown(key)
|
||||
time.sleep(duration)
|
||||
pyautogui.keyUp(key)
|
||||
|
||||
def hold_key(self, key: str) -> None:
|
||||
"""Start holding a key down.
|
||||
|
||||
Args:
|
||||
key: Key to hold
|
||||
"""
|
||||
pyautogui.keyDown(key)
|
||||
|
||||
# Track modifier keys
|
||||
if key.lower() == 'shift':
|
||||
self.shift_held = True
|
||||
elif key.lower() in ['ctrl', 'control']:
|
||||
self.ctrl_held = True
|
||||
elif key.lower() == 'alt':
|
||||
self.alt_held = True
|
||||
|
||||
def release_key(self, key: str) -> None:
|
||||
"""Stop holding a key.
|
||||
|
||||
Args:
|
||||
key: Key to release
|
||||
"""
|
||||
pyautogui.keyUp(key)
|
||||
|
||||
# Track modifier keys
|
||||
if key.lower() == 'shift':
|
||||
self.shift_held = False
|
||||
elif key.lower() in ['ctrl', 'control']:
|
||||
self.ctrl_held = False
|
||||
elif key.lower() == 'alt':
|
||||
self.alt_held = False
|
||||
|
||||
def release_all_keys(self) -> None:
|
||||
"""Release all held modifier keys."""
|
||||
if self.shift_held:
|
||||
self.release_key('shift')
|
||||
if self.ctrl_held:
|
||||
self.release_key('ctrl')
|
||||
if self.alt_held:
|
||||
self.release_key('alt')
|
||||
|
||||
def type_number_sequence(self, numbers: Union[str, int],
|
||||
use_numpad: bool = False) -> None:
|
||||
"""Type a sequence of numbers.
|
||||
|
||||
Args:
|
||||
numbers: Numbers to type
|
||||
use_numpad: Whether to use numpad keys
|
||||
"""
|
||||
number_str = str(numbers)
|
||||
|
||||
for digit in number_str:
|
||||
if digit.isdigit():
|
||||
if use_numpad:
|
||||
key = f'num{digit}'
|
||||
else:
|
||||
key = digit
|
||||
|
||||
self.type_key(key)
|
||||
time.sleep(self.calculate_key_delay(digit))
|
||||
|
||||
def simulate_pause(self, pause_type: str = 'thinking') -> None:
|
||||
"""Simulate natural pauses in typing.
|
||||
|
||||
Args:
|
||||
pause_type: Type of pause ('thinking', 'reading', 'short')
|
||||
"""
|
||||
if pause_type == 'thinking':
|
||||
duration = random.uniform(0.5, 2.0)
|
||||
elif pause_type == 'reading':
|
||||
duration = random.uniform(0.2, 0.8)
|
||||
else: # short
|
||||
duration = random.uniform(0.1, 0.3)
|
||||
|
||||
logger.debug(f"Simulating {pause_type} pause for {duration:.2f}s")
|
||||
time.sleep(duration)
|
||||
|
||||
def generate_key_sequence(self, text: str) -> KeySequence:
|
||||
"""Generate a key sequence with timing for given text.
|
||||
|
||||
Args:
|
||||
text: Text to generate sequence for
|
||||
|
||||
Returns:
|
||||
KeySequence with keys and delays
|
||||
"""
|
||||
keys = list(text)
|
||||
delays = []
|
||||
total_duration = 0.0
|
||||
|
||||
previous_key = None
|
||||
|
||||
for key in keys:
|
||||
delay = self.calculate_key_delay(key, previous_key)
|
||||
delays.append(delay)
|
||||
total_duration += delay
|
||||
previous_key = key
|
||||
|
||||
return KeySequence(keys, delays, total_duration)
|
||||
|
||||
def set_typing_speed(self, wpm: int) -> None:
|
||||
"""Set typing speed.
|
||||
|
||||
Args:
|
||||
wpm: Words per minute
|
||||
"""
|
||||
self.profile.wpm = max(10, min(200, wpm))
|
||||
chars_per_minute = self.profile.wpm * 5
|
||||
self.profile.base_char_delay = 60.0 / chars_per_minute
|
||||
|
||||
logger.info(f"Typing speed set to {self.profile.wpm} WPM")
|
||||
|
||||
def set_accuracy(self, accuracy: float) -> None:
|
||||
"""Set typing accuracy.
|
||||
|
||||
Args:
|
||||
accuracy: Accuracy from 0.0 to 1.0
|
||||
"""
|
||||
self.profile.accuracy = max(0.0, min(1.0, accuracy))
|
||||
logger.info(f"Typing accuracy set to {self.profile.accuracy * 100:.1f}%")
|
||||
345
engine/input/mouse.py
Normal file
345
engine/input/mouse.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
"""Human-like mouse movement and clicking with Bézier curves.
|
||||
|
||||
Provides realistic mouse movement patterns using Bézier curves with
|
||||
randomized control points and natural acceleration/deceleration.
|
||||
"""
|
||||
|
||||
from typing import Tuple, List, Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import logging
|
||||
|
||||
import pyautogui
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Disable pyautogui failsafe for production use
|
||||
pyautogui.FAILSAFE = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class MousePath:
|
||||
"""Represents a mouse movement path with timing."""
|
||||
points: List[Tuple[int, int]]
|
||||
delays: List[float]
|
||||
total_duration: float
|
||||
|
||||
|
||||
class BezierCurve:
|
||||
"""Bézier curve generation for natural mouse movement."""
|
||||
|
||||
@staticmethod
|
||||
def cubic_bezier(t: float, p0: Tuple[float, float], p1: Tuple[float, float],
|
||||
p2: Tuple[float, float], p3: Tuple[float, float]) -> Tuple[float, float]:
|
||||
"""Calculate point on cubic Bézier curve at parameter t.
|
||||
|
||||
Args:
|
||||
t: Parameter from 0 to 1
|
||||
p0: Start point
|
||||
p1: First control point
|
||||
p2: Second control point
|
||||
p3: End point
|
||||
|
||||
Returns:
|
||||
(x, y) point on curve
|
||||
"""
|
||||
x = (1-t)**3 * p0[0] + 3*(1-t)**2*t * p1[0] + 3*(1-t)*t**2 * p2[0] + t**3 * p3[0]
|
||||
y = (1-t)**3 * p0[1] + 3*(1-t)**2*t * p1[1] + 3*(1-t)*t**2 * p2[1] + t**3 * p3[1]
|
||||
return (x, y)
|
||||
|
||||
@staticmethod
|
||||
def generate_control_points(start: Tuple[int, int], end: Tuple[int, int],
|
||||
randomness: float = 0.3) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
||||
"""Generate random control points for natural curve.
|
||||
|
||||
Args:
|
||||
start: Starting position
|
||||
end: Ending position
|
||||
randomness: Amount of randomness (0.0 to 1.0)
|
||||
|
||||
Returns:
|
||||
Tuple of two control points
|
||||
"""
|
||||
dx = end[0] - start[0]
|
||||
dy = end[1] - start[1]
|
||||
distance = math.sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Control point offset based on distance and randomness
|
||||
offset_magnitude = distance * randomness * random.uniform(0.2, 0.8)
|
||||
|
||||
# Random angles for control points
|
||||
angle1 = random.uniform(-math.pi, math.pi)
|
||||
angle2 = random.uniform(-math.pi, math.pi)
|
||||
|
||||
# First control point (closer to start)
|
||||
cp1_x = start[0] + dx * 0.25 + math.cos(angle1) * offset_magnitude
|
||||
cp1_y = start[1] + dy * 0.25 + math.sin(angle1) * offset_magnitude
|
||||
|
||||
# Second control point (closer to end)
|
||||
cp2_x = start[0] + dx * 0.75 + math.cos(angle2) * offset_magnitude
|
||||
cp2_y = start[1] + dy * 0.75 + math.sin(angle2) * offset_magnitude
|
||||
|
||||
return ((cp1_x, cp1_y), (cp2_x, cp2_y))
|
||||
|
||||
|
||||
class MouseController:
|
||||
"""Controller for human-like mouse interactions."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize mouse controller."""
|
||||
self.current_pos = pyautogui.position()
|
||||
self.movement_speed = 1.0 # Multiplier for movement speed
|
||||
self.click_variance = 3 # Pixel variance for click positions
|
||||
|
||||
# Movement timing parameters
|
||||
self.min_duration = 0.1 # Minimum movement time
|
||||
self.max_duration = 1.5 # Maximum movement time
|
||||
self.base_speed = 1000 # Base pixels per second
|
||||
|
||||
def get_current_position(self) -> Tuple[int, int]:
|
||||
"""Get current mouse position.
|
||||
|
||||
Returns:
|
||||
(x, y) tuple of current position
|
||||
"""
|
||||
self.current_pos = pyautogui.position()
|
||||
return self.current_pos
|
||||
|
||||
def calculate_movement_duration(self, start: Tuple[int, int],
|
||||
end: Tuple[int, int]) -> float:
|
||||
"""Calculate realistic movement duration based on distance.
|
||||
|
||||
Args:
|
||||
start: Starting position
|
||||
end: Ending position
|
||||
|
||||
Returns:
|
||||
Movement duration in seconds
|
||||
"""
|
||||
dx = end[0] - start[0]
|
||||
dy = end[1] - start[1]
|
||||
distance = math.sqrt(dx*dx + dy*dy)
|
||||
|
||||
# Fitts' Law inspired calculation
|
||||
# Time increases logarithmically with distance
|
||||
base_time = distance / (self.base_speed * self.movement_speed)
|
||||
fitts_factor = math.log2(1 + distance / 10) / 10
|
||||
|
||||
duration = base_time + fitts_factor
|
||||
|
||||
# Add some randomness
|
||||
duration *= random.uniform(0.8, 1.2)
|
||||
|
||||
# Clamp to reasonable bounds
|
||||
return max(self.min_duration, min(self.max_duration, duration))
|
||||
|
||||
def generate_movement_path(self, start: Tuple[int, int], end: Tuple[int, int],
|
||||
duration: Optional[float] = None,
|
||||
steps: Optional[int] = None) -> MousePath:
|
||||
"""Generate Bézier curve path for mouse movement.
|
||||
|
||||
Args:
|
||||
start: Starting position
|
||||
end: Ending position
|
||||
duration: Movement duration, or None to calculate
|
||||
steps: Number of steps, or None to calculate
|
||||
|
||||
Returns:
|
||||
MousePath with points and timing
|
||||
"""
|
||||
if duration is None:
|
||||
duration = self.calculate_movement_duration(start, end)
|
||||
|
||||
if steps is None:
|
||||
# Calculate steps based on distance and duration
|
||||
distance = math.sqrt((end[0] - start[0])**2 + (end[1] - start[1])**2)
|
||||
steps = max(10, int(distance / 10)) # Roughly 10 pixels per step
|
||||
|
||||
# Generate control points
|
||||
cp1, cp2 = BezierCurve.generate_control_points(start, end)
|
||||
|
||||
# Generate path points
|
||||
points = []
|
||||
delays = []
|
||||
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
|
||||
# Use ease-in-out curve for timing
|
||||
timing_t = self._ease_in_out(t)
|
||||
|
||||
# Calculate position on Bézier curve
|
||||
x, y = BezierCurve.cubic_bezier(timing_t, start, cp1, cp2, end)
|
||||
points.append((int(x), int(y)))
|
||||
|
||||
# Calculate delay for this step
|
||||
if i < steps:
|
||||
delay = duration / steps
|
||||
# Add small random variation
|
||||
delay *= random.uniform(0.8, 1.2)
|
||||
delays.append(delay)
|
||||
|
||||
return MousePath(points, delays, duration)
|
||||
|
||||
def move_to(self, target: Tuple[int, int], duration: Optional[float] = None) -> None:
|
||||
"""Move mouse to target position using Bézier curve.
|
||||
|
||||
Args:
|
||||
target: Target (x, y) position
|
||||
duration: Movement duration, or None to calculate
|
||||
"""
|
||||
start = self.get_current_position()
|
||||
path = self.generate_movement_path(start, target, duration)
|
||||
|
||||
logger.debug(f"Moving mouse from {start} to {target} in {path.total_duration:.2f}s")
|
||||
|
||||
for i, point in enumerate(path.points[1:], 1):
|
||||
pyautogui.moveTo(point[0], point[1], duration=0)
|
||||
|
||||
if i <= len(path.delays):
|
||||
time.sleep(path.delays[i-1])
|
||||
|
||||
self.current_pos = target
|
||||
|
||||
def click(self, position: Optional[Tuple[int, int]] = None,
|
||||
button: str = 'left', move_first: bool = True) -> None:
|
||||
"""Click at specified position with human-like variation.
|
||||
|
||||
Args:
|
||||
position: Click position, or None for current position
|
||||
button: Mouse button ('left', 'right', 'middle')
|
||||
move_first: Whether to move to position first
|
||||
"""
|
||||
if position is None:
|
||||
position = self.get_current_position()
|
||||
else:
|
||||
# Add small random offset for more human-like clicking
|
||||
offset_x = random.randint(-self.click_variance, self.click_variance)
|
||||
offset_y = random.randint(-self.click_variance, self.click_variance)
|
||||
position = (position[0] + offset_x, position[1] + offset_y)
|
||||
|
||||
if move_first and position != self.get_current_position():
|
||||
self.move_to(position)
|
||||
|
||||
# Random pre-click delay
|
||||
time.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
logger.debug(f"Clicking {button} button at {position}")
|
||||
pyautogui.click(position[0], position[1], button=button)
|
||||
|
||||
# Random post-click delay
|
||||
time.sleep(random.uniform(0.01, 0.08))
|
||||
|
||||
def double_click(self, position: Optional[Tuple[int, int]] = None,
|
||||
move_first: bool = True) -> None:
|
||||
"""Double-click at specified position.
|
||||
|
||||
Args:
|
||||
position: Click position, or None for current position
|
||||
move_first: Whether to move to position first
|
||||
"""
|
||||
if position is None:
|
||||
position = self.get_current_position()
|
||||
|
||||
if move_first and position != self.get_current_position():
|
||||
self.move_to(position)
|
||||
|
||||
# Random delay before double-click
|
||||
time.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
logger.debug(f"Double-clicking at {position}")
|
||||
pyautogui.doubleClick(position[0], position[1])
|
||||
|
||||
# Random delay after double-click
|
||||
time.sleep(random.uniform(0.05, 0.1))
|
||||
|
||||
def drag(self, start: Tuple[int, int], end: Tuple[int, int],
|
||||
button: str = 'left', duration: Optional[float] = None) -> None:
|
||||
"""Drag from start to end position.
|
||||
|
||||
Args:
|
||||
start: Starting position
|
||||
end: Ending position
|
||||
button: Mouse button to drag with
|
||||
duration: Drag duration, or None to calculate
|
||||
"""
|
||||
# Move to start position
|
||||
self.move_to(start)
|
||||
|
||||
# Mouse down
|
||||
time.sleep(random.uniform(0.01, 0.03))
|
||||
pyautogui.mouseDown(start[0], start[1], button=button)
|
||||
|
||||
# Wait briefly before starting drag
|
||||
time.sleep(random.uniform(0.05, 0.1))
|
||||
|
||||
# Generate drag path
|
||||
path = self.generate_movement_path(start, end, duration)
|
||||
|
||||
logger.debug(f"Dragging from {start} to {end}")
|
||||
|
||||
# Execute drag movement
|
||||
for i, point in enumerate(path.points[1:], 1):
|
||||
pyautogui.moveTo(point[0], point[1], duration=0)
|
||||
|
||||
if i <= len(path.delays):
|
||||
time.sleep(path.delays[i-1])
|
||||
|
||||
# Mouse up
|
||||
time.sleep(random.uniform(0.01, 0.03))
|
||||
pyautogui.mouseUp(end[0], end[1], button=button)
|
||||
|
||||
self.current_pos = end
|
||||
|
||||
def scroll(self, clicks: int, position: Optional[Tuple[int, int]] = None) -> None:
|
||||
"""Scroll at specified position.
|
||||
|
||||
Args:
|
||||
clicks: Number of scroll clicks (positive = up, negative = down)
|
||||
position: Scroll position, or None for current position
|
||||
"""
|
||||
if position is not None and position != self.get_current_position():
|
||||
self.move_to(position)
|
||||
|
||||
# Random delay before scrolling
|
||||
time.sleep(random.uniform(0.05, 0.15))
|
||||
|
||||
# Scroll with small delays between clicks for more human-like behavior
|
||||
for i in range(abs(clicks)):
|
||||
scroll_direction = 1 if clicks > 0 else -1
|
||||
pyautogui.scroll(scroll_direction)
|
||||
|
||||
if i < abs(clicks) - 1: # Don't delay after last scroll
|
||||
time.sleep(random.uniform(0.02, 0.08))
|
||||
|
||||
def _ease_in_out(self, t: float) -> float:
|
||||
"""Ease-in-out function for smooth acceleration/deceleration.
|
||||
|
||||
Args:
|
||||
t: Input parameter (0 to 1)
|
||||
|
||||
Returns:
|
||||
Eased parameter (0 to 1)
|
||||
"""
|
||||
return t * t * (3.0 - 2.0 * t)
|
||||
|
||||
def set_movement_speed(self, speed: float) -> None:
|
||||
"""Set movement speed multiplier.
|
||||
|
||||
Args:
|
||||
speed: Speed multiplier (1.0 = normal, 2.0 = double speed, etc.)
|
||||
"""
|
||||
self.movement_speed = max(0.1, min(5.0, speed))
|
||||
logger.info(f"Mouse movement speed set to {self.movement_speed}x")
|
||||
|
||||
def set_click_variance(self, variance: int) -> None:
|
||||
"""Set click position variance in pixels.
|
||||
|
||||
Args:
|
||||
variance: Maximum pixel offset for clicks
|
||||
"""
|
||||
self.click_variance = max(0, min(10, variance))
|
||||
logger.info(f"Click variance set to {self.click_variance} pixels")
|
||||
Loading…
Add table
Add a link
Reference in a new issue