Подтвердить что ты не робот

Дизайн игры/теория, шанс урона лута/скорость всхода

У меня есть очень конкретный и длинный вопрос для всех вас. Этот вопрос касается как программирования, так и теории игр. Недавно я добавил нерегулярную руду в свою игру, основанную на поворотах: http://imgur.com/gallery/0F5D5Ij (Для тех из вас, кто выглядит, пожалуйста, простите текстуры разработки).

Теперь, на загадку, которую я созерцал. В моей игре руда генерируется каждый раз, когда создается новая карта. 0-8 единиц руды генерируются за создание уровня. У меня уже есть эта работа; за исключением того, что на данный момент он порождает "Emeraldite", что подводит меня к моему вопросу.

Как я, программист, сделаю так, чтобы узлы имели определенную редкость? Рассмотрите этот короткий макет, который не является фактически игровыми данными:

(Псевдо. Возможно, что a node будет одним из следующих)

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

Я хочу сделать так, чтобы сгенерированный node мог быть теоретически любым из вышеперечисленных, однако, с учетом вероятности. Надеюсь, этот вопрос достаточно ясен. Я пытался обвести голову вокруг этого и даже попытался выписать несколько утверждений с помощью randoms, однако, я продолжаю идти с пустыми руками.

В принципе, я просто хочу, чтобы вы, ребята, увидели мою проблему и, надеюсь, предоставили мне некоторое представление о том, как я мог бы подойти к этому динамическим способом.

Если какие-либо разъяснения необходимы, пожалуйста, спросите; извините, если это было запутано.

(Я добавляю С# в качестве тега только потому, что это язык, который я использую для этого проекта)

4b9b3361

Ответ 1

Сначала я представлял вероятность каждого типа лута как простое число. Вероятность в чистой математике условно выражается как число с плавающей запятой в диапазоне от 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).

Я предполагаю, что "Default (Empty Node)" означает вероятность того, что никто из других. В этом случае самый простой способ - не определять его - вы получаете его, если ни один из остальных не выбран.

Если включено "По умолчанию", сумма всех этих вероятностей будет равна 1 (то есть 100%) (или MaxProbability, если использовать целые числа).

Вероятность "по умолчанию" 1/10 в вашем примере на самом деле является противоречием, поскольку общая сумма всех этих вероятностей не равна 1 (это 0,38247619 - сумма вероятности, рассчитанная в моих примерах выше).

Затем вы выбираете случайное число в диапазоне от 0 до 1 (или MaxProbability, если используете целые числа), а выбранный тип лута является первым в списке, так что сумма вероятностей его и всех предыдущих ( "кумулятивная вероятность" ) больше случайного числа.

например.

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), вы выбрали бы "Серебряный" (первый с кумулятивной вероятностью больше этого).

Вы можете удерживать кумулятивные вероятности в массиве и прокручивать его до тех пор, пока не найдете один выше, чем случайное число, или достигнете конца.

Порядок списка не имеет значения. Вы выбрали случайное число только один раз для каждого экземпляра.

Включение "По умолчанию (Empty Node)" в списке означает, что последняя кумулятивная вероятность всегда будет MaxProbability, и цикл, который его ищет, никогда не пройдет мимо конца. (В качестве альтернативы, "По умолчанию" можно опустить, и вы выберете его, если цикл достигнет конца списка.)

Обратите внимание, что выбор случайного числа для каждого из них, в свою очередь, например, шанс 1/10 "Кровавого камня", а затем шанс 1/16 Медь, если не Кровавый камень, препятствует вероятностям по отношению к более ранним предметам: Фактическая вероятность Медь была бы (1/15) * (1 - (1/10)) - 10% меньше 1/15.

Здесь код для этого (фактический выбор - 5 операторов - в методе Select).

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

Ответ 2

Вы можете переписать все шансы, чтобы они использовали один и тот же делитель (например, 1000), тогда ваши шансы станут

  • Кровавый камень 10 в 1000
  • По умолчанию (Empty Node) 100 в 1000
  • Золото 20 в 1000

Затем создайте массив из 1000 элементов и заполните его с помощью 10 элементов кровавого камня,
100 Пустые элементы,
20 Золотые элементы,
и т.д.

Наконец, сгенерируйте случайное число от 0 до 1000 и используйте его как индекс в массиве элементов, это даст вы ваш случайный элемент.

Возможно, вам придется немного поиграть, так как вы, вероятно, захотите, чтобы все 1000 элементов массива были заполнены, но это общая идея.

отредактируйте свою не самую эффективную реализацию (по крайней мере, с точки зрения использования памяти, ее время работы должно быть хорошим), но я выбрал это, поскольку он позволяет получить краткое объяснение, которое не требует много математики.

Ответ 3

Прежде всего, указание пустой по умолчанию вероятности node не требуется. Другие вероятности должны быть определены таким образом, что пустой node создается, если не создается другой тип.

Как это сделать и обеспечить, чтобы генерация probabiltiies была равна тем, которые вы указали? Короче говоря:

  • преобразует вероятности в плавающую точку (это значение с общим делителем 1)
  • суммировать все вероятности и проверять, являются ли они < 1
  • напишите класс, который сохранит все вероятности
  • напишите функцию, которая получит случайный node на основе этих вероятностей

В вашем примере:

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 - чем больше она, тем меньше ошибка.

Другой вариант - как в ответе Богуша. Это более точно, но требуется больше операций для каждого сгенерированного элемента.

Опция, предложенная Томасом, требует много повторяемого кода для каждого варианта, следовательно, не универсальна. Ответ 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)+"%)");
        }
    }

}

Это гарантирует, что вероятности на самом деле одинаковы. Если вы проверили первый спайк node, затем другой, а затем другой - вы получите неправильные результаты: сначала проверили узлы, у которых была бы повышенная вероятность.

Пример прогона:

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

Ответ 4

Я думаю, что легко понять, как это работает. (Кобальт, 20: означает 1 из 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());
}

Edit:

Я добавляю раздел "Добавить общий руд и сортировать словарь".

Ответ 5

Недавно мне пришлось сделать что-то подобное, и я закончил с этим родовым "генератором икры".

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

Очевидно, что алгоритм появления не считался критическим кодом, поэтому накладные расходы на эту реализацию не вызывали беспокойства по сравнению с простотой использования. Икры были запущены на мировом творении, и это было легко более чем достаточно быстро.

Ответ 6

Astrotrain уже дал мой ответ, но так как я закодировал его, я отправлю его. Извините за синтаксис, я работаю в основном в Powershell, и это контекст, который в настоящее время находится в моем сознании. Рассмотрим этот код psuedo:

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

Ответ 7

Используйте 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. Конечно, это использует генератор псевдослучайных чисел, который должен быть достаточно хорош для игры.

Ответ 8

Несколько иной подход к идее Astrotrains будет заключаться в том, что вместо массива использовать операторы if. Поверхность заключается в том, что вам нужно меньше памяти, а недостатком является то, что для вычисления значения node потребуется больше времени процессора.

Таким образом:

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.