Question

Supposed I want to create a game. At the start of the game, the player will pick a monster.

It's easy to picks the monster fairly.

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

And picks the monster unfairly.

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

The problem is that, when I add a new monster to the game, I have to edit the if-else. Or I change the chance of winning GoldMonster to 0.2, I have to change all 0.1 into 0.2 .It's ugly, and not easily maintained.

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

How can this probability algorithm can be refactored so that the codes can be maintained easily when new monster is added and the chances of winning monsters are adjusted?

Was it helpful?

Solution

Basically what @Egor Skriptunoff said. This should scale easily. You could use a collection of Class<Monster> if you didn't want to use an enum.

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

You could perhaps make the enum Monsters plural, and have it point to a Class<? extends Monster> if you still want to instantiate monster classes. I just tried to make the example clearer.

OTHER TIPS

I would uses a total weight which increases with each monster added.

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

This is based off of Peter's answer, just more maintainable. All you have to do is add a new monster to the array and add the weight to the total weight - this can easily be extended to happen during runtime if you wish (thus, never mind making code changes, you don't even need to restart the program to add a monster (assuming the rest of your program allows this)).

Monster class:

Have an int weight variable for each monster.

If the weights are 1,2 and 7, the respective probabilities will be 10%, 20% and 70% (calculated as 100*x/(1+2+7)).

Globals:

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

Global weight initialization:

totalMonsterWeight = 0;
for (Monster monster: monsters)
  totalMonsterWeight += monster.getWeight();

Get-monster function:

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

The above is a lazy way (probably not the best way) to return a new instance during each call. The right way is probably using a Factory pattern.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top