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) @dataclass 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) @dataclass 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)