我有一个非常具体和冗长的问题。这个问题是关于编程和游戏理论的。我最近在我的回合制策略游戏中添加了可产生的矿石: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#添加为标签,因为这是我用于此项目的语言)
答案 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),然后你的机会成为
接下来,创建一个包含1000个元素的数组,并用
填充它
10个血石元素,
100个空元素,
20金元素,
等。
最后,生成0到1000之间的随机数,并将其用作元素数组的索引,这将给出 你随机的元素。
您可能需要稍微玩一下,因为您可能希望填充所有1000个数组元素,但这是一般的想法。
编辑它不是最有效的实现(至少在内存使用方面,它的运行时间应该是好的),但我选择了这个,因为它允许简洁的解释,不需要很多数学。
答案 2 :(得分:10)
首先,不需要指定default-empty节点的概率。其他概率应该以这样的方式定义,即如果没有创建其他类型,则创建空节点。
如何做到这一点并确保生成概率等于您指定的概率?简而言之:
对于你的例子:
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变体的问题。