Comme je l'ai mentionné dans mon commentaire ci-dessus, je vous recommande de profiler cela avant de trop compliquer votre code. Une for
boucle rapide sommant les dés est beaucoup plus facile à comprendre et à modifier que les formules mathématiques compliquées et la construction / recherche de tables. Faites toujours le profil en premier pour vous assurer de résoudre les problèmes importants. ;)
Cela dit, il existe deux façons principales d'échantillonner des distributions de probabilité sophistiquées d'un seul coup:
1. Distributions de probabilités cumulatives
Il existe une astuce intéressante pour échantillonner à partir de distributions de probabilités continues en utilisant uniquement une seule entrée aléatoire uniforme . Cela a à voir avec la distribution cumulative , la fonction qui répond "Quelle est la probabilité d'obtenir une valeur non supérieure à x?"
Cette fonction est non décroissante, commençant à 0 et augmentant à 1 sur son domaine. Un exemple pour la somme de deux dés à six faces est illustré ci-dessous:
Si votre fonction de distribution cumulative a une inverse pratique à calculer (ou si vous pouvez l'approximer avec des fonctions par morceaux comme les courbes de Bézier), vous pouvez l'utiliser pour échantillonner à partir de la fonction de probabilité d'origine.
La fonction inverse gère le morcellement du domaine entre 0 et 1 en intervalles mappés à chaque sortie du processus aléatoire d'origine, la zone de chalandise correspondant à sa probabilité d'origine. (Ceci est vrai à l'infini pour les distributions continues. Pour les distributions discrètes comme les lancers de dés, nous devons appliquer un arrondi prudent)
Voici un exemple d'utilisation de ceci pour émuler 2d6:
int SimRoll2d6()
{
// Get a random input in the half-open interval [0, 1).
float t = Random.Range(0f, 1f);
float v;
// Piecewise inverse calculated by hand. ;)
if(t <= 0.5f)
{
v = (1f + sqrt(1f + 288f * t)) * 0.5f;
}
else
{
v = (25f - sqrt(289f - 288f * t)) * 0.5f;
}
return floor(v + 1);
}
Comparez cela à:
int NaiveRollNd6(int n)
{
int sum = 0;
for(int i = 0; i < n; i++)
sum += Random.Range(1, 7); // I'm used to Range never returning its max
return sum;
}
Vous voyez ce que je veux dire sur la différence de clarté et de flexibilité du code? La manière naïve peut être naïve avec ses boucles, mais elle est courte et simple, immédiatement évidente sur ce qu'elle fait et facile à mettre à l'échelle pour différentes tailles et nombres de matrices. Apporter des modifications au code de distribution cumulative nécessite des calculs non triviaux, et il serait facile de casser et de provoquer des résultats inattendus sans erreurs évidentes. (Ce que j'espère que je n'ai pas fait ci-dessus)
Donc, avant de vous débarrasser d'une boucle claire, assurez-vous absolument que c'est vraiment un problème de performances qui vaut ce genre de sacrifice.
2. La méthode des alias
La méthode de distribution cumulative fonctionne bien lorsque vous pouvez exprimer l'inverse de la fonction de distribution cumulative comme une simple expression mathématique, mais ce n'est pas toujours facile ni même possible. Une alternative fiable pour les distributions discrètes est ce qu'on appelle la méthode des alias .
Cela vous permet d'échantillonner à partir de n'importe quelle distribution de probabilité discrète arbitraire en utilisant seulement deux entrées aléatoires indépendantes et uniformément réparties.
Cela fonctionne en prenant une distribution comme celle ci-dessous à gauche (ne vous inquiétez pas que les zones / poids ne totalisent pas 1, pour la méthode Alias, nous nous soucions du poids relatif ) et en le convertissant en un tableau comme celui sur le droit où:
- Il y a une colonne pour chaque résultat.
- Chaque colonne est divisée en au plus deux parties, chacune associée à l'un des résultats originaux.
- L'aire / poids relatif de chaque résultat est conservé.
(Diagramme basé sur les images de cet excellent article sur les méthodes d'échantillonnage )
Dans le code, nous représentons cela avec deux tables (ou une table d'objets avec deux propriétés) représentant la probabilité de choisir le résultat alternatif de chaque colonne, et l'identité (ou "alias") de ce résultat alternatif. Ensuite, nous pouvons échantillonner de la distribution comme suit:
int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
int column = Random.Range(0, probabilityTable.Length);
float p = Random.Range(0f, 1f);
if(p < probabilityTable[column])
{
return column;
}
else
{
return aliasTable[column];
}
}
Cela implique un peu de configuration:
Calculez les probabilités relatives de chaque résultat possible (donc si vous roulez 1000d6, nous devons calculer le nombre de façons d'obtenir chaque somme de 1000 à 6000)
Créez une paire de tableaux avec une entrée pour chaque résultat. La méthode complète va au-delà de la portée de cette réponse, donc je recommande fortement de se référer à cette explication de l'algorithme de la méthode d'alias .
Stockez ces tableaux et faites-y référence chaque fois que vous avez besoin d'un nouveau jet de dé aléatoire de cette distribution.
Il s'agit d'un compromis espace-temps . L'étape de précalcul est quelque peu exhaustive, et nous devons mettre de côté la mémoire proportionnelle au nombre de résultats que nous avons (bien que même pour 1000d6, nous parlons de kilo-octets à un chiffre, donc rien pour perdre le sommeil), mais en échange de notre échantillonnage est à temps constant, quelle que soit la complexité de notre distribution.
J'espère que l'une ou l'autre de ces méthodes peut être utile (ou que je vous ai convaincu que la simplicité de la méthode naïve vaut le temps qu'il faut pour boucler);)