如何编写一个易于维护的概率算法?

7

假设我想制作一个游戏。在游戏开始时,玩家将选择一个怪物。

公平地选择怪物是很容易的。

// get all monsters with equal chance
public Monster getMonsterFair(){
    Monster[] monsters = {new GoldMonster(), new SilverMonster(), new BronzeMonster()};
    int winIndex = random.nextInt(monsters.length);
    return monsters[winIndex];
}

并且不公平地选择了怪物。
// get monsters with unequal chance
public Monster getMonsterUnFair(){
    double r = Math.random();
    // about 10% to win the gold one
    if (r < 0.1){
        return new GoldMonster();
    }
    // about 30% to winthe silver one
    else if ( r < 0.1 + 0.2){
        return new SilverMonster();
    }
    // about 70% to win the bronze one
    else {
        return new BronzeMonster();
    }   
}

问题在于,当我向游戏中添加一个新怪物时,我必须编辑 if-else。或者,如果我将GoldMonster的获胜几率更改为0.2,那么我必须将所有的0.1更改为0.2。这很丑陋,也不易于维护。
// get monsters with unequal change & special monster
public Monster getMonsterSpecial(){
    double r = Math.random();
    // about 10% to win the gold one
    if (r < 0.1){
        return new GoldMonster();
    }
    // about 30% to win the silver one
    else if ( r < 0.1 + 0.2){
        return new SilverMonster();
    }
    // about 50% to win the special one
    else if ( r < 0.1 + 0.2 + 0.2){
        return new SpecialMonster();
    }
    // about 50% to win the bronze one
    else {
        return new BronzeMonster();
    }
}

如何重构这个概率算法,以便在添加新怪物和调整赢得怪物的几率时代码可以更容易地维护?


5
从字符串 GSSBBBBBBB 的随机位置选择一个字符。这种字符串很容易更改。 - Egor Skriptunoff
3个回答

3
基本上就是@Egor Skriptunoff说的那样。这应该很容易扩展。如果不想使用enum,可以使用Class<Monster>的集合。
enum Monster {
    GOLD(1),
    SILVER(3),
    BRONZE(6) // pseudo probabilities

    private int weight;
    // constructor etc..
}

public Monster getMonsterSpecial() {
    List<Monster> monsters = new ArrayList<>();

    for(Monster monsterType : Monster.values()) {
        monsters.addAll(Collections.nCopies(monsterType.getWeight(), monsterType)); 
    }

    int winIndex = random.nextInt(monsters.length);
    return monsters.get(winIndex);
}

你可以将枚举 Monsters 改为复数形式,并且让它指向一个 Class<? extends Monster> 类型,以便实例化怪物类。我只是试图使示例更加清晰易懂。

1
目前没有迹象表明这将会发生,但是如果有成千上万个怪物,它们的重量可能达到1000,那么这可能会成为一个瓶颈。 - Bernhard Barker
1
我认为你的意思是 monster.size() - Peter Lawrey

1

我会使用总重量,每添加一个怪物就增加一定重量。

private final Random rand = new Random();

public Monster getMonsterSpecial() {
    int weight = rand.nextInt(1+2+2+5);
    if ((weight -= 1) < 0) return new GoldMonster();
    if ((weight -= 2) < 0) return new SilverMonster();
    if ((weight -= 2) < 0) return new SpecialMonster();
    // 50% chance of bronze
    return new BronzeMonster();
}

添加怪物时仍需要添加 if 语句。 - Bernhard Barker
@Dukeling 无论如何,你都必须添加一些内容,而一行并不需要太多维护。有时候,拥有一个更全面的解决方案可能会掩盖它实际正在做的事情。;) - Peter Lawrey
1
我认为理想的解决方案是,您可以在不更改任何代码的情况下添加怪物(因此可以在运行时完成),这是我的答案可以提供的。虽然我认为甚至对于大型多人在线游戏,也没有人会在运行时进行游戏更新。 - Bernhard Barker
@Dukeling 有时候你是对的,需要一些简单且数据驱动的东西,例如从属性文件或CSV中获取。而有时候你需要能够在一个地方清楚地看到它正在做什么的东西。这真的取决于OP认为哪个对他的用例更简单。 - Peter Lawrey

1
这是基于Peter's answer的,但更易维护。您只需将新怪物添加到数组中并将其权重加入总权重即可 - 如果您希望在运行时轻松扩展此功能(因此,不要担心进行代码更改,甚至不需要重新启动程序即可添加怪物(假设您的程序的其余部分允许此操作))。

怪物类:

为每个怪物设置一个int权重变量。

如果权重为1、2和7,则各自的概率将为10%、20%和70%(计算公式为100*x/(1+2+7))。

全局变量:

Random rand = new Random();
int totalMonsterWeight;
Monster[] monsters; // set this up somewhere

全局权重初始化:
totalMonsterWeight = 0;
for (Monster monster: monsters)
  totalMonsterWeight += monster.getWeight();

获取怪物函数:
public Monster getMonster()
{
  int weight = rand.nextInt(totalMonsterWeight);
  for (Monster monster: monsters)
    if ((weight -= monster.getWeight()) < 0)
      return monster.getClass().newInstance();
}

上面的方法是一种懒惰的方式(可能不是最好的方式)在每次调用时返回一个新实例。正确的方法可能是使用工厂模式

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接