游戏设计/理论,Loot Drop Chance / Spawn Rate

时间:2014-09-23 09:11:50

标签: c# probability

我有一个非常具体和冗长的问题。这个问题是关于编程和游戏理论的。我最近在我的回合制策略游戏中添加了可产生的矿石:http://imgur.com/gallery/0F5D5Ij(对于那些看起来请原谅开发纹理的人来说)。

现在,谈到我一直在考虑的谜。在我的游戏中,每次创建新地图时都会生成矿石。每级创建生成0-8个矿石节点。我已经有了这个工作;除此之外它只产生“绿宝石”,这让我想到了我的问题。

程序员如何才能使节点具有特定的稀有性?考虑这个实际上不是游戏数据的简短模型:

(伪概率,节点将是以下之一)

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

我想这样做,理论上,生成的节点可以是上面的任何一个,但是,也考虑了几率。我希望这个问题足够清楚。我一直试图绕过这个,甚至试图用randoms写出一些if语句,然而,我一直空手而归。

基本上,我只是希望你们看到我的问题,并希望能够让我对如何以动态的方式解决这个问题提供一些见解。

如果需要澄清,请询问;如果这很复杂,那就再次感到抱歉。

(我只是将C#添加为标签,因为这是我用于此项目的语言)

8 个答案:

答案 0 :(得分:19)

我首先将每个战利品类型的概率表示为一个简单的数字。 纯数学中的概率通常表示为0到1范围内的浮点数,但为了提高效率,可以在任何(足够大)范围内使用整数(每个值是0-1值乘以最大值(其中)我在这里打电话给 MaxProbability )。

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).

我假设'默认(空节点)'表示没有其他人的概率。 在这种情况下,最简单的方法是不定义它 - 如果没有其他选择,你就得到它。

如果包含“默认”,则所有这些概率的总和将为1(即100%)(或 MaxProbability ,如果使用整数)。

你的例子中“默认”的1/10概率实际上是一个矛盾,因为所有这些概率的总和不是1(它是0.38247619 - 上面例子中计算的概率之和)。

然后你会选择一个0到1范围内的随机数(或者如果使用整数则选择MaxProbability),并且选择的战利品类型是列表中的 first ,以便概率之和它和之前的所有(“累积概率”)大于随机数。

e.g。

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)

e.g。如果0到999(含)范围内的随机数为184(或172到199范围内的任何一个),您可以选择“银”(第一个累积概率大于此值的银行)。

您可以将累积概率保存在数组中并循环遍历它,直到找到高于随机数的值,或者到达结尾。

列表的顺序无关紧要。 您每个实例只选择一个随机数。

在列表中包含“默认(空节点)”意味着最后的累积概率将始终为 MaxProbability ,并且搜索它的循环将永远不会超过结束。 (或者,可以省略'Default',如果循环到达列表的末尾,则选择它。)

请注意,依次选择每个随机数,例如:如果不是血石,那么“血石”的概率为1/10,然后是1/15的铜概率,会扭曲对早期项目的可能性: 铜的实际概率为(1/15)*(1 - (1/10)) - 比1/15小10%。

这是执行此操作的代码(实际选择的是5个语句 - 在方法选择中)。

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());
        }           
    }
}

答案 1 :(得分:16)

你可以重写所有机会,以便他们使用相同的除数(例如1000),然后你的机会成为

  • Bloodstone 10 in 1000
  • 默认(空节点)100(1000)
  • Gold in 1000

接下来,创建一个包含1000个元素的数组,并用
填充它 10个血石元素,
100个空元素,
20金元素,
等。

最后,生成0到1000之间的随机数,并将其用作元素数组的索引,这将给出 你随机的元素。

您可能需要稍微玩一下,因为您可能希望填充所有1000个数组元素,但这是一般的想法。

编辑它不是最有效的实现(至少在内存使用方面,它的运行时间应该是好的),但我选择了这个,因为它允许简洁的解释,不需要很多数学。

答案 2 :(得分:10)

首先,不需要指定default-empty节点的概率。其他概率应该以这样的方式定义,即如果没有创建其他类型,则创建空节点。

如何做到这一点并确保生成概率等于您指定的概率?简而言之:

  • 将概率转换为浮点(它是一个公约数为1的值)
  • 总结所有概率并检查它们是否&lt; 1
  • 写一个将存储所有概率的类
  • 编写一个函数,根据这些概率得到一个随机节点

对于你的例子:

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

现在,该课程至少可以通过两种方式实施。我的选项消耗大量内存,计算一次,但它也会舍入可能引入一些错误的概率值。请注意,错误取决于arrSize变量 - 它越大,错误越小。

另一种选择与Bogusz的回答一样。它更精确,但每个生成的元素需要更多操作。

Thomas建议的选项需要为每个选项提供大量可重复的代码,因此不是通用的。 Shellshock的答案将具有无效的有效概率。

Astrotrain强迫自己使用相同除数的想法几乎与我自己的相同,尽管实现方式略有不同。

以下是我的想法的示例实现(在java中,但应该非常容易移植):

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)+"%)");
        }
    }

}

这可以确保概率实际上是一致的。如果你检查了第一个节点spawn,那么另一个,然后是另一个 - 你会得到错误的结果:首先检查的节点会增加概率。

示例运行:

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%)

答案 3 :(得分:4)

我认为很容易理解它的工作原理。 (钴,20:表示20中的1 - > 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());
}

修改

我添加了部分&#34;添加普通矿石和排序字典&#34;。

答案 4 :(得分:3)

我最近不得不做类似的事情,最后我得到了这个通用的“spawn生成器”。

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;
    }
}

你会像以下一样使用它:

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();

显然,生成算法不被视为关键代码,因此与易用性相比,此实现的开销无关紧要。 Spawns是在世界创造的情况下运行的,而且速度非常快。

答案 5 :(得分:2)

Astrotrain已经给出了我的答案,但是因为我已将其编码,我将发布它。对不起语法,我主要在Powershell工作,这是我脑海中的背景。考虑这个伪代码:

// 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))

答案 6 :(得分:0)

使用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

最小值应始终为1,最大值是产生项目+ 1的几率(minValue包含在内,maxValue是独占的)。总是测试1的返回值,例如,对于Bloodstone,随机生成的数字为1的概率是百分之一。当然,这使用伪随机数生成器,这对于游戏应该足够好。

答案 7 :(得分:0)

与Astrotrains想法略有不同的方法是使用if语句代替数组。那么好处就是你需要更少的内存,这就是需要更多的CPU时间来计算节点值的缺点。

因此:

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

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

这个变种的另一个缺点是,这个变种在数组变体中占据了更多位置,在你使用它的位置代码更多,并且更容易出错/纠正(尝试在其中添加你需要更新所有变体的内容) )。

因此,为了完整起见,这里使用了这个,但是数组vairant(不考虑内存使用)不太容易出现if变体的问题。