web-dev-qa-db-fra.com

Conception/théorie du jeu, Chance de Butin/Taux de réapparition

J'ai une question très précise et longue pour vous tous. Cette question concerne à la fois la programmation et la théorie des jeux. J'ai récemment ajouté du minerai spawnable à mon jeu de stratégie par tour: http://imgur.com/gallery/0F5D5Ij (Pour ceux d'entre vous qui regardez, veuillez pardonner les textures de développement).

Maintenant, sur l'énigme que j'ai envisagée. Dans mon jeu, le minerai est généré chaque fois qu'une nouvelle carte est créée. 0 à 8 nœuds sont générés par création de niveau. J'ai déjà ce travail; sauf qu'il ne génère que "Emeraldite" à ce stade, ce qui m'amène à ma question.

Comment le programmeur pourrait-il faire en sorte que les nœuds aient une rareté spécifique? Considérez cette courte maquette qui n'est pas réellement une donnée de jeu:

(Pseudo Chances qu'un noeud sera l'un des suivants)

Bloodstone 1 in 100
Default(Empty Node) 1 in 10
Copper 1 in 15
Emeraldite 1 in 35
Gold 1 in 50
Heronite 1 in 60
Platinum 1 in 60
Shadownite 1 in 75
Silver 1 in 35
Soranite 1 in 1000
Umbrarite 1 in 1000
Cobalt 1 in 75
Iron 1 in 15

Je veux faire en sorte qu'un nœud généré puisse être, théoriquement, n'importe lequel de ce qui précède, cependant, les chances étant également prises en compte. J'espère que cette question est suffisamment claire. J'essayais de comprendre ce que je pensais et j'ai même essayé d'écrire quelques déclarations avec des alambics, mais je n'arrive pas à me rendre les mains vides.

En gros, je veux simplement que vous voyiez mon problème et que vous me donniez, espérons-le, un aperçu de la façon dont je pourrais aborder ce problème de manière dynamique.

Si des éclaircissements sont nécessaires, veuillez demander; désolé encore si cela a été compliqué.

(J'ajoute C # comme tag uniquement parce que c'est le langage que j'utilise pour ce projet)

22
Krythic

Je représenterais d'abord la probabilité de chaque type de butin sous forme d'un nombre simple. Une probabilité en mathématiques pures est habituellement exprimée sous forme de nombre à virgule flottante compris entre 0 et 1, mais pour des raisons d'efficacité, vous pouvez utiliser des entiers dans toute plage (suffisamment grande) (chaque valeur correspond à la valeur 0-1 multipliée par le maximum (qui J'appelle MaxProbability ici)).

e.g. Bloodstone (1 in 100) is 1/100 = 0.01, or MaxProbability * (1/100).
     Copper (1 in 15) is 1/15 = 0.06667, or MaxProbability * (1/15).

Je suppose que «Par défaut (nœud vide)» signifie la probabilité qu'aucune des autres ne soit probable. Dans ce cas, le moyen le plus simple n'est pas de le définir - vous l'obtenez si aucun des autres n'est choisi.

Si "Par défaut" était inclus, la somme de toutes ces probabilités serait de 1 (c'est-à-dire 100%) (ou MaxProbability , si vous utilisez des entiers).

La probabilité 1/10 de 'Défaut' dans votre exemple est en réalité une contradiction car le total de toutes ces probabilités n'est pas 1 (c'est 0.38247619 - la somme de la probabilité calculée dans mes exemples ci-dessus).

Ensuite, vous choisiriez un nombre aléatoire compris entre 0 et 1 (ou MaxProbability si vous utilisiez des entiers). Le type de butin choisi est le premier un de la liste, de sorte que la somme de ses probabilités et de toutes les précédentes ("probabilité cumulative") est plus grand que le nombre aléatoire.

par exemple.

MaxProbability = 1000   (I'm using this to make it easy to read).
     (For accurate probabilities, you could use 0x7FFFFFFF).

Type                 Probability  Cumulative
----                 -----------  ----------
Bloodstone             10            10              (0..9 yield Bloodstone)
Copper                 67            77    (10+67)   (10..76 yield Copper)
Emeraldite             29           105    (77+29)
Gold                   20           125    etc.
Heronite               17           142
Platinum               17           159
Shadownite             13           172
Silver                 29           200
Soranite                1           201
Umbrarite               1           202
Cobalt                 13           216
Iron                   67           282

Default (Empty Node) 7175          1000   (anything else)

par exemple. Si votre nombre aléatoire compris entre 0 et 999 (inclus) était 184 (ou un nombre compris entre 172 et 199), vous choisiriez "Argent" (le premier avec une probabilité cumulée supérieure à celle-ci).

Vous pouvez conserver les probabilités cumulatives dans un tableau et les parcourir jusqu'à ce que vous en trouviez une supérieure au nombre aléatoire, ou atteindre la fin.

L'ordre de la liste n'a pas d'importance. Vous avez choisi un nombre aléatoire une seule fois par instance.

Le fait d'inclure 'Par défaut (nœud vide)' dans la liste signifie que la dernière probabilité cumulée sera toujours MaxProbability et que la boucle qui la recherche ne dépassera jamais la fin. (Vous pouvez également omettre «Par défaut» et le choisir si la boucle atteint la fin de la liste.)

Notez que choisir un nombre aléatoire pour chacun à son tour, par ex. une chance sur 10 de 'Bloodstone', puis sur 1/15 sur le cuivre sinon sur Bloodstone, oriente les probabilités vers les objets précédents: La probabilité réelle de cuivre serait de (1/15) * (1 - (1/10 )) - 10% moins que 1/15.

Voici le code pour le faire (le choix actuel est de 5 instructions - dans la méthode Choisissez ).

using System;

namespace ConsoleApplication1
{
    class LootChooser
    {
        /// <summary>
        /// Choose a random loot type.
        /// </summary>
        public LootType Choose()
        {
            LootType lootType = 0;         // start at first one
            int randomValue = _rnd.Next(MaxProbability);
            while (_lootProbabilites[(int)lootType] <= randomValue)
            {
                lootType++;         // next loot type
            }
            return lootType;
        }

        /// <summary>
        /// The loot types.
        /// </summary>
        public enum LootType
        {
            Bloodstone, Copper, Emeraldite, Gold, Heronite, Platinum,
            Shadownite, Silver, Soranite, Umbrarite, Cobalt, Iron, Default
        };

        /// <summary>
        /// Cumulative probabilities - each entry corresponds to the member of LootType in the corresponding position.
        /// </summary>
        protected int[] _lootProbabilites = new int[]
        {
            10, 77, 105, 125, 142, 159, 172, 200, 201, 202, 216, 282,  // (from the table in the answer - I used a spreadsheet to generate these)
            MaxProbability
        };

        /// <summary>
        /// The range of the probability values (dividing a value in _lootProbabilites by this would give a probability in the range 0..1).
        /// </summary>
        protected const int MaxProbability = 1000;

        protected Random _rnd = new Random((int)(DateTime.Now.Ticks & 0x7FFFFFFF));    


        /// <summary>
        /// Simple 'main' to demonstrate.
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            var chooser = new LootChooser();
            for(int n=0; n < 100; n++)
                Console.Out.WriteLine(chooser.Choose());
        }           
    }
}
19
John B. Lambe

Vous pouvez réécrire toutes les chances pour qu’ils utilisent le même diviseur (par exemple 1000), vos chances deviennent alors

  • Bloodstone 10 sur 1000
  • Par défaut (nœud vide) 100 sur 1000
  • Or 20 en 1000

Ensuite, créez un tableau de 1000 éléments et remplissez-le avec
10 éléments de pierre de sang,
100 éléments vides,
20 éléments en or,
etc.

Enfin, générez un nombre aléatoire compris entre 0 et 1000, et utilisez-le comme index dans le tableau d'éléments pour obtenir votre élément aléatoire.

Vous devrez peut-être jouer un peu avec les chances, car vous voudrez probablement que tous les 1000 éléments du tableau soient remplis, mais c'est l'idée générale.

edit ce n’est pas l’implémentation la plus efficace (du moins en termes d’utilisation de la mémoire, son temps d’exécution devrait être bon), mais j’ai choisi cette option car elle permet une explication concise ne nécessitant pas beaucoup de calculs.

16
Astrotrain

Tout d'abord, il est inutile de spécifier la probabilité du nœud vide par défaut. Les autres probabilités doivent être définies de manière à ce que le nœud vide soit créé si aucun autre type n'est créé.

Comment faire cela et s'assurer que les probabilités de génération sont égales à celles que vous avez spécifiées? En bref:

  • convertir les probabilités en une virgule flottante (c'est une valeur avec un diviseur commun de 1)
  • additionnez toutes les probabilités et vérifiez si elles sont <1
  • écrire une classe qui stockera toutes les probabilités
  • écrire une fonction qui donnera un noeud aléatoire basé sur ces probabilités

Pour votre exemple:

Bloodstone 1 in 100 = 0.01
Copper 1 in 15 ~= 0.07
Emeraldite 1 in 35 ~= 0.03
Gold 1 in 50 = 0.02
Default = 0.87

Maintenant, la classe peut être implémentée d'au moins deux manières. Mon option consomme beaucoup de mémoire, effectue les calculs une fois, mais elle arrondit également les valeurs de probabilité qui peuvent introduire des erreurs. Notez que l’erreur dépend de la variable arrSize - plus elle est grande, plus l’erreur est petite.

L'autre option est comme dans la réponse de Bogusz. Il est plus précis, mais nécessite plus d'opérations pour chaque élément généré.

L'option suggérée par Thomas nécessite beaucoup de code répétable pour chaque option et n'est donc pas polyvalente. La réponse de Shellshock aura des probabilités effectives invalides.

L'idée d'Astrotrain de vous forcer à utiliser le même diviseur est pratiquement la même que la mienne, bien que la mise en œuvre soit légèrement différente.

Voici un exemple d'implémentation de mon idée ( en Java, mais devrait être porté très facilement ):

public class NodeEntry {

    String name;
    double probability;

    public NodeEntry(String name, double probability) {
        super();
        this.name = name;
        this.probability = probability;
    }

    public NodeEntry(String name, int howMany, int inHowMany) {
        this.name = name;
        this.probability = 1.0 * howMany / inHowMany;
    }

    public final String getName() {
        return name;
    }

    public final void setName(String name) {
        this.name = name;
    }

    public final double getProbability() {
        return probability;
    }

    public final void setProbability(double probability) {
        this.probability = probability;
    }


    @Override
    public String toString() {
        return name+"("+probability+")";
    }

    static final NodeEntry defaultNode = new NodeEntry("default", 0);
    public static final NodeEntry getDefaultNode() {
        return defaultNode;
    }

}

public class NodeGen {

    List<NodeEntry> nodeDefinitions = new LinkedList<NodeEntry>();

    public NodeGen() {
    }

    public boolean addNode(NodeEntry e) {
        return nodeDefinitions.add(e);
    }

    public boolean addAllNodes(Collection<? extends NodeEntry> c) {
        return nodeDefinitions.addAll(c);
    }



    static final int arrSize = 10000;

    NodeEntry randSource[] = new NodeEntry[arrSize];

    public void compile() {
        checkProbSum();

        int offset = 0;
        for (NodeEntry ne: nodeDefinitions) {
            int amount = (int) (ne.getProbability() * arrSize);
            for (int a=0; a<amount;a++) {
                randSource[a+offset] = ne; 
            }
            offset+=amount;
        }

        while (offset<arrSize) {
            randSource[offset] = NodeEntry.getDefaultNode();
            offset++;
        }
    }

    Random gen = new Random();

    public NodeEntry getRandomNode() {
        return randSource[gen.nextInt(arrSize)]; 
    }

    private void checkProbSum() {
        double sum = 0;

        for (NodeEntry ne: nodeDefinitions) {
            sum+=ne.getProbability();
        }

        if (sum >1) {
            throw new RuntimeException("nodes probability > 1");
        }

    }



    public static void main(String[] args) {
        NodeGen ng = new NodeGen();
        ng.addNode(new NodeEntry("Test 1", 0.1));
        ng.addNode(new NodeEntry("Test 2", 0.2));
        ng.addNode(new NodeEntry("Test 3", 0.2));

        ng.compile();

        Map<NodeEntry, Integer> resCount = new HashMap<NodeEntry, Integer>();

        int generations = 10000;
        for (int a=0; a<generations; a++) {
            NodeEntry node = ng.getRandomNode();
            Integer val = resCount.get(node);
            if (val == null) {
                resCount.put(node, new Integer(1));
            } else {
                resCount.put(node, new Integer(val+1));
            }
        }


        for (Map.Entry<NodeEntry, Integer> entry: resCount.entrySet()) {
            System.out.println(entry.getKey()+": "+entry.getValue()+" ("+(100.0*entry.getValue()/generations)+"%)");
        }
    }

}

Cela garantit que les probabilités sont réellement uniformes. Si vous avez vérifié le premier noeud apparaissant, puis l'autre, puis l'autre - vous obtiendriez des résultats erronés: les noeuds vérifiés en premier auraient une probabilité accrue.

Échantillon échantillon:

Test 2(0.2): 1975 (19.75%)
Test 1(0.1): 1042 (10.42%)
Test 3(0.2): 1981 (19.81%)
default(0.0): 5002 (50.02%)
10
Dariusz

Je pense qu'il est facile de comprendre comment cela fonctionne. (Cobalt, 20: signifie 1 sur 20 -> 5%)

Dictionary<string, double> ore = new Dictionary<string, double>();
Random random = new Random();

private void AddOre(string Name, double Value)
{
    ore.Add(Name, 1.0 / Value);
}

private string GetOreType()
{
    double probSum = 0;
    double Rand = random.NextDouble();

    foreach (var pair in ore)
    {
        probSum += pair.Value;
        if (probSum >= Rand)
            return pair.Key;
    }
    return "Normal Ore";  //Reaches this point only if an error occurs.
}

private void Action()
{
    AddOre("Cobalt", 20);
    AddOre("Stone", 10);
    AddOre("Iron", 100);
    AddOre("GreenOre", 300);

        //Add Common ore and sort Dictionary
        AddOre("Common ore", 1 / (1 - ore.Values.Sum()));
        ore = ore.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);

    Console.WriteLine(GetOreType());
}

Modifier:

J'ajoute la section "Ajouter un minerai commun et un dictionnaire".

4
Bogusz Michałowski

J'ai récemment eu à faire quelque chose de similaire et je me suis retrouvé avec ce "générateur de géniteurs" générique.

public interface ISpawnable : ICloneable
{
    int OneInThousandProbability { get; }
}

public class SpawnGenerator<T> where T : ISpawnable
{
    private class SpawnableWrapper
    {
        readonly T spawnable;
        readonly int minThreshold;
        readonly int maxThreshold;

        public SpawnableWrapper(T spawnable, int minThreshold)
        {
            this.spawnable = spawnable;
            this.minThreshold = minThreshold;
            this.maxThreshold = this.minThreshold + spawnable.OneInThousandProbability;
        }

        public T Spawnable { get { return this.spawnable; } }
        public int MinThreshold { get { return this.minThreshold; } }
        public int MaxThreshold { get { return this.maxThreshold; } }
    }

    private ICollection<SpawnableWrapper> spawnableEntities;
    private Random r;

    public SpawnGenerator(IEnumerable<T> objects, int seed)
    {
        Debug.Assert(objects != null);

        r = new Random(seed);
        var cumulativeProbability = 0;
        spawnableEntities = new List<SpawnableWrapper>();

        foreach (var o in objects)
        {
            var spawnable = new SpawnableWrapper(o, cumulativeProbability);
            cumulativeProbability = spawnable.MaxThreshold;
            spawnableEntities.Add(spawnable);
        }

        Debug.Assert(cumulativeProbability <= 1000);
    }

    //Note that it can spawn null (no spawn) if probabilities dont add up to 1000
    public T Spawn()
    {
        var i = r.Next(0, 1000);
        var retVal = (from s in this.spawnableEntities
                      where (s.MaxThreshold > i && s.MinThreshold <= i)
                      select s.Spawnable).FirstOrDefault();

        return retVal != null ? (T)retVal.Clone() : retVal;
    }
}

Et vous l'utiliseriez comme:

public class Gem : ISpawnable
{
    readonly string color;
    readonly int oneInThousandProbability;

    public Gem(string color, int oneInThousandProbability)
    {
        this.color = color;
        this.oneInThousandProbability = oneInThousandProbability;
    }

    public string Color { get { return this.color; } }

    public int OneInThousandProbability
    {
        get
        {
            return this.oneInThousandProbability;
        }
    }

    public object Clone()
    {
        return new Gem(this.color, this.oneInThousandProbability);
    }
}

var RedGem = new Gem("Red", 250);
var GreenGem = new Gem("Green", 400);
var BlueGem = new Gem("Blue", 100);
var PurpleGem = new Gem("Purple", 190);
var OrangeGem = new Gem("Orange", 50);
var YellowGem = new Gem("Yellow", 10);

var spawnGenerator = new SpawnGenerator<Gem>(new[] { RedGem, GreenGem, BlueGem, PurpleGem, OrangeGem, YellowGem }, DateTime.Now.Millisecond);
var randomGem = spawnGenerator.Spawn();

De toute évidence, l’algorithme de spawn n’était pas considéré comme un code critique; par conséquent, la surcharge de cette implémentation ne posait pas de problème par rapport à la facilité d’utilisation. Les spawns ont été lancés lors de la création du monde et c'était facilement plus que rapide.

3
InBetween

Astrotrain a déjà donné ma réponse, mais comme je l'ai déjà codée, je vais la poster. Désolé pour la syntaxe, je travaille principalement dans Powershell et c'est le contexte actuel dans mon esprit. Considérons ce code pseudo:

// Define the odds for each loot type
//           Description,Freq,Range
LootOddsArray = "Bloodstone",1,100,
"Copper",1,15,
"Emeraldite,"1,35,
"Gold",1,50,
"Heronite",1,60,
"Platinum",1,60,
"Shadownite",1,75,
"Silver",1,35,
"Soranite",1,1000,
"Umbrarite",1,1000,
"Cobalt",1,75,
"Iron",1,15

// Define your lookup table. It should be as big as your largest odds range.
LootLookupArray(1000)

// Fill all the 'default' values with "Nothing"
for (i=0;i<LootLookupArray.length;i++) {
    LootOddsArray(i) = "Nothing"
}

// Walk through your various treasures
for (i=0;i<LootOddsArray.length;i++)
    // Calculate how often the item will appear in the table based on the odds
    // and place that many of the item in random places in the table, not overwriting
    // any other loot already in the table
    NumOccsPer1000 = Round(LootOddsArray(i).Freq * 1000/LootOddsArray(i).Range)
    for (l=0;l<NumOccsPer1000;l++) {
        // Find an empty slot for the loot
        do
            LootIndex = Random(1000)
        while (LootLookupArray(LootIndex) != "Nothing")
        // Array(Index) is empty, put loot there
        LootLookupArray(LootIndex) = LootOddsArray(i).Description
    }
}

// Roll for Loot
Loot = LootLookupArray(Random(1000))
2
Arluin

Utilisez Random.Next http://msdn.Microsoft.com/en-us/library/2dx6wyd4(v=vs.110).aspx :

Random rnd = new Random();

if (rnd.Next(1, 101) == 1)
    // spawn Bloodstone
if (rnd.Next(1, 16) == 1)
    // spawn Copper
if (rnd.Next(1, 36) == 1)
    // spawn Emeraldite

La valeur minimale doit toujours être 1, la valeur maximale correspond aux chances de création de l'élément + 1 (minValue est inclusif, maxValue est exclusif). Testez toujours la valeur de retour pour 1, par exemple, pour Bloodstone, il y a 1 chance sur 100 que le nombre généré aléatoirement soit égal à 1. Bien sûr, cela utilise un générateur de nombres pseudo aléatoires, qui devrait suffire pour un jeu.

0
Polyfun

Une approche légèrement différente de l’idée d’Astrotrains serait qu’à la place d’un tableau à utiliser avec les instructions if. L’avantage, c’est que vous avez besoin de moins de mémoire et l’inconvénient, de disposer de plus de temps processeur pour calculer la valeur du nœud.

Ainsi:

Random rnd = new Random();
var number = rnd.next(1,1000);

if (number >= 1 && number <10)
{
  // empty
}
else
{
  if (number >= 10 && number <100)
  {
     // bloodstone
  }
  else
  {
     //......
  }
}

Un autre inconvénient de cette variante, associée à la variante tableau, est que celle-ci prend plus de place à l'endroit où vous l'utilisez et est plus sujette aux erreurs/corrections (essayez d'ajouter quelque chose à l'intérieur, vous devez mettre à jour toutes les variantes).

Ainsi, ceci est mentionné ici par souci de complétude, mais le tableau vairant (utilisation de la mémoire mise à part) est moins sujet aux problèmes que la variante if a.

0
Thomas