Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import numpy as np | |
| from dataclasses import dataclass | |
| from typing import Optional, Tuple | |
| def minimum_jerk(t: np.ndarray) -> np.ndarray: | |
| """ | |
| Minimum jerk trajectory (5th order polynomial). | |
| This is the optimal trajectory that minimizes jerk (rate of change of acceleration). | |
| Human movements closely follow this profile. | |
| Args: | |
| t: Normalized time array [0, 1] | |
| Returns: | |
| Normalized position [0, 1] | |
| """ | |
| # 5th order polynomial: 10t³ - 15t⁴ + 6t⁵ | |
| return 10.0 * t**3 - 15.0 * t**4 + 6.0 * t**5 | |
| def signal_dependent_noise( | |
| velocity: np.ndarray, | |
| k: float = 0.1, | |
| rng: Optional[np.random.Generator] = None, | |
| ) -> np.ndarray: | |
| """ | |
| Signal-dependent noise following Harris-Wolpert model. | |
| Noise variance scales with signal magnitude (Weber's law). | |
| This is a fundamental property of human motor control. | |
| Args: | |
| velocity: Velocity array | |
| k: Noise coefficient (higher = noisier) | |
| rng: Random generator | |
| Returns: | |
| Noise array with signal-dependent variance | |
| """ | |
| if rng is None: | |
| rng = np.random.default_rng() | |
| std = k * np.abs(velocity) | |
| return rng.normal(0, std + 1e-8) # Small epsilon for stability | |
| def generate_micro_corrections( | |
| n_ticks: int, | |
| frequency: float = 5.0, | |
| amplitude: float = 0.02, | |
| rng: Optional[np.random.Generator] = None, | |
| ) -> np.ndarray: | |
| """ | |
| Generate micro-corrections that humans make during aiming. | |
| These are small corrective movements with quasi-periodic timing. | |
| Args: | |
| n_ticks: Number of time ticks | |
| frequency: Average number of corrections per trajectory | |
| amplitude: Maximum correction amplitude (degrees) | |
| rng: Random generator | |
| Returns: | |
| Array of shape [n_ticks, 2] with x,y corrections | |
| """ | |
| if rng is None: | |
| rng = np.random.default_rng() | |
| corrections = np.zeros((n_ticks, 2)) | |
| if frequency <= 0 or n_ticks < 2: | |
| return corrections | |
| # Generate correction times using exponential intervals | |
| mean_interval = n_ticks / frequency | |
| current_tick = 0 | |
| while current_tick < n_ticks: | |
| interval = int(rng.exponential(mean_interval)) | |
| current_tick += max(1, interval) | |
| if current_tick < n_ticks: | |
| # Small correction impulse | |
| amp = rng.exponential(amplitude) | |
| direction = rng.uniform(0, 2 * np.pi) | |
| corrections[current_tick, 0] = amp * np.cos(direction) | |
| corrections[current_tick, 1] = amp * np.sin(direction) | |
| # Smooth corrections (they're not instant) using simple moving average | |
| kernel_size = min(5, n_ticks // 2) | |
| if kernel_size > 1: | |
| kernel = np.ones(kernel_size) / kernel_size | |
| for i in range(2): | |
| corrections[:, i] = np.convolve(corrections[:, i], kernel, mode='same') | |
| return corrections | |
| def generate_human_trajectory( | |
| start_angle: np.ndarray, | |
| target_angle: np.ndarray, | |
| skill_aim: float, | |
| skill_consistency: float, | |
| duration_ms: float, | |
| tick_rate: int = 128, | |
| rng: Optional[np.random.Generator] = None, | |
| ) -> np.ndarray: | |
| """ | |
| Generate realistic human mouse trajectory using minimum jerk principle | |
| with signal-dependent noise and micro-corrections. | |
| Args: | |
| start_angle: Starting crosshair angle [x, y] in degrees | |
| target_angle: Target angle [x, y] in degrees | |
| skill_aim: Aim skill (0-100) | |
| skill_consistency: Consistency skill (0-100) | |
| duration_ms: Movement duration in milliseconds | |
| tick_rate: Ticks per second (CS2 uses 128) | |
| rng: Random generator | |
| Returns: | |
| Array of shape [n_ticks-1, 2] with delta (dx, dy) per tick | |
| """ | |
| if rng is None: | |
| rng = np.random.default_rng() | |
| start_angle = np.asarray(start_angle, dtype=np.float64) | |
| target_angle = np.asarray(target_angle, dtype=np.float64) | |
| n_ticks = max(2, int(duration_ms * tick_rate / 1000)) | |
| t = np.linspace(0, 1, n_ticks) | |
| # Minimum jerk profile | |
| s = minimum_jerk(t) | |
| # Planned trajectory | |
| displacement = target_angle - start_angle | |
| planned = start_angle[:, None] + displacement[:, None] * s # [2, n_ticks] | |
| # Velocity for signal-dependent noise (derivative of position) | |
| velocity = np.gradient(s) * np.linalg.norm(displacement) | |
| # Signal-dependent noise (Harris-Wolpert model) | |
| # Noise scale inversely proportional to skill | |
| noise_scale = (1.0 - skill_aim / 150.0) * 0.1 # 0.033 - 0.1 degrees | |
| noise_x = signal_dependent_noise(velocity, noise_scale, rng) | |
| noise_y = signal_dependent_noise(velocity, noise_scale, rng) | |
| noise = np.stack([noise_x, noise_y], axis=0) # [2, n_ticks] | |
| # Micro-corrections (human feedback control) | |
| correction_frequency = 3.0 + skill_consistency / 20.0 # 3-8 corrections | |
| correction_amplitude = 0.02 * (1.0 - skill_aim / 100.0) # Less corrections for skilled | |
| corrections = generate_micro_corrections(n_ticks, correction_frequency, correction_amplitude, rng) | |
| # Combine: planned + noise + corrections | |
| actual = planned + noise + corrections.T | |
| # Convert to deltas | |
| deltas = np.diff(actual, axis=1).T # [n_ticks-1, 2] | |
| return deltas | |
| def generate_aimbot_trajectory( | |
| natural_trajectory: np.ndarray, | |
| target_positions: np.ndarray, | |
| intensity: float, | |
| humanization: dict, | |
| rng: Optional[np.random.Generator] = None, | |
| ) -> np.ndarray: | |
| """ | |
| Modify natural trajectory with aimbot assistance. | |
| Even with humanization, leaves detectable artifacts: | |
| - Too smooth (lacks human micro-corrections) | |
| - Noise is too uniform | |
| - Delay is too consistent | |
| Args: | |
| natural_trajectory: Human trajectory [n_ticks, 2] | |
| target_positions: Target angle at each tick [n_ticks, 2] | |
| intensity: Cheat intensity (0-1) | |
| humanization: Dict with aim_smoothing, noise_amplitude, fov_degrees | |
| rng: Random generator | |
| Returns: | |
| Modified trajectory [n_ticks, 2] | |
| """ | |
| if rng is None: | |
| rng = np.random.default_rng() | |
| modified = natural_trajectory.copy() | |
| n_ticks = len(modified) | |
| # Cumulative positions from deltas | |
| positions = np.zeros((n_ticks + 1, 2)) | |
| positions[1:] = np.cumsum(modified, axis=0) | |
| fov_limit = humanization.get("fov_degrees", 90.0) | |
| smoothing = humanization.get("aim_smoothing", 0.5) | |
| noise_amp = humanization.get("noise_amplitude", 0.0) | |
| for i in range(n_ticks): | |
| current_pos = positions[i] | |
| target_pos = target_positions[i] | |
| correction_needed = target_pos - current_pos | |
| distance = np.linalg.norm(correction_needed) | |
| if distance < fov_limit: | |
| # Apply aimbot correction | |
| if smoothing > 0: | |
| # Humanized: exponential approach (ARTIFACT: too smooth) | |
| correction = correction_needed * (1.0 - smoothing) * intensity | |
| else: | |
| # Blatant: instant (ARTIFACT: infinite jerk) | |
| correction = correction_needed * intensity | |
| # Add humanization noise (ARTIFACT: noise is too uniform) | |
| if noise_amp > 0: | |
| correction += rng.normal(0, noise_amp, 2) | |
| modified[i] += correction | |
| # Update subsequent positions | |
| positions[i+1:] += correction | |
| return modified | |
| def fitts_law_time( | |
| distance: float, | |
| target_width: float, | |
| a: float = 0.0, | |
| b: float = 0.1, | |
| ) -> float: | |
| """ | |
| Calculate movement time using Fitts' Law. | |
| MT = a + b * log2(2D/W) | |
| Args: | |
| distance: Distance to target | |
| target_width: Width of target | |
| a: Intercept (reaction time offset) | |
| b: Slope (movement time per bit) | |
| Returns: | |
| Movement time in seconds | |
| """ | |
| if target_width <= 0: | |
| target_width = 1e-6 | |
| index_of_difficulty = np.log2(2.0 * distance / target_width + 1.0) | |
| return a + b * index_of_difficulty | |
| def extract_trajectory_features(trajectory: np.ndarray) -> dict: | |
| """ | |
| Extract features from trajectory for cheat detection. | |
| Args: | |
| trajectory: Delta trajectory [n_ticks, 2] | |
| Returns: | |
| Dict of extracted features | |
| """ | |
| if len(trajectory) < 3: | |
| return { | |
| "max_jerk": 0.0, | |
| "mean_jerk": 0.0, | |
| "jerk_variance": 0.0, | |
| "path_efficiency": 1.0, | |
| "velocity_peak_timing": 0.5, | |
| } | |
| # Compute velocity and acceleration | |
| velocity = np.linalg.norm(trajectory, axis=1) | |
| acceleration = np.diff(velocity) | |
| jerk = np.diff(acceleration) if len(acceleration) > 1 else np.array([0.0]) | |
| # Jerk statistics | |
| max_jerk = float(np.max(np.abs(jerk))) if len(jerk) > 0 else 0.0 | |
| mean_jerk = float(np.mean(np.abs(jerk))) if len(jerk) > 0 else 0.0 | |
| jerk_variance = float(np.var(jerk)) if len(jerk) > 0 else 0.0 | |
| # Path efficiency (actual distance / direct distance) | |
| total_distance = np.sum(velocity) | |
| direct_distance = np.linalg.norm(np.sum(trajectory, axis=0)) | |
| path_efficiency = direct_distance / (total_distance + 1e-8) | |
| # Velocity peak timing (should be ~0.5 for bell-shaped profile) | |
| if len(velocity) > 0: | |
| peak_idx = np.argmax(velocity) | |
| velocity_peak_timing = peak_idx / len(velocity) | |
| else: | |
| velocity_peak_timing = 0.5 | |
| return { | |
| "max_jerk": max_jerk, | |
| "mean_jerk": mean_jerk, | |
| "jerk_variance": jerk_variance, | |
| "path_efficiency": path_efficiency, | |
| "velocity_peak_timing": velocity_peak_timing, | |
| } | |