# Fichier: python_cheats/cheatsheets/random.txt
# Cheatsheet Module Random Python - Guide Complet


[OK] IMPORT & BASICS

import random

# Importer des fonctions spécifiques
from random import randint, choice, shuffle

# Importer tout (déconseillé)
from random import *

# Seed pour reproductibilité
random.seed(42)                    # Seed avec entier
random.seed("texte")               # Seed avec string
random.seed()                      # Seed avec temps système (par défaut)

# Obtenir l'état interne
state = random.getstate()          # Sauvegarder l'état
random.setstate(state)             # Restaurer l'état

# Version du générateur
print(random.Random().getrandbits(1).__doc__)


[OK] NOMBRES ALÉATOIRES ENTIERS

# === random.randint(a, b) ===
# Retourne entier aléatoire N tel que a <= N <= b (inclusif des deux côtés)

random.randint(1, 10)              # Entre 1 et 10 inclus
random.randint(0, 100)             # Entre 0 et 100
random.randint(-10, 10)            # Entre -10 et 10
random.randint(5, 5)               # Toujours 5

# Exemples pratiques
dice = random.randint(1, 6)        # Lancer de dé
age = random.randint(18, 100)      # Âge aléatoire
year = random.randint(1900, 2024)  # Année aléatoire


# === random.randrange(start, stop[, step]) ===
# Retourne élément aléatoire de range(start, stop, step)
# ATTENTION: stop est EXCLU (contrairement à randint)

random.randrange(10)               # 0 à 9 (équivalent à randint(0, 9))
random.randrange(1, 10)            # 1 à 9 (équivalent à randint(1, 9))
random.randrange(0, 100, 5)        # 0, 5, 10, 15, ..., 95
random.randrange(0, 100, 10)       # 0, 10, 20, ..., 90
random.randrange(10, 100, 2)       # Nombres pairs de 10 à 98

# Exemples pratiques
even_number = random.randrange(0, 100, 2)      # Nombre pair
odd_number = random.randrange(1, 100, 2)       # Nombre impair
multiple_of_5 = random.randrange(0, 100, 5)    # Multiple de 5


# === random.getrandbits(k) ===
# Retourne entier avec k bits aléatoires

random.getrandbits(1)              # 0 ou 1
random.getrandbits(8)              # 0 à 255 (8 bits)
random.getrandbits(16)             # 0 à 65535 (16 bits)
random.getrandbits(32)             # Entier 32 bits
random.getrandbits(64)             # Entier 64 bits

# Exemples pratiques
byte_value = random.getrandbits(8)             # Valeur byte
binary_choice = bool(random.getrandbits(1))    # True/False
hex_color = f"#{random.getrandbits(24):06x}"   # Couleur hex


[OK] NOMBRES ALÉATOIRES FLOTTANTS

# === random.random() ===
# Retourne float aléatoire dans [0.0, 1.0) - 0 inclus, 1 exclu

random.random()                    # 0.0 <= x < 1.0
random.random() * 100              # 0.0 <= x < 100.0
random.random() * 10 + 5           # 5.0 <= x < 15.0

# Exemples pratiques
probability = random.random()      # Probabilité 0-1
percentage = random.random() * 100 # Pourcentage 0-100


# === random.uniform(a, b) ===
# Retourne float aléatoire N tel que a <= N <= b (ou b <= N <= a si b < a)

random.uniform(0, 1)               # Équivalent à random()
random.uniform(1.5, 10.5)          # Entre 1.5 et 10.5
random.uniform(-5, 5)              # Entre -5 et 5
random.uniform(10, 1)              # Fonctionne aussi (entre 1 et 10)

# Exemples pratiques
temperature = random.uniform(-10.0, 40.0)      # Température en °C
price = random.uniform(9.99, 99.99)            # Prix
weight = random.uniform(50.0, 100.0)           # Poids en kg
latitude = random.uniform(-90.0, 90.0)         # Latitude
longitude = random.uniform(-180.0, 180.0)      # Longitude


# === random.triangular(low, high, mode) ===
# Distribution triangulaire (plus de valeurs près du mode)

random.triangular(0, 10)           # Mode au milieu (5)
random.triangular(0, 10, 2)        # Mode à 2 (plus de valeurs vers 2)
random.triangular(0, 10, 8)        # Mode à 8 (plus de valeurs vers 8)

# Exemples pratiques
# Simulation temps de réponse (souvent autour de 100ms)
response_time = random.triangular(50, 200, 100)

# Note satisfaction client (souvent haute)
satisfaction = random.triangular(1, 5, 4.5)


[OK] SÉLECTION ALÉATOIRE DANS SÉQUENCES

# === random.choice(seq) ===
# Retourne élément aléatoire d'une séquence non-vide

# Listes
colors = ["rouge", "vert", "bleu", "jaune"]
random.choice(colors)              # Un des 4 couleurs

# Tuples
dice_faces = (1, 2, 3, 4, 5, 6)
random.choice(dice_faces)          # 1 à 6

# Strings
random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ")    # Lettre majuscule
random.choice("0123456789")                     # Chiffre

# Range
random.choice(range(1, 101))       # 1 à 100

# Exemples pratiques
suits = ["[BLACK_SPADE_SUIT]", "[BLACK_HEART_SUIT]", "[BLACK_DIAMOND_SUIT]", "[BLACK_CLUB_SUIT]"]
card_suit = random.choice(suits)   # Couleur de carte

answers = ["Oui", "Non", "Peut-être"]
answer = random.choice(answers)    # Réponse aléatoire

directions = ["Nord", "Sud", "Est", "Ouest"]
direction = random.choice(directions)


# === random.choices(population, weights=None, k=1) ===
# Retourne k éléments (AVEC remise, peut avoir doublons)
# weights: pondération optionnelle

# Sans pondération (équiprobable)
random.choices([1, 2, 3, 4, 5], k=3)           # 3 nombres (peut avoir doublons)
random.choices("ABCD", k=5)                    # 5 lettres

# Avec pondération
random.choices(["A", "B", "C"], weights=[5, 2, 1], k=10)
# A: 5/8 de chances, B: 2/8, C: 1/8

random.choices([1, 2, 3], weights=[0.5, 0.3, 0.2], k=100)
# Proportions: 50% de 1, 30% de 2, 20% de 3

# Avec cum_weights (poids cumulatifs)
random.choices(["A", "B", "C"], cum_weights=[5, 7, 8], k=10)
# Équivalent à weights=[5, 2, 1]

# Exemples pratiques
# Tirer 5 cartes AVEC remise
cards = random.choices(range(1, 53), k=5)

# Générer mot de passe de 12 caractères
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%"
password = "".join(random.choices(chars, k=12))

# Simulation dé biaisé (6 sort plus souvent)
biased_dice = random.choices([1, 2, 3, 4, 5, 6], weights=[1, 1, 1, 1, 1, 3], k=100)

# Génération données test avec distribution réaliste
grades = random.choices(["A", "B", "C", "D", "F"], weights=[10, 20, 40, 20, 10], k=100)


# === random.sample(population, k) ===
# Retourne k éléments uniques (SANS remise, pas de doublons)
# ERREUR si k > len(population)

# Échantillon sans remise
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
random.sample(numbers, 5)          # 5 nombres différents

# Mélange partiel
deck = list(range(1, 53))
hand = random.sample(deck, 5)      # 5 cartes différentes

# Sélection aléatoire
students = ["Alice", "Bob", "Charlie", "David", "Eve"]
selected = random.sample(students, 3)    # 3 étudiants différents

# Avec range (efficace mémoire)
random.sample(range(1000000), 10)  # 10 nombres parmi 1 million

# Exemples pratiques
# Loterie: tirer 6 numéros parmi 49
lottery_numbers = random.sample(range(1, 50), 6)

# Sélection équipe
team = random.sample(players_list, 11)

# Questions quiz aléatoires
quiz_questions = random.sample(all_questions, 10)

# Échantillonnage données
data_sample = random.sample(full_dataset, 1000)


[OK] MÉLANGE & PERMUTATION

# === random.shuffle(x) ===
# Mélange liste EN PLACE (modifie la liste originale)
# Retourne None

# Mélanger liste
deck = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
random.shuffle(deck)               # deck est modifié
print(deck)                        # Ordre aléatoire

# Mélanger plusieurs fois
for _ in range(5):
    random.shuffle(deck)

# ATTENTION: shuffle ne fonctionne que sur des listes mutables
# random.shuffle((1, 2, 3))        # ERREUR: tuple immutable
# random.shuffle("abc")            # ERREUR: string immutable

# Convertir pour mélanger
tuple_data = (1, 2, 3, 4, 5)
list_data = list(tuple_data)
random.shuffle(list_data)
tuple_data = tuple(list_data)

# Exemples pratiques
# Mélanger cartes
cards = list(range(1, 53))
random.shuffle(cards)

# Ordre aléatoire questions
questions = ["Q1", "Q2", "Q3", "Q4", "Q5"]
random.shuffle(questions)

# Mélanger playlist
playlist = ["song1.mp3", "song2.mp3", "song3.mp3"]
random.shuffle(playlist)


[OK] DISTRIBUTIONS STATISTIQUES

# === random.gauss(mu, sigma) ===
# Distribution gaussienne (normale)
# mu: moyenne, sigma: écart-type

random.gauss(0, 1)                 # Distribution normale standard
random.gauss(100, 15)              # QI (moyenne 100, écart-type 15)
random.gauss(170, 10)              # Taille en cm

# Exemples pratiques
# Générer notes d'examen (moyenne 70, écart-type 10)
grades = [random.gauss(70, 10) for _ in range(100)]

# Temps de réponse serveur (moyenne 200ms, écart-type 50ms)
response_times = [max(0, random.gauss(200, 50)) for _ in range(1000)]

# Tailles population (moyenne 175cm, écart-type 8cm)
heights = [random.gauss(175, 8) for _ in range(1000)]


# === random.normalvariate(mu, sigma) ===
# Alternative à gauss() (légèrement plus lente mais plus précise)

random.normalvariate(0, 1)
random.normalvariate(100, 15)


# === random.lognormvariate(mu, sigma) ===
# Distribution log-normale (toujours positive)

random.lognormvariate(0, 1)        # Revenu, tailles de fichiers

# Exemples pratiques
# Revenus (souvent distribution log-normale)
incomes = [random.lognormvariate(10, 0.5) for _ in range(1000)]

# Tailles de fichiers
file_sizes = [random.lognormvariate(10, 2) for _ in range(100)]


# === random.expovariate(lambd) ===
# Distribution exponentielle
# lambd: 1/moyenne

random.expovariate(1.0)            # Moyenne = 1
random.expovariate(0.5)            # Moyenne = 2
random.expovariate(2.0)            # Moyenne = 0.5

# Exemples pratiques
# Temps entre arrivées clients (moyenne 5 minutes)
inter_arrival = random.expovariate(1/5)

# Durée de vie composant (moyenne 1000 heures)
lifetime = random.expovariate(1/1000)

# Temps entre événements rares
wait_time = random.expovariate(0.1)


# === random.gammavariate(alpha, beta) ===
# Distribution gamma

random.gammavariate(2, 2)


# === random.betavariate(alpha, beta) ===
# Distribution beta (entre 0 et 1)

random.betavariate(2, 5)           # Valeurs entre 0 et 1
random.betavariate(0.5, 0.5)       # Distribution en U

# Exemples pratiques
# Taux de conversion (souvent distribution beta)
conversion_rate = random.betavariate(2, 8)    # Autour de 20%

# Probabilités bayésiennes
prior = random.betavariate(5, 5)


# === random.paretovariate(alpha) ===
# Distribution de Pareto (principe 80/20)

random.paretovariate(1)            # Minimum = 1
random.paretovariate(2)            # Plus concentré vers 1

# Exemples pratiques
# Distribution richesse (principe de Pareto)
wealth = [random.paretovariate(1.5) for _ in range(1000)]

# Tailles de villes
city_sizes = [random.paretovariate(2) * 10000 for _ in range(100)]


# === random.weibullvariate(alpha, beta) ===
# Distribution de Weibull

random.weibullvariate(1, 1.5)


# === random.vonmisesvariate(mu, kappa) ===
# Distribution circulaire de von Mises
# Pour angles, directions

import math
random.vonmisesvariate(0, 4)       # Angle en radians
angle_degrees = math.degrees(random.vonmisesvariate(0, 4))


[OK] GÉNÉRATEURS ALTERNATIFS

# === random.Random() ===
# Créer instance séparée (avec son propre état)

# Instance par défaut
default_rng = random.Random()
default_rng.randint(1, 10)

# Instances avec seeds différents
rng1 = random.Random(42)
rng2 = random.Random(123)

print(rng1.randint(1, 100))        # Séquence reproductible 1
print(rng2.randint(1, 100))        # Séquence reproductible 2

# Exemples pratiques
# Générateurs séparés pour différents aspects d'un jeu
enemy_rng = random.Random(42)      # Génération ennemis
loot_rng = random.Random(123)      # Génération loot
terrain_rng = random.Random(456)   # Génération terrain


# === random.SystemRandom() ===
# Utilise os.urandom() - cryptographiquement sécurisé
# Ne peut PAS utiliser seed() - vraiment aléatoire

secure_rng = random.SystemRandom()
secure_rng.randint(1, 10)
secure_rng.choice([1, 2, 3])

# Exemples pratiques
# Génération token sécurisé
import random
secure = random.SystemRandom()
token = "".join(secure.choices("0123456789abcdef", k=32))

# Sélection sécurisée
secure_choice = secure.choice(["option1", "option2", "option3"])


[OK] EXEMPLES PRATIQUES COMPLETS

# === 1. Générateur de mots de passe ===

def generate_password(length=12, use_special=True):
    """Génère mot de passe aléatoire sécurisé"""
    import random
    import string
    
    chars = string.ascii_letters + string.digits
    if use_special:
        chars += "!@#$%^&*"
    
    # Garantir au moins 1 lettre, 1 chiffre, 1 spécial
    password = [
        random.choice(string.ascii_lowercase),
        random.choice(string.ascii_uppercase),
        random.choice(string.digits),
    ]
    
    if use_special:
        password.append(random.choice("!@#$%^&*"))
    
    # Remplir le reste
    password += random.choices(chars, k=length - len(password))
    
    # Mélanger
    random.shuffle(password)
    
    return "".join(password)

# Utilisation
password = generate_password(16)


# === 2. Simulation lancer de dés ===

def roll_dice(num_dice=2, num_sides=6):
    """Simule lancer de dés"""
    rolls = [random.randint(1, num_sides) for _ in range(num_dice)]
    return sum(rolls), rolls

# Utilisation
total, individual = roll_dice(2, 6)    # 2 dés à 6 faces
print(f"Total: {total}, Dés: {individual}")

# Statistiques sur 1000 lancers
results = [roll_dice()[0] for _ in range(1000)]
print(f"Moyenne: {sum(results)/len(results):.2f}")


# === 3. Tirage loterie ===

def lottery_draw(num_balls=6, max_number=49):
    """Tire numéros de loterie"""
    numbers = random.sample(range(1, max_number + 1), num_balls)
    return sorted(numbers)

# Utilisation
winning_numbers = lottery_draw()
print(f"Numéros gagnants: {winning_numbers}")


# === 4. Mélangeur de cartes ===

class Deck:
    """Jeu de cartes"""
    
    def __init__(self):
        suits = ["[BLACK_SPADE_SUIT]", "[BLACK_HEART_SUIT]", "[BLACK_DIAMOND_SUIT]", "[BLACK_CLUB_SUIT]"]
        ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
        self.cards = [f"{rank}{suit}" for suit in suits for rank in ranks]
        self.shuffle()
    
    def shuffle(self):
        random.shuffle(self.cards)
    
    def deal(self, num_cards=1):
        dealt = self.cards[:num_cards]
        self.cards = self.cards[num_cards:]
        return dealt
    
    def reset(self):
        self.__init__()

# Utilisation
deck = Deck()
hand = deck.deal(5)
print(f"Main: {hand}")


# === 5. Générateur de données test ===

def generate_test_data(num_records=100):
    """Génère données de test réalistes"""
    import random
    
    first_names = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
    last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones"]
    cities = ["Paris", "Lyon", "Marseille", "Toulouse", "Nice"]
    
    data = []
    for _ in range(num_records):
        record = {
            "id": random.randint(1000, 9999),
            "name": f"{random.choice(first_names)} {random.choice(last_names)}",
            "age": random.randint(18, 80),
            "city": random.choice(cities),
            "salary": round(random.uniform(30000, 150000), 2),
            "score": round(random.gauss(75, 10), 1),
            "active": random.choice([True, False]),
        }
        data.append(record)
    
    return data

# Utilisation
test_data = generate_test_data(10)


# === 6. Sélecteur aléatoire pondéré ===

def weighted_random_choice(items, weights):
    """Sélection avec pondération personnalisée"""
    return random.choices(items, weights=weights, k=1)[0]

# Utilisation
monsters = ["Goblin", "Orc", "Dragon"]
spawn_weights = [70, 25, 5]  # Dragon rare

monster = weighted_random_choice(monsters, spawn_weights)


# === 7. Générateur de nombres uniques ===

def generate_unique_numbers(count, min_val, max_val):
    """Génère liste de nombres uniques"""
    if count > (max_val - min_val + 1):
        raise ValueError("Pas assez de nombres disponibles")
    
    return random.sample(range(min_val, max_val + 1), count)

# Utilisation
unique_ids = generate_unique_numbers(100, 1000, 9999)


# === 8. Simulation Monte Carlo simple ===

def estimate_pi(num_samples=100000):
    """Estime π avec méthode Monte Carlo"""
    inside_circle = 0
    
    for _ in range(num_samples):
        x = random.uniform(-1, 1)
        y = random.uniform(-1, 1)
        
        if x*x + y*y <= 1:
            inside_circle += 1
    
    return 4 * inside_circle / num_samples

# Utilisation
pi_estimate = estimate_pi(1000000)
print(f"Estimation de π: {pi_estimate}")


# === 9. Générateur de texte Lorem Ipsum ===

def generate_lorem_ipsum(num_words=50):
    """Génère texte Lorem Ipsum aléatoire"""
    words = ["lorem", "ipsum", "dolor", "sit", "amet", "consectetur",
             "adipiscing", "elit", "sed", "do", "eiusmod", "tempor"]
    
    result = []
    for i in range(num_words):
        word = random.choice(words)
        if i == 0:
            word = word.capitalize()
        result.append(word)
        
        # Ponctuation aléatoire
        if random.random() < 0.15:
            result[-1] += "."
    
    return " ".join(result)


# === 10. Simulation file d'attente ===

def simulate_queue(num_customers=100, avg_arrival=2, avg_service=5):
    """Simule file d'attente avec arrivées/services aléatoires"""
    import random
    
    current_time = 0
    wait_times = []
    
    for _ in range(num_customers):
        # Temps entre arrivées
        arrival_time = random.expovariate(1/avg_arrival)
        current_time += arrival_time
        
        # Temps de service
        service_time = random.expovariate(1/avg_service)
        
        wait_times.append(service_time)
    
    avg_wait = sum(wait_times) / len(wait_times)
    return avg_wait

# Utilisation
avg_wait_time = simulate_queue(1000)


[OK] SEED & REPRODUCTIBILITÉ

# === Importance du seed ===

# Sans seed - résultats différents à chaque exécution
print(random.randint(1, 100))      # Ex: 42
print(random.randint(1, 100))      # Ex: 89

# Avec seed - résultats identiques
random.seed(42)
print(random.randint(1, 100))      # Toujours le même
print(random.randint(1, 100))      # Toujours le même

random.seed(42)                    # Réinitialiser
print(random.randint(1, 100))      # Même résultat qu'avant


# === Sauvegarder/restaurer état ===

# Sauvegarder état
state = random.getstate()
value1 = random.randint(1, 100)
value2 = random.randint(1, 100)

# Restaurer état
random.setstate(state)
value3 = random.randint(1, 100)    # Identique à value1
value4 = random.randint(1, 100)    # Identique à value2


# === Seed pour tests unitaires ===

import random

def test_my_function():
    random.seed(42)                # Résultats prévisibles
    result = my_random_function()
    assert result == expected_value


# === Seed pour machine learning ===

import random
import numpy as np

def set_all_seeds(seed=42):
    """Initialise tous les générateurs pour reproductibilité"""
    random.seed(seed)
    np.random.seed(seed)
    # torch.manual_seed(seed) si PyTorch
    # tf.random.set_seed(seed) si TensorFlow


[OK] SECRETS MODULE (CRYPTOGRAPHIQUEMENT SÉCURISÉ)

# Pour génération tokens, mots de passe, clés de sécurité
# Utiliser secrets au lieu de random

import secrets

# Entiers aléatoires sécurisés
secrets.randbelow(100)             # 0 à 99
secrets.randbits(64)               # Entier 64 bits

# Choix sécurisé
secrets.choice([1, 2, 3, 4, 5])

# Token hexadécimal
secrets.token_hex(16)              # 32 caractères hex

# Token URL-safe
secrets.token_urlsafe(32)          # Token pour URLs

# Token bytes
secrets.token_bytes(32)            # 32 bytes

# Comparaison sécurisée (contre timing attacks)
secrets.compare_digest(a, b)


# Exemples pratiques avec secrets
def generate_secure_password(length=16):
    import secrets
    import string
    
    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
    password = "".join(secrets.choice(alphabet) for _ in range(length))
    return password

def generate_api_key():
    import secrets
    return secrets.token_urlsafe(32)

def generate_session_token():
    import secrets
    return secrets.token_hex(32)


[OK] NUMPY.RANDOM (POUR CALCUL SCIENTIFIQUE)

# NumPy offre générateurs plus rapides et plus de distributions
import numpy as np

# Nouveau générateur (NumPy >= 1.17)
rng = np.random.default_rng(seed=42)

# Entiers
rng.integers(0, 10, size=5)        # 5 entiers

# Flottants
rng.random(size=(3, 3))            # Matrice 3x3

# Distribution normale
rng.normal(loc=0, scale=1, size=100)

# Choix
rng.choice([1, 2, 3, 4, 5], size=3)

# Mélange
arr = np.array([1, 2, 3, 4, 5])
rng.shuffle(arr)

# Distribution uniforme
rng.uniform(0, 10, size=100)


[OK] PIÈGES & BONNES PRATIQUES

# [X] MAUVAIS: sample() avec k trop grand
my_list = [1, 2, 3]
random.sample(my_list, 5)            # ERREUR: ValueError

# [OK] BON: Vérifier taille
k = min(5, len(my_list))
random.sample(my_list, k)


# [X] MAUVAIS: Utiliser choice sur séquence vide
random.choice([])                    # ERREUR: IndexError

# [OK] BON: Vérifier si non vide
my_list = []
if my_list:
    choice = random.choice(my_list)
else:
    choice = None


# [X] MAUVAIS: Shuffle sur tuple/string
random.shuffle("abc")                # ERREUR: immutable
random.shuffle((1, 2, 3))            # ERREUR: immutable

# [OK] BON: Convertir en liste
s = "abc"
s_list = list(s)
random.shuffle(s_list)
s = "".join(s_list)


[OK] PERFORMANCE & OPTIMISATION

# === Comparaison performance ===

import timeit

# randint vs randrange
timeit.timeit("random.randint(1, 100)", setup="import random", number=100000)
timeit.timeit("random.randrange(1, 101)", setup="import random", number=100000)
# randrange légèrement plus rapide

# choice vs choices (1 élément)
timeit.timeit("random.choice(range(100))", setup="import random", number=100000)
timeit.timeit("random.choices(range(100), k=1)", setup="import random", number=100000)
# choice plus rapide pour 1 élément


# === Optimisations ===

# [X] LENT: Appels répétés
result = []
for _ in range(1000):
    result.append(random.randint(1, 100))

# [OK] RAPIDE: Utiliser choices
result = random.choices(range(1, 101), k=1000)

# [OK] PLUS RAPIDE: Utiliser NumPy pour gros volumes
import numpy as np
rng = np.random.default_rng()
result = rng.integers(1, 101, size=1000)


# === Pré-calculer pour performance ===

# [X] LENT: Recalculer à chaque fois
def get_random_enemy():
    enemies = ["Goblin", "Orc", "Troll", "Dragon"]
    weights = [50, 30, 15, 5]
    return random.choices(enemies, weights=weights, k=1)[0]

# [OK] RAPIDE: Pré-calculer
ENEMIES = ["Goblin", "Orc", "Troll", "Dragon"]
ENEMY_WEIGHTS = [50, 30, 15, 5]

def get_random_enemy():
    return random.choices(ENEMIES, weights=ENEMY_WEIGHTS, k=1)[0]


[OK] CAS D'USAGE AVANCÉS

# === 1. Génération procédurale de terrain ===

def generate_terrain(width, height, seed=None):
    """Génère terrain procédural avec seed pour reproductibilité"""
    if seed is not None:
        random.seed(seed)
    
    terrain = []
    for y in range(height):
        row = []
        for x in range(width):
            # Biais basé sur position
            altitude_bias = 0.5 + (y / height) * 0.3
            
            if random.random() < altitude_bias:
                tile = random.choices(
                    ["grass", "forest", "mountain"],
                    weights=[50, 30, 20],
                    k=1
                )[0]
            else:
                tile = "water"
            
            row.append(tile)
        terrain.append(row)
    
    return terrain

# Utilisation
map1 = generate_terrain(50, 50, seed=42)     # Toujours même carte
map2 = generate_terrain(50, 50, seed=42)     # Identique à map1
map3 = generate_terrain(50, 50, seed=123)    # Carte différente


# === 2. Système de butin (loot) ===

class LootTable:
    """Système de loot avec raretés"""
    
    def __init__(self):
        self.items = {
            "common": ["Potion", "Bread", "Arrow"],
            "uncommon": ["Steel Sword", "Iron Armor", "Scroll"],
            "rare": ["Magic Ring", "Enchanted Bow"],
            "legendary": ["Dragon Sword", "Phoenix Feather"]
        }
        
        self.rarity_weights = {
            "common": 60,
            "uncommon": 30,
            "rare": 9,
            "legendary": 1
        }
    
    def drop_item(self):
        """Tire un item aléatoire"""
        rarity = random.choices(
            list(self.rarity_weights.keys()),
            weights=list(self.rarity_weights.values()),
            k=1
        )[0]
        
        item = random.choice(self.items[rarity])
        return item, rarity
    
    def drop_multiple(self, num_drops):
        """Tire plusieurs items"""
        return [self.drop_item() for _ in range(num_drops)]

# Utilisation
loot = LootTable()
item, rarity = loot.drop_item()
print(f"Trouvé: {item} ({rarity})")

treasure = loot.drop_multiple(5)


# === 3. Génération de noms aléatoires ===

class NameGenerator:
    """Génère noms procéduraux"""
    
    def __init__(self):
        self.prefixes = ["Kal", "Mor", "Dar", "Zal", "Tor"]
        self.middles = ["an", "en", "on", "ar", "or"]
        self.suffixes = ["dor", "gor", "mir", "thul", "rak"]
    
    def generate_name(self, num_parts=2):
        """Génère nom aléatoire"""
        if num_parts == 2:
            return random.choice(self.prefixes) + random.choice(self.suffixes)
        else:
            return (random.choice(self.prefixes) + 
                   random.choice(self.middles) + 
                   random.choice(self.suffixes))
    
    def generate_batch(self, count=10):
        """Génère plusieurs noms"""
        return [self.generate_name() for _ in range(count)]

# Utilisation
namegen = NameGenerator()
character_name = namegen.generate_name()
print(f"Nom: {character_name}")

npc_names = namegen.generate_batch(5)


# === 4. Distribution de Poisson pour événements ===

def generate_poisson(lambda_param, size=1):
    """Génère nombres selon distribution Poisson approximative"""
    results = []
    for _ in range(size):
        k = 0
        p = 1.0
        L = 2.71828 ** (-lambda_param)  # e^(-λ)
        
        while p > L:
            k += 1
            p *= random.random()
        
        results.append(k - 1)
    
    return results if size > 1 else results[0]

# Utilisation: nombre d'appels par heure (moyenne = 5)
calls_per_hour = generate_poisson(5, size=24)


# === 5. Simulation génétique simple ===

class GeneticSimulation:
    """Simulation algorithme génétique simple"""
    
    def __init__(self, population_size=100):
        self.population_size = population_size
        # Gènes: liste de 10 valeurs entre 0 et 1
        self.population = [
            [random.random() for _ in range(10)]
            for _ in range(population_size)
        ]
    
    def fitness(self, individual):
        """Fonction fitness (exemple: somme des gènes)"""
        return sum(individual)
    
    def select_parents(self):
        """Sélection par tournoi"""
        tournament = random.sample(self.population, 5)
        tournament.sort(key=self.fitness, reverse=True)
        return tournament[0], tournament[1]
    
    def crossover(self, parent1, parent2):
        """Croisement un point"""
        point = random.randint(1, len(parent1) - 1)
        child = parent1[:point] + parent2[point:]
        return child
    
    def mutate(self, individual, mutation_rate=0.1):
        """Mutation aléatoire"""
        mutated = individual.copy()
        for i in range(len(mutated)):
            if random.random() < mutation_rate:
                mutated[i] = random.random()
        return mutated
    
    def evolve_generation(self):
        """Évolution une génération"""
        new_population = []
        
        # Élitisme: garder meilleurs 10%
        sorted_pop = sorted(self.population, key=self.fitness, reverse=True)
        elite_size = self.population_size // 10
        new_population.extend(sorted_pop[:elite_size])
        
        # Créer reste par croisement et mutation
        while len(new_population) < self.population_size:
            parent1, parent2 = self.select_parents()
            child = self.crossover(parent1, parent2)
            child = self.mutate(child)
            new_population.append(child)
        
        self.population = new_population

# Utilisation
sim = GeneticSimulation(100)
for generation in range(50):
    sim.evolve_generation()

best = max(sim.population, key=sim.fitness)
print(f"Meilleur individu: fitness = {sim.fitness(best):.2f}")


# === 6. Markov Chain pour génération texte ===

class MarkovChain:
    """Chaîne de Markov pour génération texte"""
    
    def __init__(self, order=2):
        self.order = order
        self.chain = {}
    
    def train(self, text):
        """Entraîne sur texte"""
        words = text.split()
        
        for i in range(len(words) - self.order):
            # État = n mots
            state = tuple(words[i:i + self.order])
            # Mot suivant
            next_word = words[i + self.order]
            
            if state not in self.chain:
                self.chain[state] = []
            self.chain[state].append(next_word)
    
    def generate(self, length=20, seed_state=None):
        """Génère texte aléatoire"""
        if not self.chain:
            return ""
        
        # État initial
        if seed_state is None:
            current_state = random.choice(list(self.chain.keys()))
        else:
            current_state = tuple(seed_state.split()[:self.order])
        
        result = list(current_state)
        
        for _ in range(length):
            if current_state not in self.chain:
                break
            
            next_word = random.choice(self.chain[current_state])
            result.append(next_word)
            
            # Nouvel état
            current_state = tuple(result[-self.order:])
        
        return " ".join(result)

# Utilisation
corpus = """
The quick brown fox jumps over the lazy dog.
The dog was lazy but the fox was quick.
A quick brown fox is faster than a lazy dog.
"""

markov = MarkovChain(order=2)
markov.train(corpus)
generated_text = markov.generate(15)
print(generated_text)


# === 7. Échantillonnage stratifié ===

def stratified_sample(data, strata_column, sample_size):
    """Échantillonnage stratifié"""
    from collections import defaultdict
    
    # Grouper par strate
    strata = defaultdict(list)
    for item in data:
        strata[item[strata_column]].append(item)
    
    # Calculer taille échantillon par strate
    total = len(data)
    samples = []
    
    for stratum, items in strata.items():
        # Proportionnel à la taille
        n_samples = int(len(items) / total * sample_size)
        if n_samples > 0:
            samples.extend(random.sample(items, min(n_samples, len(items))))
    
    return samples

# Utilisation
data = [
    {"id": 1, "category": "A", "value": 10},
    {"id": 2, "category": "A", "value": 20},
    {"id": 3, "category": "B", "value": 15},
    {"id": 4, "category": "B", "value": 25},
    {"id": 5, "category": "C", "value": 30},
]

sample = stratified_sample(data, "category", 3)


# === 8. Bootstrap sampling ===

def bootstrap_sample(data, num_samples=1000):
    """Bootstrap pour estimation intervalle confiance"""
    statistics = []
    
    for _ in range(num_samples):
        # Échantillon avec remise
        sample = random.choices(data, k=len(data))
        # Calculer statistique (ex: moyenne)
        stat = sum(sample) / len(sample)
        statistics.append(stat)
    
    # Intervalle confiance 95%
    statistics.sort()
    lower = statistics[int(num_samples * 0.025)]
    upper = statistics[int(num_samples * 0.975)]
    
    return lower, upper

# Utilisation
data = [random.gauss(100, 15) for _ in range(50)]
ci_lower, ci_upper = bootstrap_sample(data)
print(f"IC 95%: [{ci_lower:.2f}, {ci_upper:.2f}]")


# === 9. Générateur de graphes aléatoires ===

def generate_random_graph(num_nodes, edge_probability=0.3):
    """Génère graphe aléatoire (modèle Erdős–Rényi)"""
    edges = []
    
    for i in range(num_nodes):
        for j in range(i + 1, num_nodes):
            if random.random() < edge_probability:
                edges.append((i, j))
    
    return edges

# Utilisation
graph = generate_random_graph(10, 0.3)
print(f"Graphe avec {len(graph)} arêtes")


# === 10. Système de spawn avec cooldown ===

class EnemySpawner:
    """Système spawn ennemis avec cooldown"""
    
    def __init__(self):
        self.last_spawn = 0
        self.min_cooldown = 5
        self.max_cooldown = 15
        self.next_spawn_time = random.uniform(self.min_cooldown, self.max_cooldown)
    
    def update(self, current_time):
        """Mise à jour (appelé chaque frame/tick)"""
        if current_time - self.last_spawn >= self.next_spawn_time:
            # Spawn ennemi
            enemy = self.spawn_enemy()
            self.last_spawn = current_time
            self.next_spawn_time = random.uniform(self.min_cooldown, self.max_cooldown)
            return enemy
        return None
    
    def spawn_enemy(self):
        """Crée ennemi aléatoire"""
        enemy_types = ["Zombie", "Skeleton", "Spider"]
        weights = [50, 30, 20]
        
        enemy_type = random.choices(enemy_types, weights=weights, k=1)[0]
        health = random.randint(20, 100)
        damage = random.randint(5, 20)
        
        return {
            "type": enemy_type,
            "health": health,
            "damage": damage
        }

# Utilisation
spawner = EnemySpawner()

# Dans boucle de jeu
for tick in range(100):
    enemy = spawner.update(tick)
    if enemy:
        print(f"Tick {tick}: Spawned {enemy}")


[OK] BENCHMARKS & COMPARAISONS

import random
import time

# === Benchmark différentes méthodes ===

def benchmark(func, iterations=100000):
    """Mesure temps exécution"""
    start = time.time()
    for _ in range(iterations):
        func()
    end = time.time()
    return end - start

# Tests
iterations = 100000

# randint vs randrange
t1 = benchmark(lambda: random.randint(1, 100), iterations)
t2 = benchmark(lambda: random.randrange(1, 101), iterations)
print(f"randint: {t1:.4f}s, randrange: {t2:.4f}s")

# choice vs choices
data = list(range(100))
t1 = benchmark(lambda: random.choice(data), iterations)
t2 = benchmark(lambda: random.choices(data, k=1), iterations)
print(f"choice: {t1:.4f}s, choices(k=1): {t2:.4f}s")

# sample vs shuffle
data = list(range(100))
t1 = benchmark(lambda: random.sample(data, 50), iterations)
t2 = benchmark(lambda: random.shuffle(data.copy()), iterations)
print(f"sample(50): {t1:.4f}s, shuffle: {t2:.4f}s")


[OK] DEBUGGING & TESTS

# === Tests avec seed fixe ===

import unittest

class TestRandomFunctions(unittest.TestCase):
    
    def setUp(self):
        """Initialise seed avant chaque test"""
        random.seed(42)
    
    def test_dice_roll(self):
        """Test lancer de dé"""
        result = random.randint(1, 6)
        self.assertIn(result, range(1, 7))
    
    def test_reproducibility(self):
        """Test reproductibilité avec seed"""
        random.seed(123)
        value1 = random.randint(1, 100)
        
        random.seed(123)
        value2 = random.randint(1, 100)
        
        self.assertEqual(value1, value2)
    
    def test_sample_size(self):
        """Test taille échantillon"""
        data = list(range(100))
        sample = random.sample(data, 10)
        self.assertEqual(len(sample), 10)
        self.assertEqual(len(set(sample)), 10)  # Tous uniques


# === Debugging génération aléatoire ===

def debug_random_distribution(func, num_samples=10000):
    """Analyse distribution valeurs aléatoires"""
    from collections import Counter
    
    samples = [func() for _ in range(num_samples)]
    
    print(f"Nombre échantillons: {num_samples}")
    print(f"Min: {min(samples)}")
    print(f"Max: {max(samples)}")
    print(f"Moyenne: {sum(samples)/len(samples):.2f}")
    
    # Distribution
    if isinstance(samples[0], (int, bool)):
        counter = Counter(samples)
        print("\nDistribution:")
        for value, count in sorted(counter.items()):
            percentage = (count / num_samples) * 100
            print(f"  {value}: {count} ({percentage:.1f}%)")

# Utilisation
debug_random_distribution(lambda: random.randint(1, 6), 10000)
debug_random_distribution(lambda: random.choice(["A", "B", "C"]), 10000)


[OK] RESSOURCES & DOCUMENTATION

# Documentation officielle:
# https://docs.python.org/3/library/random.html

# Modules connexes:
# - secrets: génération cryptographique
#   https://docs.python.org/3/library/secrets.html
# 
# - numpy.random: calcul scientifique
#   https://numpy.org/doc/stable/reference/random/index.html
#
# - statistics: statistiques
#   https://docs.python.org/3/library/statistics.html

# Algorithmes:
# - Mersenne Twister (MT19937): générateur par défaut de random
# - PCG (Permuted Congruential Generator): utilisé par NumPy
# - CSPRNG: générateurs cryptographiques (secrets, SystemRandom)

# Livres & Cours:
# - "Python for Data Analysis" - Wes McKinney
# - "Fluent Python" - Luciano Ramalho
# - Real Python: https://realpython.com/


[OK] RÉSUMÉ RAPIDE

# Entiers
random.randint(a, b)               # a <= N <= b (inclusif)
random.randrange(start, stop)      # start <= N < stop (exclusif)
random.getrandbits(k)              # Entier k bits

# Flottants
random.random()                    # 0.0 <= x < 1.0
random.uniform(a, b)               # a <= x <= b

# Séquences
random.choice(seq)                 # 1 élément
random.choices(seq, k=n)           # n éléments AVEC remise
random.sample(seq, k=n)            # n éléments SANS remise
random.shuffle(list)               # Mélange EN PLACE

# Distributions
random.gauss(mu, sigma)            # Normale
random.expovariate(lambd)          # Exponentielle
random.triangular(low, high, mode) # Triangulaire

# Reproductibilité
random.seed(42)                    # Initialise seed
random.getstate()                  # Sauvegarde état
random.setstate(state)             # Restaure état

# Sécurité
import secrets                     # À utiliser pour crypto
secrets.token_hex(16)              # Token sécurisé MAUVAIS: Utiliser random pour sécurité
import random
token = "".join(random.choices("0123456789abcdef", k=32))  # INSÉCURE!

# [OK] BON: Utiliser secrets pour sécurité
import secrets
token = secrets.token_hex(16)


# [X] MAUVAIS: Oublier de seed pour reproductibilité
def test_something():
    result = random.randint(1, 100)  # Différent à chaque test!

# [OK] BON: Seed pour tests
def test_something():
    random.seed(42)
    result = random.randint(1, 100)  # Toujours le même


# [X] MAUVAIS: Seed dans boucle
for i in range(100):
    random.seed(42)                  # Toujours les mêmes valeurs!
    print(random.randint(1, 100))

# [OK] BON: Seed une fois avant boucle
random.seed(42)
for i in range(100):
    print(random.randint(1, 100))


# [X] MAUVAIS: Confondre randint et randrange
random.randint(1, 10)                # 1 à 10 INCLUS
random.randrange(1, 10)              # 1 à 9 EXCLU

# [OK] BON: Utiliser celui qui convient
random.randint(1, 10)                # Quand on veut 1 à 10
random.randrange(1, 11)              # Équivalent


# [X] MAUVAIS: Modifier liste pendant shuffle
my_list = [1, 2, 3, 4, 5]
for item in my_list:
    random.shuffle(my_list)          # Comportement imprévisible!

# [OK] BON: Shuffle une fois
my_list = [1, 2, 3, 4, 5]
random.shuffle(my_list)
