Spaces:
Running
on
Zero
Running
on
Zero
| from __future__ import annotations | |
| import numpy as np | |
| from dataclasses import dataclass, field | |
| from typing import Optional, Dict, Any, List, Iterator | |
| import random | |
| from manifold.data.profiles import PlayerProfile, SkillVector, RANK_STATISTICS, generate_correlated_skills, Rank | |
| from manifold.data.cheats import CheatBehavior, CHEAT_PROFILES, CheatType | |
| from manifold.data.trajectories import ( | |
| generate_human_trajectory, | |
| generate_aimbot_trajectory, | |
| fitts_law_time, | |
| extract_trajectory_features, | |
| ) | |
| from manifold.data.temporal import SessionSimulator, SessionState | |
| # Extended trajectory feature names for 25 features | |
| TRAJECTORY_FEATURE_NAMES = [ | |
| "max_jerk", "mean_jerk", "jerk_variance", "jerk_skewness", "jerk_kurtosis", | |
| "path_efficiency", "velocity_peak_timing", "max_velocity", "mean_velocity", "velocity_variance", | |
| "max_acceleration", "mean_acceleration", "acceleration_variance", | |
| "total_distance", "direct_distance", "x_displacement", "y_displacement", | |
| "direction_changes", "smoothness_index", "curvature_mean", "curvature_variance", | |
| "overshoot_magnitude", "correction_count", "final_error", "movement_duration_ratio", | |
| ] | |
| def extract_extended_trajectory_features(trajectory: np.ndarray) -> Dict[str, float]: | |
| """ | |
| Extract 25 features from trajectory for cheat detection. | |
| Extends the base extract_trajectory_features with additional metrics. | |
| Args: | |
| trajectory: Delta trajectory [n_ticks, 2] | |
| Returns: | |
| Dict of 25 extracted features | |
| """ | |
| # Start with base features | |
| base_features = extract_trajectory_features(trajectory) | |
| if len(trajectory) < 3: | |
| # Return zeros for all features | |
| return {name: 0.0 for name in TRAJECTORY_FEATURE_NAMES} | |
| # Compute velocity, acceleration, jerk | |
| velocity = np.linalg.norm(trajectory, axis=1) | |
| acceleration = np.diff(velocity) if len(velocity) > 1 else np.array([0.0]) | |
| jerk = np.diff(acceleration) if len(acceleration) > 1 else np.array([0.0]) | |
| # Jerk statistics (extended) | |
| jerk_skewness = float(_safe_skewness(jerk)) if len(jerk) > 2 else 0.0 | |
| jerk_kurtosis = float(_safe_kurtosis(jerk)) if len(jerk) > 3 else 0.0 | |
| # Velocity statistics | |
| max_velocity = float(np.max(velocity)) if len(velocity) > 0 else 0.0 | |
| mean_velocity = float(np.mean(velocity)) if len(velocity) > 0 else 0.0 | |
| velocity_variance = float(np.var(velocity)) if len(velocity) > 0 else 0.0 | |
| # Acceleration statistics | |
| max_acceleration = float(np.max(np.abs(acceleration))) if len(acceleration) > 0 else 0.0 | |
| mean_acceleration = float(np.mean(np.abs(acceleration))) if len(acceleration) > 0 else 0.0 | |
| acceleration_variance = float(np.var(acceleration)) if len(acceleration) > 0 else 0.0 | |
| # Distance metrics | |
| total_distance = float(np.sum(velocity)) | |
| cumulative_displacement = np.cumsum(trajectory, axis=0) | |
| direct_distance = float(np.linalg.norm(cumulative_displacement[-1])) if len(cumulative_displacement) > 0 else 0.0 | |
| x_displacement = float(cumulative_displacement[-1, 0]) if len(cumulative_displacement) > 0 else 0.0 | |
| y_displacement = float(cumulative_displacement[-1, 1]) if len(cumulative_displacement) > 0 else 0.0 | |
| # Direction changes | |
| if len(trajectory) > 1: | |
| angles = np.arctan2(trajectory[:, 1], trajectory[:, 0]) | |
| angle_diffs = np.abs(np.diff(angles)) | |
| direction_changes = float(np.sum(angle_diffs > np.pi / 4)) # Count significant direction changes | |
| else: | |
| direction_changes = 0.0 | |
| # Smoothness index (inverse of total squared jerk) | |
| smoothness_index = 1.0 / (1.0 + np.sum(jerk**2)) if len(jerk) > 0 else 1.0 | |
| # Curvature statistics | |
| if len(trajectory) > 2: | |
| # Approximate curvature as angle change per unit distance | |
| curvatures = [] | |
| for i in range(1, len(trajectory) - 1): | |
| v1 = trajectory[i] | |
| v2 = trajectory[i + 1] | |
| cross = v1[0] * v2[1] - v1[1] * v2[0] | |
| norm_product = (np.linalg.norm(v1) * np.linalg.norm(v2)) + 1e-8 | |
| curvatures.append(abs(cross / norm_product)) | |
| curvature_mean = float(np.mean(curvatures)) if curvatures else 0.0 | |
| curvature_variance = float(np.var(curvatures)) if curvatures else 0.0 | |
| else: | |
| curvature_mean = 0.0 | |
| curvature_variance = 0.0 | |
| # Overshoot detection (when trajectory goes past target then comes back) | |
| positions = np.cumsum(trajectory, axis=0) | |
| if len(positions) > 1: | |
| final_pos = positions[-1] | |
| distances_to_final = np.linalg.norm(positions - final_pos, axis=1) | |
| # Overshoot = minimum distance was achieved before the end | |
| min_dist_idx = np.argmin(distances_to_final[:-1]) if len(distances_to_final) > 1 else 0 | |
| overshoot_magnitude = float(np.max(distances_to_final[min_dist_idx:])) if min_dist_idx < len(distances_to_final) - 1 else 0.0 | |
| else: | |
| overshoot_magnitude = 0.0 | |
| # Correction count (velocity sign changes) | |
| if len(velocity) > 1: | |
| vel_diff = np.diff(velocity) | |
| correction_count = float(np.sum(np.abs(np.diff(np.sign(vel_diff))) > 0)) if len(vel_diff) > 1 else 0.0 | |
| else: | |
| correction_count = 0.0 | |
| # Final error (assuming target is at cumulative endpoint - would need target info) | |
| # For now, use variance of final positions as proxy | |
| final_error = float(np.linalg.norm(trajectory[-1])) if len(trajectory) > 0 else 0.0 | |
| # Movement duration ratio (proportion of time with significant movement) | |
| movement_threshold = 0.01 * max_velocity if max_velocity > 0 else 0.01 | |
| movement_duration_ratio = float(np.mean(velocity > movement_threshold)) if len(velocity) > 0 else 0.0 | |
| return { | |
| "max_jerk": base_features["max_jerk"], | |
| "mean_jerk": base_features["mean_jerk"], | |
| "jerk_variance": base_features["jerk_variance"], | |
| "jerk_skewness": jerk_skewness, | |
| "jerk_kurtosis": jerk_kurtosis, | |
| "path_efficiency": base_features["path_efficiency"], | |
| "velocity_peak_timing": base_features["velocity_peak_timing"], | |
| "max_velocity": max_velocity, | |
| "mean_velocity": mean_velocity, | |
| "velocity_variance": velocity_variance, | |
| "max_acceleration": max_acceleration, | |
| "mean_acceleration": mean_acceleration, | |
| "acceleration_variance": acceleration_variance, | |
| "total_distance": total_distance, | |
| "direct_distance": direct_distance, | |
| "x_displacement": x_displacement, | |
| "y_displacement": y_displacement, | |
| "direction_changes": direction_changes, | |
| "smoothness_index": smoothness_index, | |
| "curvature_mean": curvature_mean, | |
| "curvature_variance": curvature_variance, | |
| "overshoot_magnitude": overshoot_magnitude, | |
| "correction_count": correction_count, | |
| "final_error": final_error, | |
| "movement_duration_ratio": movement_duration_ratio, | |
| } | |
| def _safe_skewness(arr: np.ndarray) -> float: | |
| """Compute skewness safely.""" | |
| if len(arr) < 3: | |
| return 0.0 | |
| mean = np.mean(arr) | |
| std = np.std(arr) | |
| if std < 1e-8: | |
| return 0.0 | |
| return float(np.mean(((arr - mean) / std) ** 3)) | |
| def _safe_kurtosis(arr: np.ndarray) -> float: | |
| """Compute kurtosis safely.""" | |
| if len(arr) < 4: | |
| return 0.0 | |
| mean = np.mean(arr) | |
| std = np.std(arr) | |
| if std < 1e-8: | |
| return 0.0 | |
| return float(np.mean(((arr - mean) / std) ** 4) - 3.0) | |
| class EngagementData: | |
| """Single engagement - the atomic training unit with 64 features.""" | |
| # Context features [12] | |
| enemy_distance: float | |
| enemy_velocity: float | |
| player_velocity: float | |
| player_health: float | |
| enemy_health: float | |
| weapon_type: int | |
| is_scoped: bool | |
| is_crouched: bool | |
| round_time_remaining: float | |
| score_differential: float | |
| is_clutch: bool | |
| enemies_alive: int | |
| # Pre-engagement features [8] | |
| crosshair_angle_to_hidden_enemy: float | |
| time_tracking_hidden_ms: float | |
| prefire_indicator: bool | |
| check_pattern_efficiency: float | |
| rotation_timing_vs_enemy: float | |
| flank_awareness_score: float | |
| info_advantage_score: float | |
| position_optimality: float | |
| # Trajectory features [25] - from extract_trajectory_features | |
| trajectory_features: Dict[str, float] = field(default_factory=dict) | |
| # Timing features [10] | |
| reaction_time_ms: float = 0.0 | |
| time_to_first_shot_ms: float = 0.0 | |
| time_to_damage_ms: float = 0.0 | |
| time_to_kill_ms: float = 0.0 | |
| shot_timing_variance: float = 0.0 | |
| inter_shot_interval_mean: float = 0.0 | |
| inter_shot_interval_cv: float = 0.0 | |
| crosshair_on_enemy_to_shot_ms: float = 0.0 | |
| anticipatory_shot_rate: float = 0.0 | |
| perfect_timing_rate: float = 0.0 | |
| # Accuracy features [9] | |
| shots_fired: int = 0 | |
| shots_hit: int = 0 | |
| headshots: int = 0 | |
| damage_dealt: float = 0.0 | |
| spray_accuracy: float = 0.0 | |
| first_bullet_accuracy: float = 0.0 | |
| headshot_rate: float = 0.0 | |
| damage_efficiency: float = 0.0 | |
| kill_secured: bool = False | |
| # Labels | |
| is_cheater: bool = False | |
| cheat_type: str = "none" | |
| cheat_intensity: float = 0.0 | |
| cheat_active_this_engagement: bool = False | |
| def to_tensor(self) -> np.ndarray: | |
| """Convert to 64-dim feature vector.""" | |
| # Context features [12] | |
| context = [ | |
| self.enemy_distance, | |
| self.enemy_velocity, | |
| self.player_velocity, | |
| self.player_health, | |
| self.enemy_health, | |
| float(self.weapon_type), | |
| float(self.is_scoped), | |
| float(self.is_crouched), | |
| self.round_time_remaining, | |
| self.score_differential, | |
| float(self.is_clutch), | |
| float(self.enemies_alive), | |
| ] | |
| # Pre-engagement features [8] | |
| pre_engagement = [ | |
| self.crosshair_angle_to_hidden_enemy, | |
| self.time_tracking_hidden_ms, | |
| float(self.prefire_indicator), | |
| self.check_pattern_efficiency, | |
| self.rotation_timing_vs_enemy, | |
| self.flank_awareness_score, | |
| self.info_advantage_score, | |
| self.position_optimality, | |
| ] | |
| # Trajectory features [25] | |
| trajectory = [ | |
| self.trajectory_features.get(name, 0.0) | |
| for name in TRAJECTORY_FEATURE_NAMES | |
| ] | |
| # Timing features [10] | |
| timing = [ | |
| self.reaction_time_ms, | |
| self.time_to_first_shot_ms, | |
| self.time_to_damage_ms, | |
| self.time_to_kill_ms, | |
| self.shot_timing_variance, | |
| self.inter_shot_interval_mean, | |
| self.inter_shot_interval_cv, | |
| self.crosshair_on_enemy_to_shot_ms, | |
| self.anticipatory_shot_rate, | |
| self.perfect_timing_rate, | |
| ] | |
| # Accuracy features [9] | |
| accuracy = [ | |
| float(self.shots_fired), | |
| float(self.shots_hit), | |
| float(self.headshots), | |
| self.damage_dealt, | |
| self.spray_accuracy, | |
| self.first_bullet_accuracy, | |
| self.headshot_rate, | |
| self.damage_efficiency, | |
| float(self.kill_secured), | |
| ] | |
| # Concatenate all: 12 + 8 + 25 + 10 + 9 = 64 | |
| features = context + pre_engagement + trajectory + timing + accuracy | |
| return np.array(features, dtype=np.float32) | |
| class PlayerSession: | |
| """Complete player session with multiple engagements.""" | |
| player_id: str | |
| profile: PlayerProfile | |
| engagements: List[EngagementData] | |
| is_cheater: bool | |
| cheat_profile: Optional[str] = None | |
| rank: str = "gold_nova" | |
| def to_tensor(self) -> np.ndarray: | |
| """Convert to tensor [num_engagements, 64].""" | |
| return np.stack([e.to_tensor() for e in self.engagements]) | |
| # Weapon type mapping | |
| WEAPON_TYPES = { | |
| "rifle": 0, | |
| "smg": 1, | |
| "pistol": 2, | |
| "awp": 3, | |
| "shotgun": 4, | |
| "machine_gun": 5, | |
| } | |
| class SyntheticDataGenerator: | |
| """ | |
| Generate synthetic CS2 player behavior data. | |
| Orchestrates all data modules to create realistic player sessions | |
| with proper skill modeling, cheat injection, and temporal dynamics. | |
| """ | |
| def __init__( | |
| self, | |
| seed: Optional[int] = None, | |
| engagements_per_session: int = 200, | |
| ): | |
| self.rng = np.random.default_rng(seed) | |
| self.engagements_per_session = engagements_per_session | |
| self._seed = seed | |
| def generate_player( | |
| self, | |
| is_cheater: bool = False, | |
| rank: Optional[str] = None, | |
| cheat_profile: Optional[str] = None, | |
| ) -> PlayerSession: | |
| """Generate a single player session.""" | |
| # 1. Create player profile with correlated skills | |
| if rank is None: | |
| rank = self.rng.choice(["silver", "gold_nova", "master_guardian", "legendary_eagle", "supreme_global"]) | |
| profile = PlayerProfile.generate( | |
| rank=rank, | |
| seed=int(self.rng.integers(0, 2**31)), | |
| ) | |
| profile.is_cheater = is_cheater | |
| # 2. Initialize session simulator for temporal dynamics | |
| session = SessionSimulator.from_skill( | |
| mental_resilience=profile.skill_vector.mental_resilience, | |
| seed=int(self.rng.integers(0, 2**31)), | |
| ) | |
| # 3. Setup cheat behavior if cheater | |
| cheat_behavior: Optional[CheatBehavior] = None | |
| if is_cheater: | |
| if cheat_profile is None: | |
| cheat_profile = self.rng.choice(list(CHEAT_PROFILES.keys())) | |
| cheat_behavior = CheatBehavior.from_profile( | |
| cheat_profile, | |
| seed=int(self.rng.integers(0, 2**31)), | |
| ) | |
| # 4. Generate engagements | |
| engagements: List[EngagementData] = [] | |
| rounds_per_match = 24 | |
| base_engagements_per_round = self.engagements_per_session // rounds_per_match | |
| extra_engagements = self.engagements_per_session % rounds_per_match | |
| score_differential = 0 | |
| for round_num in range(rounds_per_match): | |
| engagements_this_round = base_engagements_per_round + (1 if round_num < extra_engagements else 0) | |
| # Determine round context | |
| is_losing = score_differential < -3 | |
| round_won = self.rng.random() < 0.5 | |
| for eng_num in range(engagements_this_round): | |
| is_clutch = (eng_num == engagements_this_round - 1) and self.rng.random() < 0.2 | |
| # Update session state | |
| event = "idle" | |
| if eng_num == 0: | |
| event = "round_win" if round_won else "round_loss" | |
| elif self.rng.random() < 0.3: | |
| event = "kill" if self.rng.random() < 0.5 else "death" | |
| if is_clutch: | |
| session.update("clutch_situation", 1000.0) | |
| else: | |
| session.update(event, 3000.0) | |
| # Generate game context | |
| game_context = { | |
| "round_time_remaining": 115.0 - (eng_num * 10.0) + self.rng.uniform(-5, 5), | |
| "score_differential": score_differential, | |
| "is_clutch": is_clutch, | |
| "enemies_alive": max(1, 5 - eng_num // 2), | |
| "is_losing": is_losing, | |
| } | |
| # Determine if cheat is active this engagement | |
| cheat_active = False | |
| if cheat_behavior is not None: | |
| cheat_active = cheat_behavior.should_activate( | |
| is_clutch=is_clutch, | |
| is_losing=is_losing, | |
| round_number=round_num, | |
| rng=self.rng, | |
| ) | |
| cheat_behavior.is_active = cheat_active | |
| engagement = self.generate_engagement( | |
| profile=profile, | |
| session=session, | |
| cheat_behavior=cheat_behavior if cheat_active else None, | |
| game_context=game_context, | |
| ) | |
| # Set labels | |
| engagement.is_cheater = is_cheater | |
| engagement.cheat_type = cheat_profile if is_cheater else "none" | |
| engagement.cheat_intensity = cheat_behavior.config.intensity if cheat_behavior else 0.0 | |
| engagement.cheat_active_this_engagement = cheat_active | |
| engagements.append(engagement) | |
| # Update score | |
| if round_won: | |
| score_differential += 1 | |
| else: | |
| score_differential -= 1 | |
| return PlayerSession( | |
| player_id=profile.profile_id, | |
| profile=profile, | |
| engagements=engagements, | |
| is_cheater=is_cheater, | |
| cheat_profile=cheat_profile, | |
| rank=rank, | |
| ) | |
| def generate_engagement( | |
| self, | |
| profile: PlayerProfile, | |
| session: SessionSimulator, | |
| cheat_behavior: Optional[CheatBehavior] = None, | |
| game_context: Optional[Dict[str, Any]] = None, | |
| ) -> EngagementData: | |
| """Generate a single engagement.""" | |
| if game_context is None: | |
| game_context = {} | |
| skills = profile.skill_vector | |
| rank_stats = RANK_STATISTICS[profile.rank] | |
| # Get session modifiers | |
| modifiers = session.get_modifiers() | |
| # 1. Generate context features | |
| enemy_distance = self.rng.uniform(5.0, 50.0) # meters | |
| enemy_velocity = self.rng.uniform(0.0, 250.0) # units/s | |
| player_velocity = self.rng.uniform(0.0, 250.0) | |
| player_health = self.rng.uniform(20.0, 100.0) | |
| enemy_health = self.rng.uniform(20.0, 100.0) | |
| weapon_type = int(self.rng.integers(0, len(WEAPON_TYPES))) | |
| is_scoped = weapon_type == WEAPON_TYPES["awp"] and self.rng.random() < 0.7 | |
| is_crouched = self.rng.random() < 0.3 | |
| round_time_remaining = game_context.get("round_time_remaining", 90.0) | |
| score_differential = game_context.get("score_differential", 0) | |
| is_clutch = game_context.get("is_clutch", False) | |
| enemies_alive = game_context.get("enemies_alive", 3) | |
| # 2. Generate pre-engagement features | |
| # These are affected by game sense and wallhack cheats | |
| has_wallhack = ( | |
| cheat_behavior is not None and | |
| CheatType.WALLHACK in cheat_behavior.config.cheat_types | |
| ) | |
| game_sense_effective = skills.game_sense * modifiers.get("game_sense_mult", 1.0) | |
| # Crosshair angle to hidden enemy (wallhackers track better) | |
| if has_wallhack: | |
| crosshair_angle_to_hidden = self.rng.uniform(0.0, 15.0) # Suspiciously good | |
| time_tracking_hidden = self.rng.uniform(500.0, 2000.0) # Long tracking time | |
| prefire_indicator = self.rng.random() < (0.3 * (1.0 - cheat_behavior.config.humanization.get("prefire_suppression", 0.0))) | |
| else: | |
| crosshair_angle_to_hidden = self.rng.uniform(20.0, 90.0) * (1.0 - game_sense_effective / 150.0) | |
| time_tracking_hidden = self.rng.exponential(200.0) | |
| prefire_indicator = self.rng.random() < (0.05 * game_sense_effective / 100.0) | |
| check_pattern_efficiency = (game_sense_effective / 100.0) * self.rng.uniform(0.7, 1.0) | |
| rotation_timing_vs_enemy = self.rng.uniform(0.3, 1.0) * (0.5 + game_sense_effective / 200.0) | |
| flank_awareness_score = self.rng.uniform(0.2, 1.0) * (0.5 + game_sense_effective / 200.0) | |
| info_advantage_score = self.rng.uniform(0.0, 1.0) | |
| position_optimality = self.rng.uniform(0.3, 1.0) * (0.5 + game_sense_effective / 200.0) | |
| if has_wallhack: | |
| check_pattern_efficiency = min(1.0, check_pattern_efficiency * 1.3) | |
| rotation_timing_vs_enemy = min(1.0, rotation_timing_vs_enemy * 1.2) | |
| flank_awareness_score = min(1.0, flank_awareness_score * 1.4) | |
| # 3. Generate trajectory | |
| # Start and target angles | |
| start_angle = np.array([ | |
| self.rng.uniform(-30.0, 30.0), | |
| self.rng.uniform(-20.0, 20.0), | |
| ]) | |
| target_angle = np.array([ | |
| self.rng.uniform(-5.0, 5.0), | |
| self.rng.uniform(-3.0, 3.0), | |
| ]) | |
| # Movement time based on Fitts' law and skill | |
| target_width = 3.0 if weapon_type == WEAPON_TYPES["awp"] else 8.0 # Hitbox size in degrees | |
| distance = np.linalg.norm(target_angle - start_angle) | |
| base_movement_time = fitts_law_time(distance, target_width, a=0.1, b=0.15) | |
| skill_factor = (100.0 - skills.raw_aim) / 100.0 # Lower skill = longer time | |
| movement_time_s = base_movement_time * (1.0 + skill_factor * 0.5) * modifiers.get("reaction_time_mult", 1.0) | |
| movement_time_ms = movement_time_s * 1000.0 | |
| # Generate human trajectory | |
| trajectory = generate_human_trajectory( | |
| start_angle=start_angle, | |
| target_angle=target_angle, | |
| skill_aim=skills.raw_aim, | |
| skill_consistency=skills.consistency, | |
| duration_ms=movement_time_ms, | |
| tick_rate=128, | |
| rng=self.rng, | |
| ) | |
| # 4. Apply aimbot modification if active | |
| has_aimbot = ( | |
| cheat_behavior is not None and | |
| CheatType.AIMBOT in cheat_behavior.config.cheat_types | |
| ) | |
| if has_aimbot: | |
| n_ticks = len(trajectory) | |
| # Target positions (enemy moves slightly) | |
| target_positions = np.tile(target_angle, (n_ticks, 1)) | |
| target_positions += self.rng.normal(0, 0.5, (n_ticks, 2)) # Small enemy movement | |
| trajectory = generate_aimbot_trajectory( | |
| natural_trajectory=trajectory, | |
| target_positions=target_positions, | |
| intensity=cheat_behavior.config.intensity, | |
| humanization=cheat_behavior.config.humanization, | |
| rng=self.rng, | |
| ) | |
| # 5. Extract trajectory features | |
| trajectory_features = extract_extended_trajectory_features(trajectory) | |
| # 6. Compute timing features | |
| rt_low, rt_high = rank_stats["reaction_time_ms"] | |
| base_reaction_time = self.rng.uniform(rt_low, rt_high) | |
| # Apply session modifiers | |
| reaction_time_ms = base_reaction_time * modifiers.get("reaction_time_mult", 1.0) * modifiers.get("focus_reaction_mult", 1.0) | |
| # Aimbot affects reaction time (but adds artificial delay for humanization) | |
| if has_aimbot: | |
| cheat_delay = cheat_behavior.config.humanization.get("reaction_delay_ms", 0.0) | |
| reaction_time_ms = max(50.0, reaction_time_ms * 0.7 + cheat_delay) | |
| # Triggerbot affects time to first shot | |
| has_triggerbot = ( | |
| cheat_behavior is not None and | |
| CheatType.TRIGGERBOT in cheat_behavior.config.cheat_types | |
| ) | |
| time_to_first_shot_ms = reaction_time_ms + movement_time_ms * 0.5 | |
| if has_triggerbot: | |
| # Triggerbot fires instantly when crosshair is on target | |
| time_to_first_shot_ms *= 0.6 # Suspiciously fast | |
| time_to_damage_ms = time_to_first_shot_ms + self.rng.uniform(0, 100) | |
| time_to_kill_ms = time_to_damage_ms + self.rng.uniform(100, 500) | |
| shot_timing_variance = self.rng.uniform(10.0, 50.0) * (1.0 - skills.consistency / 150.0) | |
| if has_triggerbot: | |
| shot_timing_variance *= 0.3 # Too consistent | |
| inter_shot_interval_mean = 100.0 + self.rng.uniform(-20, 50) # ms | |
| inter_shot_interval_cv = self.rng.uniform(0.1, 0.4) * (1.0 - skills.consistency / 150.0) | |
| crosshair_on_enemy_to_shot_ms = self.rng.uniform(50, 200) * (1.0 - skills.reaction_speed / 150.0) | |
| if has_triggerbot: | |
| crosshair_on_enemy_to_shot_ms = self.rng.uniform(5, 30) # Inhuman speed | |
| anticipatory_shot_rate = 0.02 + skills.game_sense / 1000.0 | |
| if prefire_indicator: | |
| anticipatory_shot_rate += 0.1 | |
| perfect_timing_rate = skills.consistency / 200.0 | |
| if has_triggerbot: | |
| perfect_timing_rate = min(1.0, perfect_timing_rate * 2.0) | |
| # 7. Compute accuracy features | |
| base_accuracy = self.rng.uniform(*rank_stats["accuracy"]) | |
| accuracy = base_accuracy * modifiers.get("accuracy_mult", 1.0) | |
| if has_aimbot: | |
| # Aimbot improves accuracy but may intentionally miss | |
| if cheat_behavior.should_miss_intentionally(self.rng): | |
| accuracy *= 0.5 | |
| else: | |
| accuracy = min(1.0, accuracy + 0.3 * cheat_behavior.config.intensity) | |
| shots_fired = int(self.rng.integers(3, 15)) | |
| shots_hit = int(np.clip(shots_fired * accuracy * self.rng.uniform(0.8, 1.2), 0, shots_fired)) | |
| hs_low, hs_high = rank_stats["hs_percent"] | |
| hs_rate = self.rng.uniform(hs_low, hs_high) | |
| if has_aimbot: | |
| hs_rate = min(1.0, hs_rate + 0.2 * cheat_behavior.config.intensity) | |
| headshots = int(np.clip(shots_hit * hs_rate, 0, shots_hit)) | |
| damage_per_hit = 25.0 + headshots * 75.0 / max(1, shots_hit) # Headshots do more damage | |
| damage_dealt = shots_hit * damage_per_hit | |
| spray_accuracy = accuracy * self.rng.uniform(0.6, 1.0) * (0.5 + skills.spray_control / 200.0) | |
| first_bullet_accuracy = accuracy * self.rng.uniform(0.8, 1.2) * (0.5 + skills.crosshair_placement / 200.0) | |
| headshot_rate = headshots / max(1, shots_hit) | |
| damage_efficiency = damage_dealt / max(1, shots_fired * 100.0) | |
| kill_secured = damage_dealt >= enemy_health | |
| return EngagementData( | |
| # Context features | |
| enemy_distance=enemy_distance, | |
| enemy_velocity=enemy_velocity, | |
| player_velocity=player_velocity, | |
| player_health=player_health, | |
| enemy_health=enemy_health, | |
| weapon_type=weapon_type, | |
| is_scoped=is_scoped, | |
| is_crouched=is_crouched, | |
| round_time_remaining=round_time_remaining, | |
| score_differential=score_differential, | |
| is_clutch=is_clutch, | |
| enemies_alive=enemies_alive, | |
| # Pre-engagement features | |
| crosshair_angle_to_hidden_enemy=crosshair_angle_to_hidden, | |
| time_tracking_hidden_ms=time_tracking_hidden, | |
| prefire_indicator=prefire_indicator, | |
| check_pattern_efficiency=check_pattern_efficiency, | |
| rotation_timing_vs_enemy=rotation_timing_vs_enemy, | |
| flank_awareness_score=flank_awareness_score, | |
| info_advantage_score=info_advantage_score, | |
| position_optimality=position_optimality, | |
| # Trajectory features | |
| trajectory_features=trajectory_features, | |
| # Timing features | |
| reaction_time_ms=reaction_time_ms, | |
| time_to_first_shot_ms=time_to_first_shot_ms, | |
| time_to_damage_ms=time_to_damage_ms, | |
| time_to_kill_ms=time_to_kill_ms, | |
| shot_timing_variance=shot_timing_variance, | |
| inter_shot_interval_mean=inter_shot_interval_mean, | |
| inter_shot_interval_cv=inter_shot_interval_cv, | |
| crosshair_on_enemy_to_shot_ms=crosshair_on_enemy_to_shot_ms, | |
| anticipatory_shot_rate=anticipatory_shot_rate, | |
| perfect_timing_rate=perfect_timing_rate, | |
| # Accuracy features | |
| shots_fired=shots_fired, | |
| shots_hit=shots_hit, | |
| headshots=headshots, | |
| damage_dealt=damage_dealt, | |
| spray_accuracy=spray_accuracy, | |
| first_bullet_accuracy=first_bullet_accuracy, | |
| headshot_rate=headshot_rate, | |
| damage_efficiency=damage_efficiency, | |
| kill_secured=kill_secured, | |
| ) | |
| def generate_batch( | |
| self, | |
| num_legit: int, | |
| num_cheaters: int, | |
| cheater_distribution: Optional[Dict[str, float]] = None, | |
| ) -> List[PlayerSession]: | |
| """Generate batch of player sessions.""" | |
| if cheater_distribution is None: | |
| # Default distribution across cheat profiles | |
| cheater_distribution = { | |
| "blatant_rage": 0.1, | |
| "obvious": 0.2, | |
| "closet_moderate": 0.3, | |
| "closet_subtle": 0.3, | |
| "wallhack_only": 0.1, | |
| } | |
| sessions: List[PlayerSession] = [] | |
| # Generate legit players | |
| for _ in range(num_legit): | |
| sessions.append(self.generate_player(is_cheater=False)) | |
| # Generate cheaters with distribution | |
| cheat_profiles = list(cheater_distribution.keys()) | |
| cheat_probs = list(cheater_distribution.values()) | |
| for _ in range(num_cheaters): | |
| profile = self.rng.choice(cheat_profiles, p=cheat_probs) | |
| sessions.append(self.generate_player(is_cheater=True, cheat_profile=profile)) | |
| # Shuffle | |
| self.rng.shuffle(sessions) | |
| return sessions | |
| def generate_stream( | |
| self, | |
| num_legit: int, | |
| num_cheaters: int, | |
| ) -> Iterator[PlayerSession]: | |
| """Memory-efficient streaming generator.""" | |
| total = num_legit + num_cheaters | |
| # Create shuffled indices for legit vs cheater | |
| is_cheater_flags = [False] * num_legit + [True] * num_cheaters | |
| self.rng.shuffle(is_cheater_flags) | |
| for is_cheater in is_cheater_flags: | |
| yield self.generate_player(is_cheater=is_cheater) | |