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
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}%")
|
||||
Loading…
Add table
Add a link
Reference in a new issue