Initial project structure: reusable isometric bot engine with D2R implementation

This commit is contained in:
Hoid 2026-02-14 08:50:36 +00:00
commit e0282a7111
44 changed files with 3433 additions and 0 deletions

368
engine/input/keyboard.py Normal file
View 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}%")