web-dev-qa-db-ja.com

ゲームデザイン/理論、戦利品ドロップチャンス/スポーン率

皆さんに非常に具体的で長い間質問があります。この質問は、プログラミングとゲーム理論の両方に関するものです。最近、ターン制ストラテジーゲームにスポーン可能な鉱石を追加しました: 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

理論的には、生成されたノードが上記のいずれかになり、オッズも考慮されるようにしたいと思います。質問が十分に明確であることを願っています。私はこれに頭を悩ませようとしていて、ランダムなifステートメントをいくつか書き出そうとしましたが、手ぶらで出てきます。

基本的に、私は皆さんに私の問題を見てもらいたいだけです。うまくいけば、動的な方法でこれにアプローチする方法についての洞察を提供してください。

説明が必要な場合は、お問い合わせください。これが複雑だった場合は、もう一度申し訳ありません。

(C#をタグとして追加しているのは、それがこのプロジェクトで使用している言語だからです)

22
Krythic

まず、各戦利品タイプの確率を単純な数値として表します。純粋数学の確率は、従来、0から1の範囲の浮動小数点数として表されますが、効率を上げるために、任意の(十分に大きい)範囲の整数を使用できます(各値は、0-1の値に最大値を掛けたものです(私はMaxProbability here))と呼んでいます。

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

「デフォルト(空のノード)」は、他のいずれも存在しない確率を意味すると想定しています。この場合、最も簡単な方法はそれを定義しないことです-他のどれも選択されていない場合にそれを取得します。

'Default'が含まれている場合、これらすべての確率の合計は1(つまり、100%)(または、整数を使用している場合はMaxProbability)になります。

あなたの例の「デフォルト」の1/10の確率は、これらすべての確率の合計が1ではないため、実際には矛盾しています(0.38247619-上記の例で計算された確率の合計)。

次に、0から1の範囲の乱数(または整数を使用している場合はMaxProbability)を選択します。選択した戦利品の種類は、リスト内のfirst 1であり、その確率の合計と以前のもの(「累積確率」)はすべてより大きい乱数です。

例えば.

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)

例えば0から999(両端を含む)の範囲の乱数が184(または172から199の範囲の任意のもの)である場合、「シルバー」(累積確率がこれよりも大きい最初の乱数)を選択します。

累積確率を配列に保持し、乱数よりも高い確率が見つかるまで、または最後に到達するまでループすることができます。

リストの順序は重要ではありません。インスタンスごとに1回だけ乱数を選択しました。

リストに「デフォルト(空のノード)」を含めると、最後の累積確率は常にMaxProbabilityになり、それを検索するループが終了を超えることはありません。 (または、「デフォルト」を省略して、ループがリストの最後に達した場合に選択することもできます。)

それぞれに順番に乱数を選択することに注意してください。 'Bloodstone'の1/10の確率、次にBloodstoneでない場合は銅の1/15の確率は、確率を以前の項目に偏らせます。銅の実際の確率は(1/15)*(11/10 ))-1/15より10%少ない。

これを行うためのコードは次のとおりです(実際の選択は5つのステートメントです-メソッド内で---(Choose)。

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

すべてのチャンスを書き直して、同じ除数(1000など)を使用するようにすると、チャンスは次のようになります。

  • 1000分のブラッドストーン10
  • デフォルト(空のノード)1000分の100
  • 1000分のゴールド20

次に、1000個の要素の配列を作成し、次のように入力します
10ブラッドストーン要素、
100個の空の要素、
20ゴールド要素、
等。

最後に、0〜1000の乱数を生成し、それを要素配列へのインデックスとして使用すると、ランダムな要素が得られます。

1000個の配列要素すべてを埋めたいと思うかもしれないので、少しチャンスを試してみる必要があるかもしれませんが、これは一般的な考え方です。

編集最も効率的な実装ではありません(少なくともメモリ使用量の点では、実行時間は良いはずです)が、多くを必要としない簡潔な説明が可能になるため、これを選択しました数学の。

16
Astrotrain

まず、デフォルトの空のノードの確率を指定する必要はありません。他の確率は、他のタイプが作成されない場合に空のノードが作成されるように定義する必要があります。

これを行い、生成確率が指定したものと等しいことを確認するにはどうすればよいですか?要するに:

  • 確率を浮動小数点に変換します(これは、最大公約数が1の値です)
  • すべての確率を合計し、それらが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

これで、クラスは少なくとも2つの方法で実装できます。私のオプションは多くのメモリを消費し、計算を1回実行しますが、エラーを引き起こす可能性のある確率値も丸めます。エラーはarrSize変数に依存することに注意してください。変数が大きいほど、エラーは小さくなります。

他の選択肢は、ボグスの答えのとおりです。より正確ですが、生成された要素ごとにより多くの操作が必要です。

Thomasが提案したオプションは、オプションごとに多くの繰り返し可能なコードを必要とするため、用途が広くありません。 Shellshockの答えには、無効な有効確率があります。

同じ除数を使用するように強制するアストロトレインのアイデアは、実装が少し異なりますが、実質的に私と同じです。

これが私のアイデアのサンプル実装です(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)+"%)");
        }
    }

}

これにより、確率が実際に均一になります。最初のノードのスポーン、次に他のノード、次に他のノードのスポーンをチェックした場合、間違った結果が得られます。最初にチェックされたノードの確率が高くなります。

サンプル実行:

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

それがどのように機能するかは理解しやすいと思います。 (コバルト、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());
}

編集:

「一般的な鉱石を追加して辞書を並べ替える」セクションを追加します。

4

最近、似たようなことをしなければならなかったので、この一般的な「スポーンジェネレーター」に行き着きました。

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

明らかに、スポーンアルゴリズムは重要なコードとは見なされていなかったため、使いやすさと比較した場合、この実装のオーバーヘッドは問題ではありませんでした。スポーンはワールドクリエーションで実行され、それは簡単に十分な速さ以上でした。

3
InBetween

アストロトレインはすでに私の答えを出しましたが、私はすでにそれをコーディングしたので、それを投稿します。構文については申し訳ありませんが、私は主に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))
2
Arluin

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である確率は100分の1です。もちろん、これは疑似乱数ジェネレーターを使用します。これはゲームに十分なはずです。

0
Polyfun

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バリアントが持つ問題の可能性が低くなります。

0
Thomas